Files
network_security/AGENTS.md

9.0 KiB

Network Security

Build spec for Paperclip. Follow the phases in section 11 in order. Do not skip a phase's Definition of Done. When something is unclear, ask Sam — do not guess.


1. Purpose and outcome

Make the lab fail closed. Today a new service is exposed to the internet the moment it exists, and the only thing protecting it is remembering to add auth. That is backwards.

Outcome (the single success statement):

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

  • 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.

Out of scope

  • Rewriting application features.
  • Changing what *.lab hosts are — only how they are reached.

3. Why this exists — the incident

On 2026-10-13 an automated scanner found console.lab.audasmedia.com.au on the public internet and exploited a critical authentication bypass in the family console:

if username is None and basic is not None:
    username = basic.username     # password never verified

HTTPBasic only parses the header. The code trusted the username alone, so curl -u sam:anything authenticated as admin. The attacker used it to drive Jervis (the house voice) with secret-exfiltration prompts for two days, unnoticed.

The bypass is fixed. This project fixes the conditions that made it matter.

4. Prerequisites

Need Status / note
Caddy .35, already the single reverse proxy for *.lab
Tailscale Installed on .13, .27, .51, and the phone (sams-pixel-8a)
Docker Everywhere it is needed
A decision *.lab stays public for genuine public sites. Only admin UIs move.

5. Functional requirements

ID Requirement
FR-1 Inventory. Produce a table of every *.lab host: what it is, its backend, and whether it has auth.
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.
FR-6 Secret scanning. gitleaks as a pre-commit hook on every repo; trufflehog over existing history.
FR-7 Code scanning. semgrep in CI/pre-commit for our own code.
FR-8 Image scanning. trivy over the running container images.
FR-9 A correct-by-default auth helper for FastAPI apps, plus a test asserting valid username + wrong password → 401.
FR-10 A baseline scan with nuclei against .lab, so we find issues before the bots do.

6. Screens and UX flow

  • Authelia portal — login + 2FA page in front of protected hosts.
  • CrowdSec dashboard — who is being banned, and why.
  • The inventory table (FR-1) — a document, not a UI.

7. Look and feel

Reuse the Family Home Lab design system for any page we build ourselves.

  • /home/sam/Docker/Containers/family-home-lab/DESIGN.md
  • /home/sam/Docker/Containers/family-home-lab/portal/static/tokens.css

Authelia and CrowdSec ship their own UIs — do not restyle them.

8. Integrations

System Use
Caddy (.35) forward-auth, the CrowdSec bouncer, the public allowlist
Tailscale admin access without public exposure
Authelia SSO + 2FA
CrowdSec log parsing + decisions + bouncer
Apprise (.35:8210) alerts on bans and scan spikes
Gitea pre-commit hooks, CI

9. Technology (locked)

Item Value
Reverse proxy Caddy (existing, .35)
Auth Authelia (forward-auth)
IDS/IPS CrowdSec + Caddy bouncer
Remote access Tailscale
Secret scan gitleaks (pre-commit), trufflehog (history)
SAST semgrep
Image scan trivy
DAST nuclei
Container One per service. Own folder, own compose file.

10. Rules

  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.
  5. One commit and push per phase.
  6. Bound every docker command (timeout, --entrypoint sh when running a shell inside an image).

11. Build order

Phase 1 — Inventory Table of every *.lab host: purpose, backend, auth, public or not. DoD: the table exists and is complete. Nothing changes yet.

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 in the dashboard.

Phase 4 — Authelia forward-auth + allowlist DoD: a newly created *.lab host is protected without anyone remembering to add auth. Public sites still work.

Phase 5 — Secret scanning gitleaks pre-commit on every repo; trufflehog over history. DoD: findings triaged; every real leak rotated.

Phase 6 — Code and image scanning semgrep + trivy. DoD: both run in CI; findings triaged.

Phase 7 — Auth helper + test A shared FastAPI dependency that verifies the password, plus the test. DoD: the test fails against the old bypass pattern and passes against the fix.

Phase 8 — Baseline scan + documentation nuclei against .lab; write it all up. DoD: no high findings unreviewed; docs published.

12. Acceptance criteria

  • 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.
  • Semgrep, trivy, and the auth test run in CI.
  • A nuclei baseline exists.

13. Decisions and open questions

Resolved:

  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).
  3. Scanning is accepted as normal — the goal is control, not zero noise.
  4. Authelia over Authentik (lighter, fits Caddy forward-auth).

maps.lab — PUBLIC BY DESIGN (decided 2026-10-14).

Obsidian embeds the Archify maps and Gitea content, so maps.lab must stay reachable without auth. Accepted: it discloses internal hostnames, ports and service names to anyone who asks.

Rationale from Sam: "the network info is not really that bad — if someone gets in they have that anyway. No keys, passwords etc matter."

Rules that follow from this:

  • NEVER publish a secret, key, password, token or credential to maps.lab. It is world-readable. (Publishing a public SSH key is fine.)
  • Directories named after secrets (for example inmotionhosting_S3_email) must not contain sensitive material.
  • Optional hardening, not required: turn off directory browse so the tree index is not listable while direct links keep working.

Remaining:

  1. Which other hosts are genuinely public? (Needs Sam's list for the allowlist.)
  2. Should CrowdSec run in ban mode from the start, or observe first?
  3. Where do the alerts go — Apprise to which channel?
  4. Do we add nuclei/semgrep to Gitea CI, or run them by hand?