A libdns provider for the eDNS DNS-01
challenge API (https://dns-challenge.edns.de) offered by edns.de.
It exists so that Caddy, CertMagic or any other libdns consumer can solve ACME DNS-01 challenges for zones hosted at eDNS — including wildcard certificates. For Caddy, use the module wrapper: caddy-dns/ednsde.
The eDNS challenge API is purpose-built for ACME. It can add and remove TXT challenge records, and it can do nothing else — in particular it has no endpoint that lists the records of a zone.
This provider therefore implements only two libdns interfaces:
| Interface | Implemented | Why |
|---|---|---|
libdns.RecordAppender |
yes | addChallengeRecord |
libdns.RecordDeleter |
yes | removeChallengeRecord |
libdns.RecordGetter |
no | the API cannot list records |
libdns.RecordSetter |
no | cannot be implemented without reading first |
The two unsupported methods are absent rather than present-and-failing. Go
interfaces are structural, so a stub returning "not supported" would still
satisfy libdns.RecordGetter and move the failure from compile time into the
middle of a certificate request. certmagic.DNSProvider requires only the two
interfaces above, so nothing is missing for Caddy.
This is not a general-purpose DNS management library. It cannot create A records, it cannot read your zone, and it will refuse anything that is not TXT.
import (
"context"
"github.com/libdns/libdns"
ednsde "github.com/libdns/ednsde"
)
provider := &ednsde.Provider{APIToken: os.Getenv("EDNS_TOKEN")}
added, err := provider.AppendRecords(context.Background(), "example.com.", []libdns.Record{
libdns.TXT{Name: "_acme-challenge", Text: "<43-character ACME digest>"},
})- In the eDNS web interface, go to SSL-Zertifikate → Automation-API-Verwaltung → API-Zugang anlegen and create a token.
- Open the zone you want to use it for and select that token on the zone's DNS-01-Challenge tab.
Step 2 is easy to miss. Without it every request for that zone is answered with
401, with the same message as an entirely invalid token — the API does not
distinguish the two cases. The error returned by this package says so.
One token can be assigned to several zones.
Names are passed through verbatim. The API does not add an _acme-challenge
prefix of its own; the libdns record name becomes the API's subdomain
parameter unchanged. A record on the zone apex (libdns name @) is sent with
the subdomain field omitted — sending it as an empty string is answered with
400.
TTL is not configurable. eDNS fixes challenge records at 300 seconds. The TTL of an input record is ignored, and the returned records report 300s, which is what is actually in the zone.
Challenge values must be 10–64 characters without whitespace. This is
validated before the request goes out, so you get a useful message instead of a
400. An ACME key authorization digest is 43 characters and always fits.
Several values may share one name. This is what a SAN certificate covering
both example.com and *.example.com needs, and it works — but add them in one
call. A value added to a name that already answers with one stays invisible for
about 300 seconds; see the propagation section below.
Deleting only affects records this API created. Records added by hand in the web interface are reported as not found. Deleting something that is not there is not an error, but it is not reported as deleted either.
Propagation is not instant, and what governs it is not the nameservers. Measured against a live zone by querying each authoritative nameserver directly:
| Operation | Visible on all authoritative nameservers after |
|---|---|
| add, on a name with no records yet | 23–27 s |
| add, alongside values sent in the same call | the same 23–27 s |
| add, on a name that already answers with a value | ~301 s |
| remove | ~307 s |
Those 300 s are the record's own TTL. eDNS answers a challenge name from a generation that lives for the TTL rather than publishing each change straight through, so a name that already answers keeps its answer until the generation expires. Exactly when a generation is minted is not something these measurements pin down — only that a fresh name is fast and a name in flight is not.
The removal lag is the least of it: ACME validation succeeds as long as some TXT record at the name carries the expected value, so a record left over from a previous run does no harm while it fades.
For ACME this is usually invisible, because a client presents all challenges of
an order before it waits for any of them: two values for example.com and
*.example.com go out in one AppendRecords call and appear together. It bites
on the second pass — a retried order, or a re-issuance while the previous value
is still visible. Allow for it: a propagation timeout of 10 minutes rather
than the two-minute default is the honest setting for this API.
A recursive resolver in the path adds your zone's SOA minimum on top. A
challenge name does not exist before the first issuance, and that NXDOMAIN is
cached for the SOA minimum (RFC 2308). This does not apply to CertMagic's own
check, which queries the authoritative nameservers directly unless you configure
resolvers; it does apply if you configure them, and to any client that checks
recursively. If that is your setup, check the minimum and keep it low:
dig +short SOA example.com | awk '{print "negative TTL:", $NF}'At 86400 a first issuance can stall for a day. At 300 it costs five minutes once.
Retries. Connection errors, 429 and 5xx are retried up to three times
with a short backoff, as libdns expects. 4xx responses are returned
immediately; they will not succeed on a retry.
Unit tests run everywhere and need no credentials:
go test ./...Integration tests talk to the real API and write into a real zone. They clean up after themselves, including when an assertion fails:
EDNS_TOKEN=... EDNS_TEST_ZONE=example.com go test -tags integration -v ./...MIT