Correct the model: services stay PUBLIC, protected by forward-auth + 2FA (not Tailscale-only)
This commit is contained in:
47
AGENTS.md
47
AGENTS.md
@@ -13,14 +13,15 @@ That is backwards.
|
|||||||
|
|
||||||
**Outcome (the single success statement):**
|
**Outcome (the single success statement):**
|
||||||
|
|
||||||
> Exposing a service is a deliberate act. By default it is protected — and scans
|
> Every service stays reachable from anywhere — but reaching it means
|
||||||
> against the lab are blocked and visible, not absorbed.
|
authenticating. Scans against the lab are blocked and visible, not absorbed.
|
||||||
|
|
||||||
## 2. Scope
|
## 2. Scope
|
||||||
|
|
||||||
**In scope**
|
**In scope**
|
||||||
- Move admin UIs off the public internet.
|
- **Authenticate** the public surface — reachability stays, exposure does not.
|
||||||
- One authentication layer in front of the whole `*.lab` wildcard.
|
- One authentication layer in front of the `*.lab` wildcard, with a short explicit
|
||||||
|
unauthenticated allowlist.
|
||||||
- Intrusion detection and auto-banning.
|
- Intrusion detection and auto-banning.
|
||||||
- Secret scanning, code scanning, image scanning.
|
- Secret scanning, code scanning, image scanning.
|
||||||
- A correct-by-default auth helper for our own FastAPI apps.
|
- 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 |
|
| ID | Requirement |
|
||||||
|---|---|
|
|---|---|
|
||||||
| FR-1 | **Inventory.** Produce a table of every `*.lab` host: what it is, its backend, and whether it has auth. |
|
| 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-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-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. |
|
| 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
|
## 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.
|
2. **Never remove auth to "fix" a problem.** Fix the auth instead.
|
||||||
3. Do not break genuinely public sites — they go on the allowlist.
|
3. Do not break genuinely public sites — they go on the allowlist.
|
||||||
4. Alerts go through **Apprise**.
|
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.
|
Table of every `*.lab` host: purpose, backend, auth, public or not.
|
||||||
*DoD:* the table exists and is complete. **Nothing changes yet.**
|
*DoD:* the table exists and is complete. **Nothing changes yet.**
|
||||||
|
|
||||||
**Phase 2 — Admin UIs off the public internet**
|
**Phase 2 — Protect the public surface**
|
||||||
Move admin surfaces to Tailscale-only. Console, Portainer, Gitea, Paperclip first.
|
Put **Authelia forward-auth (SSO + 2FA)** in front of the sensitive hosts. They
|
||||||
*DoD:* admin UIs are **unreachable from the public internet** and reachable over
|
stay **publicly reachable** — that is the point. Console, Paperclip and Gitea must
|
||||||
Tailscale — proven both ways.
|
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**
|
**Phase 3 — CrowdSec + Caddy bouncer**
|
||||||
*DoD:* a deliberate scan from an external host is **banned**, and the ban is visible
|
*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
|
## 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 new `*.lab` host is protected by default.
|
||||||
- A scan from outside is banned automatically and visible.
|
- A scan from outside is banned automatically and visible.
|
||||||
- Repo history is scanned for secrets and findings are rotated.
|
- 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
|
## 13. Decisions and open questions
|
||||||
|
|
||||||
Resolved:
|
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.
|
2. Scanning is accepted as normal — the goal is **control**, not zero noise.
|
||||||
3. Authelia over Authentik (lighter, fits Caddy forward-auth).
|
3. Authelia over Authentik (lighter, fits Caddy forward-auth).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user