Master vault password is fetched from Vaultwarden via the rbw agent (scripts/vault-pass-client.sh, wired as vault_password_file) instead of a plaintext .vault_pass. Vault secrets use a nested vault.<service>.<key> map. Encrypted vault.yml files are excluded from lint. Includes the host rename in Makefile and STATUS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
3.1 KiB
Markdown
76 lines
3.1 KiB
Markdown
# 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.<service>.<key>` 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
|