Files
coredns-powerdns-api-wrapper/README.md
2026-07-10 23:53:43 +02:00

3.3 KiB

PowerDNS APIs for CoreDNS

A Go service exposing the subset of the PowerDNS Authoritative HTTP API that acme.sh's dns_pdns provider uses, translating rrset operations into the MySQL table read by the CoreDNS mysql plugin. Contract in one sentence: PowerDNS at the HTTP edge, CoreDNS rows at the DB edge.

Configuration

Configuration is environment variables only — there is no config file.

Variable Purpose Default
PDNS_API_KEY Shared secret for X-API-Key (required, no default)
LISTEN_ADDR HTTP listen address :3000
MYSQL_DSN Go MySQL DSN (user:pass@tcp(host:3306)/dbname?parseTime=true) (required)
TABLE_PREFIX Table name prefix; must mirror the Corefile table_prefix directive (e.g. coredns_) coredns_
PDNS_SERVER_ID Server id in API paths localhost
DEFAULT_TTL TTL when acme.sh doesn't send one 120
LOG_LEVEL debug / info / warn / error info
PUID UID to drop privileges to after startup (required)
PGID GID to drop privileges to after startup (required)

Startup fails fast, naming the missing variable, if a required one is absent.

Privilege drop

The container starts the binary as root. Before serving any traffic, the binary irrevocably drops to PGID/PUID (setgroupssetgidsetuid). The image does not declare a USER directive — the drop is the binary's job, not the container's. If started as a non-root user already, the drop is skipped and the process serves as the invoking user.

Running

docker run --rm \
  --publish 3000:3000 \
  --env PDNS_API_KEY=changeme \
  --env MYSQL_DSN="coredns:coredns@tcp(127.0.0.1:3306)/coredns?parseTime=true" \
  --env TABLE_PREFIX=coredns_ \
  --env PDNS_SERVER_ID=localhost \
  --env DEFAULT_TTL=120 \
  --env LOG_LEVEL=info \
  --env PUID=1000 \
  --env PGID=1000 \
  git.bjphoster.com/source/coredns-powerdns-api-wrapper:latest

/healthz is an unauthenticated health check that pings MySQL; it exposes no data.

acme.sh usage

docker run -it --rm -v ~/acme.sh:/acme.sh \
  -e PDNS_Url="https://your-endpoint" \
  -e PDNS_ServerId="localhost" \
  -e PDNS_Token="$PDNS_API_KEY" \
  -e PDNS_Ttl="120" \
  neilpang/acme.sh --issue --staging --dns dns_pdns \
  -d example.com -d www.example.com

For production issuance, drop --staging (and optionally add --server letsencrypt).

Implemented API surface, all under /api/v1, all requiring X-API-Key:

  • GET /api/v1/servers/{server_id}/zones
  • GET /api/v1/servers/{server_id}/zones/{zone_id}
  • PATCH /api/v1/servers/{server_id}/zones/{zone_id} (rrsets, REPLACE/DELETE)
  • PUT /api/v1/servers/{server_id}/zones/{zone_id}/notify

Propagation / --dnssleep guidance

Record writes to an existing zone are visible to CoreDNS immediately — record lookups hit MySQL live, so no --dnssleep is needed for TXT changes. Only newly created zones are subject to the Corefile's zone_update_interval (120s), since that setting only gates the cached zone list. Verified empirically; see docs/SEMANTICS.md.

Known limitations

  • Only the acme.sh API slice is implemented: no zone CRUD beyond GET, no cryptokeys/metadata/search/TSIG, no DNSSEC.
  • This service serves no DNS itself — CoreDNS remains the resolver.
  • rrset types supported for writes: TXT, A, AAAA, CNAME, NS.