Set up automatic DNS-01 (AWS Route 53)¶
What you'll have at the end: renewals that need no operator at all. The Hub
holds scoped AWS credentials, publishes the _acme-challenge TXT record itself
when a certificate is due, waits for it to become visible, lets the CA validate
it, and removes the record afterwards.
With no DNS provider configured the Hub cannot answer a DNS-01 challenge on its own. A profile set to manual falls back to showing the TXT record in the endpoint's drawer (name and value, each with a Copy button) and waiting for you; a profile set to automatic does not degrade — it fails, loudly, naming the missing configuration.
This lane is proven
Route 53 is the one automatic DNS-01 provider that has issued a real
certificate. On 2026-08-22 confluence.seecrypt.net renewed through it
against Let's Encrypt production — issued, installed, verified and the
renewal closed in 5.3 seconds, with nginx serving the new serial. The
Cloudflare and
Google Cloud DNS adapters are implemented but
have not yet been run against a live order.
Prerequisites:
- An AWS account with the public hosted zone for the domain you want certificates for. The zone must be the one that actually answers queries on the internet — a zone that is not delegated to Route 53 at the registrar will accept the record and still fail validation.
- Permission to create an IAM policy and an IAM user.
- Hub admin access.
- Outbound HTTPS from the Hub to
route53.amazonaws.com, and outbound DNS on port 53 from the Hub to arbitrary internet nameservers — see Network the Hub needs below. The second one is new and is easy to miss.
1. Find your hosted zone ID¶
AWS console → Route 53 → Hosted zones → click your domain.
The Hosted zone ID is on the detail panel — a string like
Z0123456789ABCDEFGHIJ. Copy it.
If you see two zones with the same name, take the public one. A private zone is only visible inside a VPC, and a CA cannot read it.
⚠️ Copy the whole ID. A one-character-short paste reached AWS and came back
404 with nothing shown on the page — that exact failure
(Z006781229GVM4GAO3AT for Z006781229GVM4GAO3ATC) cost a live debugging
session. The field now validates the shape in the browser, but it cannot know
which zone you meant.
2. Create a scoped IAM policy¶
IAM → Policies → Create policy → JSON, and paste this, replacing
ZONEID with the ID from step 1:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CyphersHubDns01",
"Effect": "Allow",
"Action": [
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/ZONEID"
}
]
}
Those are the only three calls the Hub makes: GetHostedZone is the health
check, ListResourceRecordSets reads the challenge record's existing values
before every write — the Hub publishes multi-value TXT records by
read-modify-write, so concurrent orders (a wildcard and its base name, or two
certificates in one zone) never overwrite each other's challenges — and
ChangeResourceRecordSets writes (UPSERT) and removes (DELETE) the
record. A policy missing ListResourceRecordSets fails the issuance with a
403 Forbidden on the /rrset?maxitems=1 read. Name the policy something
like cyphers-hub-dns01 and create it.
Scope it to the zones you mean. The Resource line is what stops these
credentials editing any other zone in the account. Do not substitute "*".
To cover a second zone, name both ARNs — one policy and one IAM user can serve several zones:
The split into separate entries happens in the Hub, not in AWS: a Route 53 credential holds exactly one hosted zone ID, so each zone is its own entry in step 4 — the same access key pasted twice, with a different hosted zone ID.
3. Create an IAM user and access key¶
IAM → Users → Create user. Give it a name like cyphers-hub-dns01
and do not grant console access — this identity is only ever used by the
Hub's API calls.
Attach the policy from step 2 directly (Attach policies directly → select
cyphers-hub-dns01).
Then open the user → Security credentials → Create access key → choose Application running outside AWS. Copy the Access key ID and Secret access key. The secret is shown once; if you lose it, delete the key and make another.
4. Enter the credentials in the Hub¶
In the Hub: Settings → Certificate issuance → DNS providers → expand Add DNS provider.
| Field | Value |
|---|---|
| DNS provider ID | A short name you choose, like corp-route53. This is what a renewal profile stores. |
| DNS provider name | A display label, like Corp production zones |
| Service | AWS Route 53 |
| Access key ID | from step 3 |
| Secret access key | from step 3 — use Show to proof-read the paste |
| Hosted zone ID | from step 1 |
| Region | us-east-1 — pre-filled; see below |
Click Save DNS provider.
The DNS provider ID takes 1–64 characters of letters, digits, - or _,
and is lowercased on save — Corp-Route53 is stored as corp-route53.
Keep it stable once a profile names it.
⚠️ Region must be us-east-1, whatever region your zone or your other AWS
resources are in. Route 53 is a global service: its endpoint is
route53.amazonaws.com and AWS requires requests to it be signed under the
us-east-1 credential scope. Entering the region your infrastructure happens
to live in produces a signature AWS rejects. The field is pre-filled with
us-east-1; the usual reason it ends up wrong is someone changing it on
purpose.
The credentials are stored AES-256-GCM encrypted under the CA passphrase and are write-only at the API surface — reads never return the secret. The Show toggle reveals what you have just typed into the form, not anything the Hub has stored.
One entry per zone, and entries do not replace each other. A Route 53 credential holds exactly one hosted zone ID, so a second zone — even in the same AWS account, under the same IAM user — is a second entry. Adding a Cloudflare or Google Cloud DNS entry does not disturb this one either. The Hub stores as many as you add, and each renewal profile picks the one that serves its domains (step 6).
Re-saving an existing DNS provider ID replaces that entry's credentials in place, without disturbing the profiles that name it. That is how you rotate an access key.
If you see a 'Hub default (legacy)' block
Earlier releases stored a single Hub-wide DNS provider, and a Hub upgraded from one still holds it. It is what every renewal profile that has not chosen a provider of its own uses — so removing it stops DNS-01 for all of them. Leave it until those profiles name a provider explicitly.
5. Prove the credentials¶
Saving runs the Hub's health check — a signed GetHostedZone against the
zone you named. It either confirms the credentials or fails immediately,
which is the point: it separates a credential problem from a certificate
problem before any CA is involved. The check runs before the write, so a
credential AWS rejects is never stored. The entry's Test button re-runs the
same check later.
Each stored entry also names the renewal profiles using it, and Remove is refused while any profile still does — it tells you which, rather than letting those renewals fail later with no sign of what changed.
A rejection is reported inside the DNS provider panel, next to the button you pressed. If you are on an older build that reports errors at the bottom of Settings, scroll: six consecutive AWS rejections once read as "the Save button does nothing".
| Symptom | Cause |
|---|---|
Signature / InvalidSignature error |
Region is not us-east-1 (step 4), or the secret was truncated on paste |
AccessDenied |
The policy is not attached to this user, or its Resource ARN names a different zone |
NoSuchHostedZone |
Hosted zone ID typo, or you copied a private zone's ID |
| Timeout | The Hub has no outbound HTTPS route to route53.amazonaws.com |
6. Point a renewal profile at it¶
Settings → Renewal Profiles → pick or create a profile → set Validation Method to DNS-01 (Automatic via API). A DNS Provider select appears beneath it:
| DNS Provider | What the profile does |
|---|---|
| The entry from step 4 | Publishes through those credentials, and only those |
| Hub default | Uses the legacy Hub-wide provider, if this Hub still holds one |
Only do this on a profile whose domains live in the hosted zone that entry
names. The Hub writes _acme-challenge.<domain> into that one zone; a domain
served from a different zone has nowhere for the record to land.
⚠️ A profile that names a provider never falls back to another one. If the
entry is removed, or its credentials will not decrypt, the renewal is refused
rather than published through whichever provider happens to be configured. That
refusal is deliberate: silently reaching for another entry would write
_acme-challenge into the wrong zone, and a configuration mistake would read
as a validation timeout instead of as a configuration mistake.
⚠️ Automatic does not fall back to manual. The choice is committed when
the renewal is dispatched, and if the Hub has no DNS provider at that moment
the renewal fails with "acme_dns01_direct selected but no DNS provider is
configured on this Hub". Renewal pre-flight does warn beforehand — an
Informational manual_dns01 finding — but its wording is written for the
manual lane and understates what happens here. Configure the provider first.
For a wildcard (*.example.com) the record the Hub writes is
_acme-challenge.example.com — the apex, not the literal * label, which is
not a legal DNS name.
7. Renew, and watch it happen¶
Nothing else is required. Click Renew Now on the endpoint in Manage, or enable autopilot and let the sweep do it.
What the Hub does, in order:
- Pre-flight — CAA check, domain allowlist, Scout liveness, the refusal gates.
- The Scout generates the private key — or loads the held one, on a key-reuse profile — and builds the CSR on the endpoint, submitting only the CSR. The key never leaves the host.
- The Hub opens the ACME order and, for every authorization that still needs
proving,
UPSERTs the TXT record at TTL 120. - The Hub waits for the record to become visible — up to 35 seconds, polling every 2 seconds — before telling the CA to look.
- The CA validates, the Hub finalizes with the Scout's CSR and downloads the chain.
- The Hub deletes the challenge record, on success and on failure alike.
- The Scout writes the bundle atomically —
fullchain.pem,cert.pem,chain.pem,ca.pem, beside theprivkey.pemit wrote before the CSR went out — validates the deployment, and runs your localdeployhook to reload the service. - A probe observes the endpoint serving the new certificate and the renewal closes.
On the proven run, steps 1–8 took 5.3 seconds end to end.
Why the wait in step 4 exists, and why it is 35 seconds¶
The lane originally published the record and asked the CA to validate in the
same breath. The write was fine; the CA simply looked before the record could
be there. Measured: order started, order invalid, renewal failed — 3.66
seconds, reported as the unhelpful orderNotReady.
⚠️ The 35-second budget is not free to grow. The entire Hub-side order runs inside the Scout's 60-second CSR request/reply window. Order submission, the post-validation poll, finalize and the certificate download all have to fit in what the DNS wait leaves behind. A larger DNS budget just trades a DNS timeout for a Scout timeout — which is strictly worse, because the Scout stops waiting while the Hub is still mid-order.
Why the Hub queries your nameservers directly¶
The propagation check does not go through the Hub's normal resolver. It
walks up the labels of the challenge name to the zone apex, resolves that
zone's NS records to addresses, and queries those servers directly with
caching switched off.
This is not fussiness. Challenge records carry TTL 120, so once anything has
resolved that name a recursive resolver keeps serving the old answer for up to
two minutes — longer than any budget that fits inside the Scout's window. On
2026-08-22 the Hub UPSERTed a new value, asked its system resolver, and got
the previous challenge value back for the whole 35 seconds while Route 53
had been correct within seconds. Let's Encrypt resolves from the root and
shares no cache with the Hub, so the pre-check was stricter than the validation
it exists to protect, and blocked an order that would have passed.
If the zone's nameservers cannot be determined the Hub falls back to the system resolver and logs a warning — a cached answer still beats no check.
Network the Hub needs¶
| To | Port | For |
|---|---|---|
route53.amazonaws.com |
443/TCP | Publishing and removing the TXT record, and the health check |
| Your zone's authoritative nameservers | 53/UDP+TCP | The propagation pre-check in step 4 |
| Your CA's ACME directory | 443/TCP | The order itself |
| The host's own system resolvers | 53 | CAA pre-flight, and finding the zone's nameservers in the first place |
⚠️ A network that only permits DNS to an internal forwarder will make the propagation check fall back to that forwarder's cache — the exact condition that cost a live renewal. Allow the Hub to reach public nameservers on 53.
When a renewal fails¶
| Message | Meaning | What to do |
|---|---|---|
"DNS TXT record <name> not found … Still not there after 35s, asked the zone's own authoritative nameservers" |
The record was published but never became visible at the zone's own servers. | Nothing was submitted to the CA and the order was not burned — retrying is safe. Check the zone is the one the registrar delegates to, and that the Hub can reach nameservers on port 53. |
"DNS TXT record <name> has wrong value. Expected: … Found: …" |
A different value is live at that name. | Usually a concurrent order, or a leftover record. The Hub's own cleanup is value-matched, so it never removes another order's record — check the zone for a stale entry. |
"ACME order is Invalid and cannot be finalized — <domain>: <CA's problem>" |
The CA looked and refused, and this is its own words. | Read the appended reason: it is the actual complaint (bad key authorization, unreachable nameserver, CAA), not a generic status. |
| "acme_dns01_direct selected but no DNS provider is configured on this Hub" | The profile is on the automatic lane and nothing resolves — it names no provider and the Hub holds no legacy default. | Configure one (steps 1–5) and select it on the profile (step 6), or move the profile to DNS-01 (Manual TXT). |
"Renewal profile <name> names DNS provider <id>, which no longer exists" |
The entry the profile selects has been removed. | Re-add it under the same ID (step 4), or select another entry on the profile — but only one that serves the same zone. |
"DNS provider <id> … could not be loaded … a CA passphrase rotation strands them" |
The entry exists but its credentials will not decrypt. | Re-enter the credentials under the same ID. Rotating the CA passphrase invalidates every stored DNS credential, exactly as it does stored ACME accounts. |
nginx -t reports key values mismatch after a renewal |
You are on a build from before 2026-08-22. | The lane used to publish cert.pem and ca.pem but not fullchain.pem, so a renewal rotated the key and left the previous certificate in the file your server reads. Upgrade the Scout. |
A leftover _acme-challenge record in the zone is also a version tell: before
2026-08-22 Route 53 cleanup sent a placeholder value, which Route 53 rejected,
so every challenge record was orphaned permanently. Cleanup now matches on the
real value. Sweep any old ones by hand.
What this does and does not cover¶
Automatic DNS-01 removes the manual TXT step, which is what makes unattended DNS-01 renewal — and therefore autopilot on a DNS-01 profile — possible at all.
It does not change which CA issues, whether a key is reused, or how the certificate is delivered. Those stay with the renewal profile.
Related¶
- Renew through a paid / third-party CA — the manual DNS-01 lane, and EAB
- Renewal lanes — how this lane compares
- Renewal profiles — validation method, and the rest of the profile
- Enable autopilot safely
- Recover from refusals