Skip to content

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.dev loses its /api PathPrefix rule entirely. The public hostname becomes Authentik-only, with no bypass of any kind.
  • A new hostname sabnzbd-api.media.cauda.dev carries the /api prefix to the SABnzbd backend for API-key clients, added to SABnzbd's host_whitelist.
  • Both site coredns-lan ConfigMaps get an exact record for the new hostname resolving to the local site's RFC1918 Gateway VIP, with a cauda.dev/config-revision annotation forcing a deterministic rollout.
  • The coherency checker gains rules for the private-exposure contract, and the cluster/gateway and cluster/coredns-lan AGENTS.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 /api path 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.dev was attached to main-home and main-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 Gateways private-home (10.1.30.13) and private-office (10.2.30.12), with private routes permitted to attach only to Gateways marked security.cauda.dev/exposure=private-lan-only.
  • What survives: the hostname split itself. Current cluster/gateway/routes/ media.yaml still has a sabnzbd route that is Authentik-only and a separate sabnzbd-api route carrying /api. What does not survive: the security.cauda.dev/authentik-bypass-* annotation vocabulary (replaced by security.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