# ADR-002 — Security baseline ## Context Every managed host must reach a defined security baseline before any services are deployed. This baseline is applied by the `base` role and is non-negotiable — it runs first, on every host, every time. The goal is a principled, maintainable baseline appropriate for a homelab with some public-facing services — not a compliance exercise. ## Baseline components ### Access & authentication - SSH key authentication only — password auth disabled - Root login disabled — `PermitRootLogin no` - Dedicated `ansible` user with locked-down sudo (NOPASSWD for automation) - No shared user accounts — per-person SSH keys in `group_vars/all/vars.yml` ### Firewall - `nftables` (native on Debian 13, replaces iptables) - Default policy: deny inbound, allow established/related, allow loopback - Rules managed entirely by Ansible — never edited manually on hosts - Port definitions live in `group_vars/` so rules stay in sync with deployed services - Docker's own iptables rules are disabled — nftables manages all filtering > **Note on Docker + nftables**: Docker historically bypassed iptables-based firewalls. > This is addressed by setting `"iptables": false` in Docker daemon config and managing > all rules via nftables explicitly. See `docs/decisions/004-docker-model.md`. ### Intrusion deterrence - `fail2ban` monitoring SSH (and optionally reverse proxy logs) - Configured to ban after 5 failed attempts, 1-hour ban ### Updates - `unattended-upgrades` enabled for **security patches only** - Full system upgrades triggered deliberately via Ansible (`make deploy PLAYBOOK=upgrade`) - No automatic reboots — reboots are a conscious operational decision ### Minimal attack surface - No unnecessary packages installed - Docker daemon TCP socket disabled — Unix socket only - No open ports beyond those explicitly defined in firewall rules ### Audit trail - `auditd` installed and running with a baseline ruleset - Logs shipped to a central location if a log aggregation service is available ## Secrets management - Ansible Vault for all secrets (API keys, passwords, certificates), structured as a nested `vault..` map (ADR-003) - The master vault password lives in **Vaultwarden** and is fetched on demand by `scripts/vault-pass-client.sh` (wired as `vault_password_file`) through the `rbw` agent — never written to a plaintext file on disk. Unlock once per session with `rbw unlock`; nothing decryptable sits at rest in the repo or working tree - See `docs/runbooks/rotate-secrets.md` for `rbw` setup and rotation ## What this baseline does not include - Full CIS benchmark hardening — adds complexity for marginal gain at this scale - SELinux / AppArmor — not applied by default, revisit if threat model changes - Intrusion detection (IDS) — out of scope for now ## Decision This baseline was chosen to be: - **Effective** against the realistic threat model (exposed services, shared repo) - **Maintainable** by a small team without security expertise overhead - **Automated** — no manual steps should be needed to reach baseline state