github.com/libdns/ednsde

Package ednsde implements a libdns provider for the eDNS DNS-01 challenge API offered by edns.de at https://dns-challenge.edns.de. The API is purpose-built for ACME DNS-01 challenges: it can add and remove TXT challenge records and does nothing else. In particular it has no endpoint that lists the records of a zone, so libdns.RecordGetter cannot be implemented honestly and libdns.RecordSetter cannot be implemented at all. Those methods are therefore deliberately absent rather than present-and- failing, so that misuse is caught by the compiler instead of during a certificate request. Only libdns.RecordAppender and libdns.RecordDeleter are provided, which is exactly what certmagic and Caddy require. The access token must be assigned to the zone on that zone's "DNS-01-Challenge" tab in the eDNS web interface, otherwise every request for it is answered with 401.


License
MIT
Install
go get github.com/libdns/ednsde

Documentation

eDNS for libdns

Go Reference

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.

Scope: challenge records only

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.

Usage

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>"},
})

Getting an access token

  1. In the eDNS web interface, go to SSL-Zertifikate → Automation-API-Verwaltung → API-Zugang anlegen and create a token.
  2. 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.

Behaviour worth knowing

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.

Testing

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 ./...

Licence

MIT