Skip to content

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 53Hosted 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 → PoliciesCreate policyJSON, 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:

      "Resource": [
        "arn:aws:route53:::hostedzone/ZONEID1",
        "arn:aws:route53:::hostedzone/ZONEID2"
      ]

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 → UsersCreate 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 credentialsCreate 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: SettingsCertificate issuanceDNS 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 saveCorp-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

SettingsRenewal 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:

  1. Pre-flight — CAA check, domain allowlist, Scout liveness, the refusal gates.
  2. 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.
  3. The Hub opens the ACME order and, for every authorization that still needs proving, UPSERTs the TXT record at TTL 120.
  4. The Hub waits for the record to become visible — up to 35 seconds, polling every 2 seconds — before telling the CA to look.
  5. The CA validates, the Hub finalizes with the Scout's CSR and downloads the chain.
  6. The Hub deletes the challenge record, on success and on failure alike.
  7. The Scout writes the bundle atomically — fullchain.pem, cert.pem, chain.pem, ca.pem, beside the privkey.pem it wrote before the CSR went out — validates the deployment, and runs your local deploy hook to reload the service.
  8. 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.