ACM ACME Over AWS PrivateLink: Issue Public TLS Certificates From a VPC With No Internet Access
ACM ACME Over AWS PrivateLink: Issue Public TLS Certificates From a VPC With No Internet Access
On October 6, 2026, AWS Certificate Manager added AWS PrivateLink support for ACME issuance. A server in a subnet with no internet gateway and no NAT gateway can now request and renew a publicly trusted TLS certificate with a stock ACME client such as Certbot, and the issuance traffic never leaves the Amazon network. Until now, a locked-down VPC had two bad options for public certificates on self-managed servers: open an egress path to an ACME server on the internet, or issue the certificate somewhere else and copy the private key in. This guide covers the full setup: the ACM side, the VPC endpoint, an endpoint policy that actually works, and the client command.
What shipped, and what was already there
ACM has had a managed ACME server since mid-2026 (the AWS News Blog post is dated June 30, 2026). You create an ACME endpoint, pre-approve domains on it, hand a client a set of credentials, and any ACMEv2 client can pull certificates from Amazon Trust Services. What was missing was a private network path to that server. The October 6 launch adds it, in all commercial AWS Regions.
There are two separate VPC endpoint services, and mixing them up is the first mistake people make:
| VPC endpoint service | What it carries | Who needs it |
|---|---|---|
com.amazonaws.<region>.acm-acme | Management API calls such as CreateAcmeEndpoint, CreateAcmeDomainValidation, CreateAcmeExternalAccountBinding | PKI admin tooling running inside the VPC |
com.amazonaws.<region>.acm-acme-enroll | The ACME protocol itself: account registration, orders, finalize, download, revoke | Every ACME client (Certbot, cert-manager, acme.sh) |
They are independent. An endpoint for one gives no access to the other, and each has its own endpoint policy. For issuing certificates from private subnets you need acm-acme-enroll.
How ACM's ACME differs from Let's Encrypt
Three differences change how you set this up.
- Domains are pre-approved, not challenged. The endpoint uses
PRE_APPROVEDauthorization. An administrator proves domain ownership once with a CNAME record, and clients never answer an HTTP-01 or DNS-01 challenge. That is exactly why this works in a closed network: nothing needs to reach your server from outside, and the client needs no DNS credentials. - Clients register with External Account Binding (EAB). Each EAB is a key ID plus a MAC key, tied to an IAM role. ACM assumes that role at issuance, so IAM policies, SCPs and CloudTrail apply to ACME requests the same way they apply to
acm:RequestCertificate. - Certificates are valid for 45 days, and the private key never leaves the client. ACM does not renew these; your ACME client does. They also cannot be attached to ELB, CloudFront or API Gateway, because AWS does not hold the key.
Step 1: Create the ACME endpoint (admin, once)
1aws acm create-acme-endpoint \
2 --authorization-behavior PRE_APPROVED \
3 --contact REQUIRED \
4 --certificate-authority '{
5 "PublicCertificateAuthority": {
6 "AllowedKeyAlgorithms": ["EC_prime256v1", "RSA_2048"]
7 }
8 }'
The response contains an AcmeEndpointArn. Fetch the directory URL from it:
1aws acm describe-acme-endpoint \
2 --acme-endpoint-arn arn:aws:acm:us-east-1:111122223333:acme-endpoint/<endpoint-id> \
3 --query 'AcmeEndpoint.[Status,EndpointUrl]'
The URL has the form https://acm-acme-enroll.us-east-1.api.aws/<endpoint-id>/directory. Wait for ACTIVE before continuing.
Step 2: Pre-approve the domain
1aws acm create-acme-domain-validation \
2 --acme-endpoint-arn arn:aws:acm:us-east-1:111122223333:acme-endpoint/<endpoint-id> \
3 --domain-name internal.example.com \
4 --prevalidation-options '{
5 "DnsPrevalidation": {
6 "DomainScope": {
7 "ExactDomain": "ENABLED",
8 "Subdomains": "ENABLED",
9 "Wildcards": "DISABLED"
10 },
11 "HostedZoneId": "Z1234567890"
12 }
13 }'
With a Route 53 HostedZoneId, ACM provisions the CNAME for you. Without it, run aws acm describe-acme-domain-validation to read the CNAME name and value and create the record yourself. ACM looks for the record for up to 72 hours, then marks the validation INVALID with reason TIMED_OUT. The scope is the real control here: the example above lets clients issue for internal.example.com and any subdomain, but not a wildcard.
Validations are per endpoint. A second ACME endpoint for the same domain needs its own CNAME.
Step 3: Create the IAM role and the EAB credentials
The role must trust the ACME service principal:
1{
2 "Version": "2012-10-17",
3 "Statement": [{
4 "Effect": "Allow",
5 "Principal": { "Service": "acm-acme.amazonaws.com" },
6 "Action": ["sts:AssumeRole", "sts:TagSession", "sts:SetSourceIdentity"],
7 "Condition": {
8 "StringLikeIfExists": { "sts:SourceIdentity": "acm-acme-*" }
9 }
10 }]
11}
Its permissions policy needs acm:RequestCertificate to issue and acm:RevokeCertificate to revoke. Then create the binding and read the credentials:
1aws acm create-acme-external-account-binding \
2 --acme-endpoint-arn arn:aws:acm:us-east-1:111122223333:acme-endpoint/<endpoint-id> \
3 --role-arn arn:aws:iam::111122223333:role/AcmeIssuanceRole \
4 --expiration '{"Value": 7, "Type": "DAYS"}'
5
6aws acm get-acme-external-account-binding-credentials \
7 --acme-external-account-binding-arn <eab-arn>
The output is a KeyId and a MacKey. Treat the MAC key as a secret. Whoever creates the EAB also needs iam:PassRole on the role.
Step 4: Create the VPC interface endpoint
First check which Availability Zones the service supports in your Region, since AWS notes that some may not:
1aws ec2 describe-vpc-endpoint-services \
2 --service-names com.amazonaws.us-east-1.acm-acme-enroll \
3 --query 'ServiceDetails[0].AvailabilityZones'
Then create the endpoint in the same Region as the ACME endpoint. Cross-Region is not supported.
1aws ec2 create-vpc-endpoint \
2 --vpc-id vpc-0abc123 \
3 --service-name com.amazonaws.us-east-1.acm-acme-enroll \
4 --vpc-endpoint-type Interface \
5 --subnet-ids subnet-0aaa111 subnet-0bbb222 \
6 --security-group-ids sg-0endpoint \
7 --private-dns-enabled
Two details matter:
- Private DNS must be enabled. It makes the existing hostname
acm-acme-enroll.us-east-1.api.awsresolve to the endpoint's private IPs, so the directory URL in your client config does not change. Without it, clients cannot reach the ACME endpoint through the VPC endpoint. - The endpoint's security group must allow inbound TCP 443 from the subnets where your ACME clients run.
Step 5: Attach an endpoint policy that does not break issuance
The obvious policy, allowing only acm:RequestCertificate, fails. Most ACME protocol steps (account registration, order, authorization, download) are evaluated against the ACME endpoint resource and have no named IAM action, so the policy needs "Action": "*" scoped to that resource. Finalize and revoke are evaluated against the certificate resource.
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Principal": "*",
6 "Effect": "Allow",
7 "Action": "*",
8 "Resource": "arn:aws:acm:us-east-1:111122223333:acme-endpoint/<endpoint-id>"
9 },
10 {
11 "Principal": "*",
12 "Effect": "Allow",
13 "Action": ["acm:RequestCertificate", "acm:RevokeCertificate"],
14 "Resource": "arn:aws:acm:us-east-1:111122223333:certificate/*"
15 }
16 ]
17}
1aws ec2 modify-vpc-endpoint \
2 --vpc-endpoint-id vpce-0abc123 \
3 --policy-document file://acme-endpoint-policy.json
This pins the VPC endpoint to one ACME endpoint. It cannot tell callers apart, so decide who may issue what on the EAB role, not here.
Step 6: Issue the certificate from the private subnet
Confirm name resolution and reachability first:
1dig +short acm-acme-enroll.us-east-1.api.aws
2# expect private IPs from your VPC CIDR, e.g. 10.20.1.158
3
4curl -s -o /dev/null -w '%{http_code}\n' \
5 https://acm-acme-enroll.us-east-1.api.aws/<endpoint-id>/directory
6# expect 200
Then run Certbot:
1sudo certbot certonly \
2 --standalone \
3 --non-interactive \
4 --agree-tos \
5 --email ops@example.com \
6 --server https://acm-acme-enroll.us-east-1.api.aws/<endpoint-id>/directory \
7 --eab-kid <KeyId> \
8 --eab-hmac-key <MacKey> \
9 --issuance-timeout 120 \
10 --domain app.internal.example.com
--issuance-timeout 120 is not optional in practice. AWS documents that issuance can take up to two minutes, and many clients give up after 30 to 90 seconds. The certificate and key land in /etc/letsencrypt/live/app.internal.example.com/. Renewal is the usual certbot renew --quiet on a timer; when the same ACME account renews with the same names and key algorithm, ACM keeps the same certificate ARN.
To see what was issued:
1aws acm list-certificates --certificate-key-pair-origins ACME
ACME certificates are hidden from list-certificates unless you pass that filter.
A note on cert-manager
AWS lists cert-manager as a supported client, but its documentation only shows a Certbot example. In cert-manager, EAB goes in the standard externalAccountBinding block of an ACME ClusterIssuer (keyID plus a keySecretRef pointing at a Secret holding the MAC key), with server set to the directory URL. We have not run cert-manager against an ACM endpoint ourselves, so test in a non-production cluster first, and watch the two-minute issuance time against your controller's timeouts.
Best practices
- One EAB role per team or environment. Restrict each role with the
acm:DomainNamesandacm:KeyAlgorithmcondition keys so a leaked EAB cannot issue for someone else's hostname. - Set an expiration on EAB credentials. They are only needed for account registration. A 7-day EAB that is used once is safer than a permanent one sitting in a wiki.
- Alarm on
CertificateIssuanceFailedin theAWS/CertificateManagernamespace, dimensionAcmeEndpointArn. With 45-day certificates, a silently failing renewal becomes an outage quickly. - Use
CertificateTagson the endpoint so every issued certificate is tagged with its owner. The EAB role then also needsacm:AddTagsToCertificate.
Common mistakes to avoid
- Creating
acm-acmeinstead ofacm-acme-enroll. The first is the management API. Clients need the second. - Assuming a private hostname stays private. These are public certificates and are written to Certificate Transparency logs like any other ACM public certificate. If hostnames are sensitive, think about that before issuing.
- Expecting to revoke the EAB to stop a client. After registration, the ACME account lives independently. Revoking or deleting the EAB only blocks new registrations. Use
RevokeAcmeAccountto cut off an existing account. - Forgetting revocation checks. OCSP and CRL URLs are plain HTTP under
*.amazontrust.com. PrivateLink does not carry them, so clients inside a closed VPC that enforce revocation checking will need their own path to those hosts. - Trying to put the certificate on an ALB. Not supported for ACME-issued certificates. Use
RequestCertificatefor integrated services.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Certbot hangs, then Network is unreachable for acm-acme-enroll.<region>.api.aws | Hostname still resolves to public IPs | Enable private DNS on the VPC endpoint. Resolution can take a couple of minutes to switch after creation |
| Hostname resolves to public IPs on a custom DNS server | VPC endpoints only support Amazon-provided DNS | Add a conditional forwarder to the Route 53 Resolver for the hostname |
| Connection times out to a private IP | Security group on the endpoint | Allow inbound 443 from the client subnets |
| Access denied during account registration or order | Endpoint policy lists only named ACM actions | Add the "Action": "*" statement on the acme-endpoint ARN |
| Order fails for a name you expected to work | Domain scope | Check ExactDomain, Subdomains, Wildcards on the domain validation, and that its status is VALID |
Domain validation is INVALID with CAA_ERROR | CAA record excludes Amazon | Allow Amazon in the domain's CAA records |
| Client times out, certificate appears in ACM anyway | Client timeout under 120 seconds | Raise the issuance timeout |
FAQ
Do I have to change the ACME directory URL to use PrivateLink?
No. With private DNS enabled, the same acm-acme-enroll.<region>.api.aws hostname resolves to the VPC endpoint.
Does the domain validation CNAME go through PrivateLink too? No. Domain validation is a DNS record the administrator creates once. Clients do not perform any challenge.
Can a second VPC share the endpoint? A VPC endpoint's private DNS applies to its own VPC. For other VPCs, create an endpoint in each, or share name resolution to the first one. One practitioner write-up did this with a private hosted zone alias to the endpoint's DNS name.
What does it cost? Standard PrivateLink charges for the interface endpoint, plus ACM's ACME pricing, which is per domain name in each certificate at issuance, with a different rate for wildcards. Check both pricing pages for current numbers.
Is this available in GovCloud? The launch covers all commercial Regions. AWS said GovCloud (US), China and the European Sovereign Cloud would follow for ACME support.
Key takeaways
| Item | Value |
|---|---|
| Launch date | October 6, 2026 |
| Client-side VPC endpoint service | com.amazonaws.<region>.acm-acme-enroll |
| Admin API VPC endpoint service | com.amazonaws.<region>.acm-acme |
| Private DNS | Required |
| Certificate validity | 45 days, renewed by the client |
| Minimum client issuance timeout | 120 seconds |
| Endpoint policy | Action: * on the acme-endpoint ARN, plus RequestCertificate and RevokeCertificate on certificate/* |
| Works with ELB, CloudFront, API Gateway | No |
Further Reading
- Rotating SSL or TLS Certificate in AWS RDS
- Create Public and Private VPC With VPC Flow Logs Using Cloudformation
- Amazon EKS 1.37 Upgrade Guide: SELinuxMount, Kubelet Changes and the Auto Mode Default That Flips
- AWS docs: Issuing ACME certificates over AWS PrivateLink
- AWS docs: IAM for ACME certificate automation