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
*.labwildcard, 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
*.labhosts 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
- 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.
- Never remove auth to "fix" a problem. Fix the auth instead.
- Do not break genuinely public sites — they go on the allowlist.
- Alerts go through Apprise.
- One commit and push per phase.
- Bound every docker command (
timeout,--entrypoint shwhen 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
*.labhost 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
nucleibaseline exists.
13. Decisions and open questions
Resolved:
- 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.
*.labstays public. The unauthenticated allowlist is short and explicit (maps.lab).- Scanning is accepted as normal — the goal is control, not zero noise.
- 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
browseso the tree index is not listable while direct links keep working.
Remaining:
- Which other hosts are genuinely public? (Needs Sam's list for the allowlist.)
- Should CrowdSec run in ban mode from the start, or observe first?
- Where do the alerts go — Apprise to which channel?
- Do we add
nuclei/semgrepto Gitea CI, or run them by hand?