From 399e3c17a70a7ec80fd5d9fd8fc821b270e7210c Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 26 Jul 2026 10:32:21 +0200 Subject: [PATCH] Refresh docs to reflect completed dyndns per-host auth fix README.md's credential-audit table and command-injection writeup still described the per-user auth binding gap as open; it's been closed since 2026-07-26. SETUP.md's TSIG section still taught the old single-shared- key pattern for a fresh install - replaced with the per-host key + update-policy pattern actually running in production now, plus a pointer to the phased-rollout approach documented in TODO.md. Co-Authored-By: Claude Sonnet 5 --- README.md | 11 ++++++++--- SETUP.md | 46 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b6bbeb5..ff81a39 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ Audited 2026-07-20 — which public hostnames require credentials: | Host | Requires credentials? | Notes | |---|---|---| | `jayfield.org` / `www.jayfield.org` | No (intentionally public static site) | Was accidentally 401'ing every visitor via a stray `.htaccess` — fixed, see `TODO.md` | -| `dyndns.jayfield.org` | Yes — HTTP Basic Auth | Vhost-level `` block, intentional. Had an OS command injection in its update scripts (fixed 2026-07-20, see `TODO.md`); the app itself still has no real per-user auth binding (open `TODO.md` item) | +| `dyndns.jayfield.org` | Yes — HTTP Basic Auth, one dedicated account per hostname | Vhost-level `` block, intentional. Had an OS command injection in its update scripts (fixed 2026-07-20) and no real per-user auth binding (fixed 2026-07-26 with per-host TSIG keys — see `TODO.md`) | | `mail.jayfield.org` | N/A — root is `403` (no content served there) | | | `mail.jayfield.org/rspamd` | Yes — rspamd's own login page | | | `cloud.jayfield.org` (Nextcloud) | Yes — redirects to `/login` | | @@ -385,6 +385,11 @@ broken legitimate dyndns updates — fixed with a POSIX ACL scoped to just that key file, not group membership, since the `bind` group also owns `rndc.key` — and (b) that access logging for every real vhost on this host is currently a no-op, and (c) that the dyndns app has no real -per-user auth binding. (b) and (c) are left open — see `TODO.md`.) +per-user auth binding. (b) fixed 2026-07-25 — see `TODO.md` for the root cause and the -per-vhost `CustomLog` fix; (c) remains open. +per-vhost `CustomLog` fix. (c) fixed 2026-07-26: every dyndns hostname +now has its own dedicated `.htpasswd` account and TSIG key, with BIND's +`update-policy` (not just an app-level check) refusing any update where +the authenticated user doesn't match the requested hostname — see +`TODO.md` for the full rollout, including the now-retired shared key +and legacy fallback. diff --git a/SETUP.md b/SETUP.md index cbb655f..bd1c16e 100644 --- a/SETUP.md +++ b/SETUP.md @@ -192,14 +192,48 @@ dig @127.0.0.1 jayfield.org +short # your own zone resolves ### TSIG for dynamic DNS (if you need a dyndns subdomain) +**Don't use one shared key for every hostname in the zone** — this host +originally did, and it meant any dyndns credential could update *any* +hostname, not just its own (found 2026-07-20, fixed 2026-07-26, see +`TODO.md`). Generate one key per hostname instead: + ```bash -sudo tsig-keygen -a hmac-sha256 dyndns.jayfield.org. | sudo tee /etc/bind/dyndns.jayfield.org.key -sudo chmod 640 /etc/bind/dyndns.jayfield.org.key -sudo chown root:bind /etc/bind/dyndns.jayfield.org.key # NOT 644 — this key is the only auth for dynamic updates +for h in ; do + sudo tsig-keygen -a hmac-sha512 "$h.dyndns.jayfield.org" | \ + sudo tee "/etc/bind/$h.dyndns.jayfield.org.key" + sudo chown root:bind "/etc/bind/$h.dyndns.jayfield.org.key" + sudo chmod 640 "/etc/bind/$h.dyndns.jayfield.org.key" + sudo setfacl -m u:www-data:r "/etc/bind/$h.dyndns.jayfield.org.key" # nsupdate runs as www-data +done ``` -Add a matching `key` block + `update-policy` (or a delegated zone) in -`named.conf.local` — see the actual working config on this host for the -exact `nsupdate` invocation pattern used by `/var/www/dyndns`. + +In `named.conf.default-zones`, `include` each key file and grant it +permission for *only* its own name via `update-policy` (not +`allow-update`, which can't be scoped per name): + +``` +include "/etc/bind/.dyndns.jayfield.org.key"; +include "/etc/bind/.dyndns.jayfield.org.key"; + +zone "dyndns.jayfield.org" { + type master; + file "/var/cache/bind/db.dyndns.jayfield.org"; + update-policy { + grant ".dyndns.jayfield.org" name .dyndns.jayfield.org. A AAAA TXT; + grant ".dyndns.jayfield.org" name .dyndns.jayfield.org. A AAAA TXT; + }; +}; +``` + +Give each hostname its own `.htpasswd` account too, and have the PHP +select the TSIG key based on `$_SERVER['PHP_AUTH_USER']` (the +*authenticated* identity) rather than whatever hostname the request +claims — that way BIND itself refuses a cross-host update even if the +app-level check were ever bypassed. See the live +`/var/www/dyndns/nsupdate*.php` on this host for the exact pattern, and +`TODO.md`'s 2026-07-26 dyndns entries for the full rollout writeup +(including how to phase in a legacy fallback for devices that need +reconfiguring first, without a hard cutover). ---