fix(caddy): the *arr stack and qBittorrent are LAN-only at the edge #151

Merged
dank merged 1 commits from fix/arr-lan-only into main 2026-11-04 13:49:34 -08:00
Owner

fix(caddy): the *arr stack and qBittorrent are LAN-only at the edge

Frank's ask was "get those off porkbun". Measured answer: they were never on
Porkbun
— the zone holds 20 A records and not one of them is prowlarr.,
radarr., lidarr., readarr. or qbittorrent., and there is no wildcard. But
the reason they were reachable anyway is worse than a DNS record, so this PR
closes the actual gate.

Why a missing record was never the gate

Caddy issues one certificate per name, and every issuance is published in
Certificate Transparency logs. crt.sh for %.franksdank.cloud returns 31 names,
including the whole *arr stack, qbittorrent., hermes., hermesadmin.,
pihole. and the retired sonarr./comfyui./chat.. Caddy also answers any
Host it knows, so a stranger who names a vhost reaches it; the only thing that
was stopping them was not knowing the name — and the name is public.

Proof the edge accepts them today: curl --resolve radarr.franksdank.cloud:443:…
reaches the upstream (the answer is the upstream's 502 right now, because
FrankBook-Pro is asleep — the gate is what answers, not the vhost list).

The change

Each of the five vhosts now carries a source gate:

radarr.franksdank.cloud {
	@lan remote_ip 127.0.0.1/32 ::1/128 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12
	handle @lan {
		reverse_proxy 192.168.0.112:7878
	}
	handle {
		respond 404
	}
}

remote_ip sees the true client because the G36 DNATs the 80/443 forward
straight to Caddy with no proxy hop. LAN clients are unaffected; anything from
the internet gets 404. The same block is documented in AGENTS.md (External
Exposure Policy) as the required shape for a household-only service, so the next
"no public record = internal" claim is not believed again.

Verification

  • caddy validate on the rendered config inside the running container: Valid
    configuration
    .
  • ./tests/run.sh: 841 ok.
  • The gate's split is provable without waiting for a WAN client once deployed: a
    LAN request must reach the upstream (a dead upstream still proves it passed the
    gate — its 502, not the gate's 404), and an external fetcher must get 404.

Deploy note

dankclaw/Caddyfile is in the FrankBook e2e blast radius, so the [3/3] gate will
pull in 19-frankbook.spec.ts — and FrankBook-Pro (192.168.0.112) is powered off
right now (ARP FAILED, no MAC on record to wake it), so that project cannot
pass regardless of this change. Run the deploy when the Mac is back, or accept
--skip-e2e for this one and re-run the project afterwards.


cwd: /home/blank/.omp/wt/wt-20261104-104444-6d1fc1f · session: 01a1a7eb-c584-72d3-9d7e-c02d981cd662

# fix(caddy): the *arr stack and qBittorrent are LAN-only at the edge Frank's ask was "get those off porkbun". Measured answer: **they were never on Porkbun** — the zone holds 20 A records and not one of them is `prowlarr.`, `radarr.`, `lidarr.`, `readarr.` or `qbittorrent.`, and there is no wildcard. But the reason they were reachable anyway is worse than a DNS record, so this PR closes the actual gate. ## Why a missing record was never the gate Caddy issues **one certificate per name**, and every issuance is published in Certificate Transparency logs. `crt.sh` for `%.franksdank.cloud` returns 31 names, including the whole *arr stack, `qbittorrent.`, `hermes.`, `hermesadmin.`, `pihole.` and the retired `sonarr.`/`comfyui.`/`chat.`. Caddy also answers any Host it knows, so a stranger who names a vhost reaches it; the only thing that was stopping them was not knowing the name — and the name is public. Proof the edge accepts them today: `curl --resolve radarr.franksdank.cloud:443:…` reaches the upstream (the answer is the upstream's `502` right now, because FrankBook-Pro is asleep — the gate is what answers, not the vhost list). ## The change Each of the five vhosts now carries a source gate: ``` radarr.franksdank.cloud { @lan remote_ip 127.0.0.1/32 ::1/128 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 handle @lan { reverse_proxy 192.168.0.112:7878 } handle { respond 404 } } ``` `remote_ip` sees the true client because the G36 DNATs the 80/443 forward straight to Caddy with no proxy hop. LAN clients are unaffected; anything from the internet gets `404`. The same block is documented in `AGENTS.md` (External Exposure Policy) as the required shape for a household-only service, so the next "no public record = internal" claim is not believed again. ## Verification - `caddy validate` on the rendered config inside the running container: **Valid configuration**. - `./tests/run.sh`: 841 ok. - The gate's split is provable without waiting for a WAN client once deployed: a LAN request must reach the upstream (a dead upstream still proves it passed the gate — its `502`, not the gate's `404`), and an external fetcher must get `404`. ## Deploy note `dankclaw/Caddyfile` is in the FrankBook e2e blast radius, so the [3/3] gate will pull in `19-frankbook.spec.ts` — and FrankBook-Pro (192.168.0.112) is powered off right now (ARP `FAILED`, no MAC on record to wake it), so that project cannot pass regardless of this change. Run the deploy when the Mac is back, or accept `--skip-e2e` for this one and re-run the project afterwards. --- cwd: /home/blank/.omp/wt/wt-20261104-104444-6d1fc1f · session: 01a1a7eb-c584-72d3-9d7e-c02d981cd662
dank added 1 commit 2026-11-04 13:45:28 -08:00
A missing Porkbun record was never the gate: Caddy issues one certificate per
name and every one is in public CT logs, so the *arr hostnames were discoverable
and the edge accepted them. Each of the five vhosts now carries a remote_ip
source gate (LAN proxies through, everything else gets 404) - the posture the
Sonarr comment already claimed. AGENTS.md records the LAN-gate shape as the
required form of internal-only, and Caddy.md records why DNS secrecy is fiction.
dank merged commit a3ddb4c65a into main 2026-11-04 13:49:34 -08:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#151