ednsde

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 10 Imported by: 0

README

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. Two values on one name are no slower to publish than one.

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 how long it takes varies more than is comfortable. The API confirms an add or a remove immediately; when the authoritative nameservers start answering with it is a separate matter, and not one this package can influence. Measured by querying each of the zone's authoritative nameservers directly:

Operation Visible on all authoritative nameservers after
add, on a name with no records yet 6 s, 26 s and 36 s in three runs within the hour
add, two values on one fresh name 6 s — no slower than a single value
add, from a CI runner rather than a workstation over 120 s on one occasion, on a name and shape that took 6 s locally
remove ~307 s, consistently

The one figure that repeats is the removal: 307 s is almost exactly the 300 s record TTL, which says eDNS lets a removed answer expire rather than pushing the change. The spread on the add side has no explanation here that the measurements actually support, so treat the fast cases as luck, not as the contract.

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.

What follows for a caller: give the propagation check a generous budget. Ten minutes rather than the two-minute default. A tight timeout does not make eDNS faster; it just turns a slow publish into a failed certificate.

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

Documentation

Overview

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.

Index

Constants

View Source
const DefaultEndpoint = "https://dns-challenge.edns.de"

DefaultEndpoint is the production eDNS challenge API.

Variables

This section is empty.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status of the response. The eDNS API also repeats
	// it in the body, where the two always agree.
	StatusCode int

	// Message is the API's own description of the failure, passed through
	// unchanged.
	Message string

	// Hint carries advice the API itself does not give, most importantly for
	// 401, where an invalid token and a token that is merely not assigned to
	// the zone are indistinguishable.
	Hint string
}

APIError is returned for any non-200 answer from the eDNS API. Callers can use errors.As to reach it, for example to distinguish an authorization problem (401) from a malformed request (400).

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface, combining the API's own message with our hint where there is one.

type Provider

type Provider struct {
	// APIToken is the eDNS access token, sent as the X-API-TOKEN header. It
	// must be assigned to every zone this Provider is used for.
	APIToken string `json:"api_token,omitempty"`

	// Endpoint overrides the API base URL. It exists so tests can point the
	// Provider at a stub server; leave it empty to use [DefaultEndpoint].
	//
	// It is deliberately excluded from JSON, and the Caddy module does not
	// parse it either: a redirectable API URL in the certificate path is
	// configuration surface that buys nobody anything.
	Endpoint string `json:"-"`

	// HTTPClient overrides the HTTP client used for API calls.
	HTTPClient *http.Client `json:"-"`
}

Provider manages ACME challenge records through the eDNS challenge API.

A Provider is safe for concurrent use. Its fields are read-only once constructed and must not be modified afterwards.

func (*Provider) AppendRecords

func (p *Provider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)

AppendRecords creates the given challenge records in the zone and returns those that are now present.

Only TXT records are accepted. The TTL of the input is ignored: eDNS fixes challenge records at 300 seconds and offers no way to change that, so the returned records carry that value rather than the requested one.

A record that already exists is reported as created, because the resulting zone state is the one that was asked for.

func (*Provider) DeleteRecords

func (p *Provider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error)

DeleteRecords removes the given challenge records from the zone and returns those that were actually deleted.

Records that no longer exist are silently skipped: they are left out of the returned slice, as libdns requires, but are not treated as an error, so that cleaning up after a failed challenge stays quiet.

Unlike the general libdns contract, the value of a record may not be left empty. Deleting "everything with this name" would require reading the zone first, and the eDNS challenge API cannot list records. eDNS also only removes records that were created through this same API; records added by hand in the web interface are reported as not found.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL