# 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` (`setgroups` → `setgid` → `setuid`). 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`.