Files
2026-07-10 23:53:43 +02:00

152 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EVIDENCE.md — phase gate outputs (recorded 2026-07-10)
Test zone shown as `example.com` — a redaction of the real delegated sub-zone
used during the runs (publicly delegated to `1.2.3.4` → CoreDNS `:5053`).
To reproduce, that name will need changing: ACME needs a real zone to
validate against even on the staging CA (validation follows the public DNS
tree), and Let's Encrypt refuses to issue for `example.com` itself by policy
(`rejectedIdentifier`). API key redacted as `$PDNS_API_KEY`, server IP as
`1.2.3.4` throughout.
## Phase 1 — Go tests
```
$ go test ./... -count=1
ok git.bjphoster.com/source/coredns-powerdns-api-wrapper 1.440s
```
27 top-level tests (auth, zone list/detail, rrset aggregation + field order,
PATCH REPLACE/DELETE, TXT quoting both directions, FQDN normalization, notify,
trailing-dot equivalence, 404/422 bodies, config fail-fast, privdrop non-root
path). Isolated table `test_pdnsapi_records`; live `coredns_records` row count
identical before/after (4/4).
## Phase 2 — curl on localhost (2026-07-10T21:43 CEST)
```
$ curl -H "X-API-Key: $PDNS_API_KEY" http://127.0.0.1:3000/api/v1/servers/localhost/zones
[{"id":"example.com.","name":"example.com.","url":"/api/v1/servers/localhost/zones/example.com.","kind":"Native"}]
$ curl .../zones/example.com. # zone detail (trimmed)
{"id":"example.com.","name":"example.com.","kind":"Native","url":"...","rrsets":[
{"name":"example.com.","type":"A","ttl":300,"records":[{"content":"1.2.3.4","disabled":false}]},
{"name":"example.com.","type":"NS","ttl":300,"records":[{"content":"ns1.example.com.","disabled":false}]},
{"name":"example.com.","type":"SOA","ttl":300,"records":[{"content":"ns1.example.com. hostmaster.example.com. 0 44 55 66 300","disabled":false}]},
{"name":"www.example.com.","type":"CNAME","ttl":300,"records":[{"content":"example.com.","disabled":false}]}]}
$ PATCH REPLACE single token "test-token-123" → HTTP 204
$ wrong API key → {"error":"Unauthorized"} HTTP 401
$ missing zone (nosuchzone.org.) → {"error":"Not Found"} HTTP 404
$ malformed rrset (REPLACE, records: []) → {"error":"REPLACE requires at least one record"} HTTP 422
$ PATCH REPLACE two records on _acme-challenge → HTTP 204
$ SELECT name,ttl,content,record_type FROM coredns_records WHERE name LIKE '_acme%';
_acme-challenge 120 {"text":"token-for-example-com"} TXT
_acme-challenge 120 {"text":"token-for-www-example-com"} TXT
```
## Phase 3 — dig propagation
`dig @1.2.3.4` is not reachable from inside this host (hairpin NAT +
ISP-level UDP/53 interception, proven by getting an answer from an IP with no
DNS server). Verification ran against the same CoreDNS instance directly and,
externally, via public resolvers (see Phase 5 prep).
```
$ dig @127.0.0.1 -p 5053 TXT _acme-challenge.example.com +norecurse +noall +answer
_acme-challenge.example.com. 120 IN TXT "token-for-example-com"
_acme-challenge.example.com. 120 IN TXT "token-for-www-example-com" ← both values of the 2-record REPLACE
$ PATCH changetype:DELETE → HTTP 204, then:
$ dig ... → status: NXDOMAIN; SELECT COUNT(*) ... LIKE '_acme%' → 0
```
Record writes on an existing zone were visible immediately (no
zone_update_interval wait; the interval only gates new-zone visibility).
## Phase 4 — ngrok public endpoint (https://claude-test-endpoint.ngrok-free.dev)
```
$ zones via ngrok → identical body to Phase 2, HTTP 200
$ zone detail via ngrok → identical body to Phase 2, HTTP 200
$ PATCH REPLACE via ngrok → HTTP 204; dig @127.0.0.1 -p 5053 served "ngrok-test-token"
$ wrong key via ngrok → {"error":"Unauthorized"} HTTP 401
$ missing zone via ngrok → {"error":"Not Found"} HTTP 404
$ changetype BOGUS → {"error":"unknown or missing changetype"} HTTP 422
$ PATCH DELETE (cleanup) → HTTP 204
```
## Phase 5 — acme.sh staging E2E (zone example.com)
Zone setup: SOA/NS/A(apex)/A(ns1)/CNAME(www) rows inserted; CoreDNS served the
new zone after ~10s; external chain verified before the run:
```
$ curl 'https://dns.google/resolve?name=example.com&type=SOA'
{"Status":0,...,"Comment":"Response from 1.2.3.4."} ← public chain hits our CoreDNS
check-host.net: 10/10 nodes (DE HK IL×2 IN IR MD UA US×2) resolved A=1.2.3.4
```
First attempt failed with `During secondary validation: DNS problem: query
timed out` (transient reachability of one LE vantage point); retry succeeded
with zero acme.sh-side workarounds:
```
docker run --rm -v ~/acme.sh:/acme.sh -e PDNS_Url=... -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
[...] Adding TXT value: GzM5RMGKqZ... for domain: _acme-challenge.example.com
[...] The TXT record has been successfully added.
[...] Success for domain example.com / www.example.com
[...] Cert success.
Your cert is in: /acme.sh/example.com_ecc/example.com.cer
```
Server log (request sequence per acme.sh call): GET /zones 200 (_get_root) →
GET /zones/{zone} 200 (existing challenges) → PATCH 204 → PUT notify 200;
cleanup DELETE PATCHes 204. After cleanup: DB `_acme%` rows = 0, dig NXDOMAIN.
```
$ openssl x509 -noout -issuer -ext subjectAltName -dates
issuer=C=US, O=Let's Encrypt, CN=(STAGING) Artificial Amaranth YE1
DNS:example.com, DNS:www.example.com
```
## Phase 6 — container image E2E + privilege drop
```
$ APP_VERSION=0.1.0 make docker → git.bjphoster.com/source/coredns-powerdns-api-wrapper:0.1.0 (7.94MB, scratch)
$ docker run -d --name pdns-api-e2e --network host --env PDNS_API_KEY \
--env MYSQL_DSN=... --env PUID=4321 --env PGID=4321 <image>
$ docker logs pdns-api-e2e
level=INFO msg="privileges dropped" uid=4321 gid=4321
level=INFO msg="mysql pool ready"
level=INFO msg=listening addr=:3000
$ docker top pdns-api-e2e -eo pid,uid,gid,comm ← host view
PID UID GID COMMAND
446573 4321 4321 app ← NOT root
$ curl http://127.0.0.1:3000/healthz → {"status":"ok"}
```
Full Phase 5 re-run against the container (cached LE authorizations
deactivated first so dns-01 actually re-ran):
```
[...] Verifying: example.com → Success
[...] Verifying: www.example.com → Success
[...] Cert success.
```
Container request log shows the same GET/PATCH/notify sequence; post-run DB
`_acme%` rows = 0, dig NXDOMAIN, cert dated Jul 10 19:19:58 2026 GMT with both
SANs, staging issuer.
## Definition-of-done greps
```
$ grep -ri yaml --exclude-dir=.git . → only CLAUDE.md/PLAN.md prose;
nothing config-related in Go sources, makefile, or dockerfile
(update_child_repos.sh, the last stray reference, has since been deleted)
$ gofmt -l . → clean; go vet ./... → clean
```