DNS-01
The client publishes a TXT record proving control of the domain. This is the only challenge type that can authorize a wildcard, and the only one that requires no inbound connectivity to the client at all.
How it works
- The client is given a random
token. - It computes the key authorization:
token + "." + base64url(SHA256(JWK thumbprint)). - It publishes a TXT record at
_acme-challenge.<identifier>whose value isbase64url(SHA256(keyAuthorization)). - It triggers the challenge, and
acme-proxyqueries that name throughdns.resolver.
The value is the digest, not the key authorization. This is the single most common mistake when writing a DNS responder by hand:
http-01serves the key authorization verbatim,dns-01serves the base64url-encoded SHA-256 of it. The two are not interchangeable.
acme-proxy matches any TXT record present at the name. That is required
rather than lenient: an order covering both example.com and *.example.com
produces two authorizations that publish two different values to the same
_acme-challenge.example.com, and both must be able to validate.
Configuration
There is no [challenge.dns_01] table — unlike the other two types, dns-01
has no per-type key. Enable it, and point dns.resolver wherever your
authoritative data lives:
[challenge]
enabled = ["dns-01"]
[dns]
# host:port of the nameserver every lookup goes through.
resolver = "10.0.0.53:53"
Leaving dns.resolver unset uses the system configuration (/etc/resolv.conf).
The resolver is deliberately uncached
The shared resolver performs no caching. This matters here more than anywhere else: a client typically publishes its TXT record and triggers the challenge seconds later, and a cached negative answer — an NXDOMAIN or empty NOERROR from the moment before publication — would defeat validation for the whole negative-TTL window.
(The one component that does cache is filter.reverse_dns, which builds its
own resolver. PTR lookups for an address that keeps connecting are exactly what
a cache is for.)
What this does not remove is propagation delay in your own DNS infrastructure.
If dns.resolver points at a recursive resolver rather than the authoritative
server, the record still has to reach it. Pointing straight at the authoritative
nameserver is the reliable choice for an internal deployment.
Wildcards
[challenge]
enabled = ["dns-01"] # required for *.example.com to be accepted at all
Without dns-01 enabled, newOrder refuses a wildcard identifier with
rejectedIdentifier. With it enabled:
- the authorization is created on the base name, flagged
"wildcard": true; - it offers
dns-01and nothing else, regardless of what else is enabled; - the TXT record still goes to
_acme-challenge.example.com— there is no_acme-challenge.*.example.com.
Client support
Every major client implements dns-01, but each needs a provider plugin or hook
script to write the record: certbot’s --manual with an auth hook or a DNS
plugin, acme.sh --dns dns_<provider>, lego’s --dns <provider>.
If you would rather your clients did not hold DNS credentials, that is exactly
what the relay signer backend exists for:
your clients prove control to acme-proxy however you like, and acme-proxy
holds the single RFC 2136 TSIG key that answers the upstream CA.
Troubleshooting
| Event | Meaning |
|---|---|
challenge_dns_01_loaded | TXT records were retrieved for the name. |
challenge_dns_01_matched | One of them matched. Validation passed. |
challenge_validation_failed | No record matched, or the lookup failed. |
If the lookup returns nothing, check in this order: the record exists at
_acme-challenge.<name> (not at the name itself); its value is the digest;
dns.resolver can actually see the zone; and the record has finished
propagating to whatever dns.resolver points at.
A TXT record split into multiple character-strings is reassembled by concatenation before comparison, so a long value chunked by your DNS server is handled correctly.