AWS ACM module
Imports (and on renewal re-imports) the certificate into AWS Certificate Manager. AWS propagates a same-ARN re-import to every attached resource — CloudFront, ALB/ELB, API Gateway — so no CloudFront/ELB API calls are needed.
Overview
CloudFront, ALB/ELB and API Gateway don't accept a certificate directly — they reference an ACM certificate by ARN. When a certificate is re-imported into the same ARN, AWS automatically pushes the new key material to every resource using that ARN. This single module therefore covers all three services without touching their APIs.
- Type:
awsacm, credential type:aws_aksk(access key ID + secret access key), slotdefault. - Per target: resolve the ACM ARN →
acm:ImportCertificate(leaf + private key + intermediate chain) → persist the resulting ARN for the next renewal. - Rollback: supported — a rollback re-imports a previous retained certificate version to the ACM ARN, and AWS propagates it like any re-import (Rollback). ACM refuses a re-import that changes the key type or size, so a rollback across a key-algorithm change fails with
AWS_IMPORT.
Region rules
- CloudFront requires the certificate in
us-east-1— always, regardless of where your origin runs. - ALB/ELB and API Gateway use the resource's own region — set
regionaccordingly. - CloudFront accepts only ECDSA P-256 or RSA-2048 keys. An RSA-3072/4096 or ECDSA P-384 certificate imports into ACM fine but CloudFront rejects it on association — pick the certificate's key type accordingly.
- A pinned
certificate_arn(target or override) whose region segment disagrees with the targetregionis rejected up front — before any AWS call — withAWS_IMPORTinstead of surfacing as an opaque AWS ResourceNotFound. A remembered or tag-discovered ARN gets the same check after it is resolved.
Create the AWS credential
In the AWS console:
- IAM → Policies → Create policy → JSON — paste the policy below, name it (e.g.
CertAutoPilotACM). - IAM → Users → Create user (e.g.
certautopilot-acm), no console access. On the permissions step choose Attach policies directly and select the policy. - Open the user → Security credentials → Access keys → Create access key → use case Application running outside AWS → Create.
- Copy the Access key ID and Secret access key — the secret is shown only once.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CertAutoPilotACMCertificates",
"Effect": "Allow",
"Action": [
"acm:ImportCertificate",
"acm:DescribeCertificate",
"acm:ListTagsForCertificate",
"acm:AddTagsToCertificate"
],
"Resource": "arn:aws:acm:<region>:<account-id>:certificate/*"
},
{
"Sid": "CertAutoPilotACMList",
"Effect": "Allow",
"Action": "acm:ListCertificates",
"Resource": "*"
}
]
}
acm:ListCertificates does not support resource-level permissions, so its Resource must be *; the other actions are scoped to the certificate ARN pattern of the target's region and account (a new certificate has no ARN yet, hence the wildcard id). acm:AddTagsToCertificate is optional: it is used only to tag an adopted certificate after its first re-import (see Idempotent create); without it that step logs a warning and the deployment still succeeds. No CloudFront/ELB/API Gateway permissions are needed.
Then in CertAutoPilot: Settings → Distribution → Credentials → Add Credential → type AWS Access Key (AK/SK) (aws_aksk) → paste both values → Save.
Create an ACM target
Settings → Distribution → Targets → New → module AWS ACM (CloudFront / ALB / API Gateway).
| Field | Required | Description |
|---|---|---|
region | yes | AWS region of the ACM certificate (us-east-1 for CloudFront). |
certificate_arn | no | Re-import into this existing ACM certificate (adopt a cert already attached to CloudFront/ALB — keeps attachments). Empty → reuse the ARN remembered from a previous run, else create a new certificate. |
certificate_name | no | Name tag applied when a new certificate is created, and added to an adopted certificate that has no Name tag yet (an existing Name is never overwritten). Max 255 characters (AWS tag-value limit), no control characters — the same rule applies to the override. Empty = auto-generated per certificate as cap-<token>-<domain>. Both the ARN and the Name can be overridden per distribution from the certificate's Distributions → Overrides drawer. |
Pick the aws_aksk credential from the previous step.
Choosing from ACM (live picker)
The Certificate ARN field, on the target and on a certificate's per-target override row, can list the certificates in the target's region instead of asking for an ARN from the console. Each row is labelled by the domain it covers and carries its status, whether anything is currently using it, and its expiry.
One row fact matters more than the rest: a certificate ACM issued itself cannot be adopted. ACM refuses to import over its own certificates, so such a row says so and picking it would fail on every renewal. Leave the field empty to create a new certificate instead.
Ownership is deliberately not shown. Reading the certautopilot:certificate-id
tag costs one extra API call per certificate, and the deployment already resolves
ownership by tag when it runs; the picker's job is to make ARNs readable.
Listing is read-only and asks ACM for every key type (RSA 1024–4096, ECDSA P-256/P-384/P-521) — AWS's default listing returns only RSA 1024/2048. It reads pages of 50 and stops at 500 rows (12 pages) or the 28-second browse budget; the last row is marked truncated when more exist. The field still accepts a typed ARN, so an unreachable region never blocks the form.
Idempotent create — no duplicates
Every certificate the module creates is tagged certautopilot:certificate-id=<id>. On a run with no known ARN, the module first searches ACM (paginated ListCertificates with all key types requested — AWS's default returns only RSA 1024/2048, which used to hide ECDSA and RSA 3072/4096 certificates from discovery — pre-filtered by primary domain, then ListTagsForCertificate) for a certificate carrying that tag and re-imports into it instead of creating a duplicate. This closes the orphan window left by a cancelled or raced first run.
Discovery is best-effort: a denied or failed ListCertificates/ListTagsForCertificate, or no match, is treated as "not found" and the run creates a new certificate — it is not reported as an error.
ARN resolution ladder, each rung verified with DescribeCertificate: operator-set certificate_arn (override, then target) → ARN remembered from a previous run for this certificate → tag discovery → create new. A rung whose certificate was deleted in the console (ResourceNotFound) is skipped and the next one is tried, so a deleted pin falls back to the remembered ARN instead of hiding it; any other describe error keeps that ARN so the import reports the real problem. A pinned ARN is never rewritten — clear or replace a pin whose certificate was deleted.
Adopted certificates are tagged too. A certificate adopted by pin does not carry the ownership tag (ACM accepts tags only on a create). After its first successful re-import the module adds certautopilot:certificate-id (and Name when configured and absent) with AddTagsToCertificate, so the certificate stays discoverable if the pin is later cleared or the remembered state is lost. A tag already naming another certificate is left untouched. A denied AddTagsToCertificate is logged as a warning; the deployment still succeeds.
An ARN that names a certificate ACM issued itself (AMAZON_ISSUED / PRIVATE) is refused before the import with AWS_IMPORT (validation) — ACM only allows an import over an IMPORTED certificate.
First-time CloudFront attachment
The first import creates the ACM certificate but nothing references it yet. Once, in the CloudFront console:
- Edit the distribution → Settings.
- Add your domain under Alternate domain name (CNAME).
- Under Custom SSL certificate, select the ACM certificate the module created (its
Nametag =certificate_name). - Save and wait for the distribution to deploy (minutes).
Every subsequent renewal re-imports into the same ARN and CloudFront picks it up automatically — no console visits, no further configuration.
ACM's managed renewal applies only to certificates ACM itself issued. Imported certificates are never auto-renewed by AWS — CertAutoPilot drives every renewal and re-imports into the same ARN. Ignore the "renewal ineligible" note on imported certs in the ACM console.
Operations
| Operation | Behavior |
|---|---|
| Execute | Import (create) or re-import (in-place update) into ACM; stores the resulting ARN as remote state per certificate on the target, so one target can serve many certificates, each in its own ACM certificate. |
| DryRun | With an ARN: DescribeCertificate and report "update" (what it replaces; refuses an ACM-issued certificate); without: ListCertificates to prove credentials/region and report "create". |
| Validate | Implemented (DescribeCertificate — confirms the ACM cert exists) but not invoked by any workflow. End-to-end serving checks belong to a tls_fingerprint validation endpoint since CloudFront propagation is asynchronous. |
| Rollback | Supported — re-imports a previous retained certificate version to the same ARN (Rollback). |
| HealthCheck | ListCertificates in the target region (10 s timeout) — credential/region reachability. |
Error codes
| Code | Meaning | Resolution |
|---|---|---|
AWS_CREDENTIAL | Missing or malformed aws_aksk credential | Provide a credential JSON with access_key_id and secret_access_key. |
AWS_AUTH | AWS rejected the credentials or denied the action | Check the access key and that the IAM policy grants the acm:* actions above. |
AWS_CONNECT | Could not reach the AWS endpoint (also used for ACM throttling, which is retried as transient) | Check outbound network access to acm.<region>.amazonaws.com. |
AWS_IMPORT | ImportCertificate failed — bad chain, key mismatch, key type changed; or, before the import: ARN region ≠ target region, or the ARN names a certificate ACM issued itself | Chain must be intermediates-only; key must match the certificate; fix the region; clear an ACM-issued or wrong-key-type ARN to create a new certificate. |
Troubleshooting
Browser says the certificate isn't trusted behind CloudFront
CloudFront serves whatever you imported — including a private-CA certificate that public browsers won't trust. Viewer-facing CloudFront certificates must come from a publicly trusted CA (e.g. your ACME issuer), not from an internal AD CS CA.
CloudFront serves the *.cloudfront.net certificate instead of mine
The Alternate domain name (CNAME) is missing on the distribution, or the custom SSL certificate isn't selected. Complete the first-time attachment steps.
Renewal succeeded but CloudFront still serves the old certificate
Propagation after a re-import takes minutes, not seconds. Wait, or configure a validation endpoint with generous retries against the CloudFront domain to get an explicit signal when the new cert is live.
A duplicate ACM certificate appeared
Should not happen for module-created certs (tag discovery prevents it). It can if the certautopilot:certificate-id tag was manually removed, the certificate's ACM DomainName differs from the primary domain, the IAM policy lacks acm:ListTagsForCertificate (discovery then silently finds nothing), or an adopted certificate could never be tagged (look for the acm:AddTagsToCertificate warning in the run log). Set certificate_arn on the target to pin the correct one, and delete the orphan in the ACM console.