0043. Isolate the SABnzbd API on a private hostname¶
- Status: superseded by ADR-0037 — hostname split stands, isolation mechanism replaced
- Date: 2026-07-17
- Deciders: lusoris
- Related: PR #2617, PR #2616, PR #2619, BUG-43, BUG-44
Context¶
SABnzbd browser integrations cannot complete an Authentik browser flow, so they
authenticate to SABnzbd with its API key instead. To accommodate that, the
sabnzbd HTTPRoute carried an unauthenticated /api PathPrefix rule pointing
straight at the SABnzbd backend on the single public hostname
sabnzbd.media.cauda.dev, with the route attached to both main-home and
main-office. Everything else on that hostname went through the Authentik proxy
outpost.
The Cloudflare catch-all terminates on those same Gateways. The /api bypass
was therefore not private at all: a forced public-edge request that bypassed LAN
split DNS entirely (DoH-resolved edge 188.114.96.3) returned HTTP 200 for
https://sabnzbd.media.cauda.dev/api?mode=version.
The route carried security.cauda.dev/authentik-bypass-* annotations declaring
that "every public cloudflared ingress must deny this declared path before
catch-all", and the repo's cloudflared-* ConfigMaps did contain those deny
rules. They were inert. The audit
(.workingdir2/bugs/sabnzbd-cloudflare-remote-config-state-audit-2026-07-17.md)
captured live connector behaviour: every connector starts from the mounted
ConfigMap and then logs a Cloudflare-pushed Updated to new configuration. The
pushed runtime config at both sites contained no SAB guard, and contained
rules absent from the repo entirely. The tunnels are remote-managed; the
in-repo ConfigMaps are documentation of intent, not enforcement.
Repairing that through Terraform was attempted in PR #2616 and blocked. The
canonical local state contains zero Cloudflare resources, so all four tunnel
and tunnel-config resources are absent; terraform.tfvars is missing
cloudflare_account_id, both tunnel secrets and the Authentik client secret;
and the stored API token fails Cloudflare's own /user/tokens/verify with HTTP
400. A diagnostic plan returned 4 to add, proving a -target apply would pull
in the absent tunnel resources rather than touching only the two configs. The
audit's verdict was BLOCKED / DO NOT APPLY, tracked as BUG-43.
The exposure had to be closed through GitOps without depending on that state.
Decision¶
Split the hostname, and stop serving unauthenticated traffic on the public one.
sabnzbd.media.cauda.devloses its/apiPathPrefix rule entirely. The public hostname becomes Authentik-only, with no bypass of any kind.- A new hostname
sabnzbd-api.media.cauda.devcarries the/apiprefix to the SABnzbd backend for API-key clients, added to SABnzbd'shost_whitelist. - Both site
coredns-lanConfigMaps get an exact record for the new hostname resolving to the local site's RFC1918 Gateway VIP, with acauda.dev/config-revisionannotation forcing a deterministic rollout. - The coherency checker gains rules for the private-exposure contract, and the
cluster/gatewayandcluster/coredns-lanAGENTS.md invariants are updated.
What actually provided the isolation was the Gateway layer, not the tunnel
configuration. The PR summary described the change as enforcing "absence from
Cloudflare tunnel/Terraform declarations", and that framing is wrong: as
established above, editing a cloudflared-* ConfigMap changes nothing at
runtime. The public exposure closed because the unauthenticated /api rule was
removed from a route attached to publicly-terminating Gateways. Split DNS
governs resolution, not reachability, and the cloudflared denies never fired.
This is recorded explicitly because that same misunderstanding later cost two
attempts on the Harbor exposure (BUG-48), where a ConfigMap-level fix rendered,
linted and synced green while enforcing nothing.
Alternatives considered¶
- Repair the Cloudflare tunnel configuration through Terraform (PR #2616). Rejected as unsafe and, per the evidence above, not the actual control point. The state is absent, the credentials invalid, and an apply would create four resources rather than update two. Still open as BUG-43.
- Keep the single hostname and rely on the in-repo cloudflared deny rules. Rejected: the live-config capture proved those rules were not present in the pushed runtime configuration.
Consequences¶
- The public SABnzbd hostname has no authentication bypass; the previously
reachable
/apipath is gone from it. - BUG-43 moved from INVESTIGATING to MITIGATED — the public API is closed, but Cloudflare/Terraform state reconciliation remains blocked and unresolved.
- This decision's isolation mechanism was superseded roughly forty minutes
after it merged.
sabnzbd-api.media.cauda.devwas attached tomain-homeandmain-office— the same Gateways the Cloudflare catch-all uses as origins. Once public wildcard DNS resolved the new name, requests reached SABnzbd again despite the private DNS intent. That is BUG-44, fixed by PR #2619 (merged 21:17Z against this PR's 20:37Z) and recorded as ADR-0037: dedicated LAN-only Gatewaysprivate-home(10.1.30.13) andprivate-office(10.2.30.12), with private routes permitted to attach only to Gateways markedsecurity.cauda.dev/exposure=private-lan-only. - What survives: the hostname split itself. Current
cluster/gateway/routes/ media.yamlstill has asabnzbdroute that is Authentik-only and a separatesabnzbd-apiroute carrying/api. What does not survive: thesecurity.cauda.dev/authentik-bypass-*annotation vocabulary (replaced bysecurity.cauda.dev/exposure/private-gateways/private-dns-records), the attachment to the shared public Gateways, and the DNS targets — LAN records now point at.13/.12, not the main VIPs. - The pattern established by the superseding ADR was later reused verbatim to close the Harbor exposure (BUG-48, 2026-07-20), which is the strongest evidence that the Gateway-attachment boundary, not this ADR's split DNS, is the durable control.
References¶
- PR #2617 — fix(media): isolate SABnzbd API on private hostname
- PR #2619 — fix(gateway): isolate private API routes (the superseding change)
- PR #2616 — the blocked Cloudflare target-plan attempt
.workingdir2/bugs/sabnzbd-cloudflare-remote-config-state-audit-2026-07-17.md— Terraform state, provider-auth and live pushed-config evidence.workingdir2/bugs/08-known-bugs.md— BUG-43, BUG-44, BUG-48, and the "cloudflared ConfigMaps are dead config" callout