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

89 lines
3.3 KiB
Markdown

# 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`.