Appearance
Security model
Goal
Shrink what the internet can touch, keep credentials out of git/docs, and make abuse noisy but low-impact.
Edge posture
Only three service classes face the world:
| Port | Role | Controls |
|---|---|---|
| 22/tcp | Administration | Keys, Fail2Ban, optional future Tailscale-only |
| 80/tcp | ACME + redirect | Caddy |
| 443/tcp | All apps | Caddy TLS, per-host routing |
Everything else listens on loopback. UFW defaults deny incoming; Docker publishes application ports as 127.0.0.1:… so a stray -p 8080:80 on all interfaces is treated as a defect.
Defense in depth (what each layer buys)
- UFW/nftables — coarse port filter
- Fail2Ban (sshd) — short bans on password/noise storms (default 10m/5 tries — consider longer bans)
- AppArmor — host +
docker-defaultconfinement - Unattended upgrades — security patches without waiting for a human
- etckeeper — forensic trail for
/etcedits - TLS at Caddy — no cleartext app exposure
No single layer is perfect; together they force an attacker through SSH or a web app bug, not a random database port.
Identity
- Human operator:
tywith sudo - Root SSH: key-only policy in sshd; prefer not logging in as root at all
- App secrets:
.envbeside compose, Vaultwarden as organizational source of truth — never paste secrets into MkDocs
Residual risk (documented honestly)
- SSH on the public internet — scanners are constant; key-only helps, VPN/Tailscale would help more.
- Speculative CPU issues — “Spec rstack overflow” called out as not fully mitigated without host/microcode cooperation.
- Concentrated host — one VM holds many apps; backups and stack isolation matter.
- Short Fail2Ban window — 10 minutes is mild.
- Snapshot-style docs — status tables drift; verify live when it matters (SoloOps: live-verified truth).