From 2441528ac738716ae4df08c8efcfbdfa17c23bed Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Wed, 14 Oct 2026 14:47:33 +1100 Subject: [PATCH] Correct the model: services stay PUBLIC, protected by forward-auth + 2FA (not Tailscale-only) --- AGENTS.md | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4217126..8f46b19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,14 +13,15 @@ That is backwards. **Outcome (the single success statement):** -> Exposing a service is a deliberate act. By default it is protected — and scans -> against the lab are blocked and visible, not absorbed. +> Every service stays reachable from anywhere — but reaching it means +authenticating. Scans against the lab are blocked and visible, not absorbed. ## 2. Scope **In scope** -- Move admin UIs off the public internet. -- One authentication layer in front of the whole `*.lab` wildcard. +- **Authenticate** the public surface — reachability stays, exposure does not. +- One authentication layer in front of the `*.lab` wildcard, with a short explicit + unauthenticated allowlist. - Intrusion detection and auto-banning. - Secret scanning, code scanning, image scanning. - A correct-by-default auth helper for our own FastAPI apps. @@ -60,7 +61,7 @@ The bypass is fixed. **This project fixes the conditions that made it matter.** | ID | Requirement | |---|---| | FR-1 | **Inventory.** Produce a table of every `*.lab` host: what it is, its backend, and whether it has auth. | -| FR-2 | **Admin UIs are not public.** Console, Portainer, Gitea, Paperclip, and every other admin surface are reachable **only over Tailscale**. | +| FR-2 | **Every public host is authenticated.** Console, Paperclip, Gitea and the rest stay **reachable from the internet**, because they must work from any machine with no client software. They sit behind **forward-auth (SSO + 2FA)**, or their own login where that is stronger. Only an explicit allowlist is unauthenticated (`maps.lab`). | | FR-3 | **Default-deny at Caddy.** Every `*.lab` host passes through **forward-auth**. A host is protected unless it is on an explicit public allowlist. | | FR-4 | **SSO + 2FA** (Authelia) in front of the wildcard, with per-host policy. | | FR-5 | **CrowdSec** reads the Caddy and sshd logs, detects scans, and **auto-bans** via a Caddy bouncer. | @@ -112,7 +113,9 @@ Authelia and CrowdSec ship their own UIs — do not restyle them. ## 10. Rules -1. **Fail closed.** A new host is protected by default; exposure is explicit. +1. **Fail closed means authenticated, not hidden.** Public reachability is +expected and required. A new host is protected **by default**; the unauthenticated +public allowlist is explicit and short. 2. **Never remove auth to "fix" a problem.** Fix the auth instead. 3. Do not break genuinely public sites — they go on the allowlist. 4. Alerts go through **Apprise**. @@ -126,10 +129,23 @@ Authelia and CrowdSec ship their own UIs — do not restyle them. Table of every `*.lab` host: purpose, backend, auth, public or not. *DoD:* the table exists and is complete. **Nothing changes yet.** -**Phase 2 — Admin UIs off the public internet** -Move admin surfaces to Tailscale-only. Console, Portainer, Gitea, Paperclip first. -*DoD:* admin UIs are **unreachable from the public internet** and reachable over -Tailscale — proven both ways. +**Phase 2 — Protect the public surface** +Put **Authelia forward-auth (SSO + 2FA)** in front of the sensitive hosts. They +stay **publicly reachable** — that is the point. Console, Paperclip and Gitea must +work from any browser, on any machine, with no client software installed. + +Per-host policy: + +| Host | Policy | +|---|---| +| console, Paperclip, Portainer | forward-auth + **2FA** (owner only) | +| Gitea | **public read** for anonymous viewers; forward-auth + 2FA for **write** | +| `maps.lab` | public allowlist, **no auth** — Obsidian and other apps embed it | +| anything not needed publicly | simply not routed publicly | + +*DoD:* signing in from a machine **with no Tailscale and no VPN** works. An +unauthenticated request to a protected host is refused. Public read still works on +Gitea and maps. **Phase 3 — CrowdSec + Caddy bouncer** *DoD:* a deliberate scan from an external host is **banned**, and the ban is visible @@ -157,7 +173,9 @@ A shared FastAPI dependency that verifies the password, plus the test. ## 12. Acceptance criteria -- No admin UI is reachable from the public internet. +- Every protected host refuses unauthenticated access **and** works from a machine + with no Tailscale and no VPN. +- Gitea read is public for employers; Gitea write requires 2FA. - A new `*.lab` host is protected by default. - A scan from outside is banned automatically and visible. - Repo history is scanned for secrets and findings are rotated. @@ -167,7 +185,12 @@ A shared FastAPI dependency that verifies the password, plus the test. ## 13. Decisions and open questions Resolved: -1. `*.lab` stays **public** for genuine public sites. Only admin UIs move. +1. **Everything stays public.** Console, Paperclip and Gitea MUST work from an + arbitrary machine on the road, with no Tailscale and no VPN. The fix is + **stronger auth**, not hiding them. Tailscale is for SSH and for services that + genuinely do not need public access. +2. `*.lab` stays **public**. The unauthenticated allowlist is short and explicit + (`maps.lab`). 2. Scanning is accepted as normal — the goal is **control**, not zero noise. 3. Authelia over Authentik (lighter, fits Caddy forward-auth).