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 serviceWhat it carriesWho needs it
com.amazonaws.<region>.acm-acmeManagement API calls such as CreateAcmeEndpoint, CreateAcmeDomainValidation, CreateAcmeExternalAccountBindingPKI admin tooling running inside the VPC
com.amazonaws.<region>.acm-acme-enrollThe ACME protocol itself: account registration, orders, finalize, download, revokeEvery 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.

  1. Domains are pre-approved, not challenged. The endpoint uses PRE_APPROVED authorization. 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.
  2. 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.
  3. 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.aws resolve 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:DomainNames and acm:KeyAlgorithm condition 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 CertificateIssuanceFailed in the AWS/CertificateManager namespace, dimension AcmeEndpointArn. With 45-day certificates, a silently failing renewal becomes an outage quickly.
  • Use CertificateTags on the endpoint so every issued certificate is tagged with its owner. The EAB role then also needs acm:AddTagsToCertificate.

Common mistakes to avoid

  • Creating acm-acme instead of acm-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 RevokeAcmeAccount to 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 RequestCertificate for integrated services.

Troubleshooting

SymptomLikely causeFix
Certbot hangs, then Network is unreachable for acm-acme-enroll.<region>.api.awsHostname still resolves to public IPsEnable 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 serverVPC endpoints only support Amazon-provided DNSAdd a conditional forwarder to the Route 53 Resolver for the hostname
Connection times out to a private IPSecurity group on the endpointAllow inbound 443 from the client subnets
Access denied during account registration or orderEndpoint policy lists only named ACM actionsAdd the "Action": "*" statement on the acme-endpoint ARN
Order fails for a name you expected to workDomain scopeCheck ExactDomain, Subdomains, Wildcards on the domain validation, and that its status is VALID
Domain validation is INVALID with CAA_ERRORCAA record excludes AmazonAllow Amazon in the domain's CAA records
Client times out, certificate appears in ACM anywayClient timeout under 120 secondsRaise 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

ItemValue
Launch dateOctober 6, 2026
Client-side VPC endpoint servicecom.amazonaws.<region>.acm-acme-enroll
Admin API VPC endpoint servicecom.amazonaws.<region>.acm-acme
Private DNSRequired
Certificate validity45 days, renewed by the client
Minimum client issuance timeout120 seconds
Endpoint policyAction: * on the acme-endpoint ARN, plus RequestCertificate and RevokeCertificate on certificate/*
Works with ELB, CloudFront, API GatewayNo

Further Reading