reverse_proxy is the first built+applied service role; add the per-service records CLAUDE.md/ADR-002/008/017/021 require. Add access__*/backup__* data to defaults as the source of truth (ADR-021/022). reverse_proxy is stateless (ACME certs re-issue via HTTP-01), so it declares backup__state: false with a reason rather than a BACKUP.md (ADR-022 convention). The access__*/backup__* cross-role field names intentionally don't carry the reverse_proxy__ prefix, so each is marked `# noqa: var-naming[no-role-prefix]` (ansible-lint has no per-prefix allowlist; rule stays enabled elsewhere). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.8 KiB
3.8 KiB
Security — reverse_proxy (Caddy)
Exposure
- Published ports:
80/tcp+443/tcp(HTTP→HTTPS redirect + TLS). Both are declared in thegroup_varsfirewall catalog as the askaripublic_webopens (ADR-020); the Hetzner Cloud Firewall also opens 80/443 (and 3478 for NetBird). Port 80 must stay open to the internet for the ACME HTTP-01 challenge. - Auth surface: none of its own. Caddy is the TLS terminator and router; per-service
authentication (Authentik
forward_auth) is added at each route in Phase 2 (ADR-024 §4). Today it fronts only a staticrespondtest vhost and (M4b) the NetBird stack, which carries its own auth. - Reachability: public — askari is internet-facing. Caddy is the single public entry
point; upstreams sit on the internal
bomaDocker network and are reached by name, not published directly. - Data sensitivity: none persistent worth protecting — only ACME account keys +
issued certificates in the
caddy_datavolume, which are re-issuable (HTTP-01). No user data, no secrets at rest. See backup record:backup__state: false(stateless).
Checklist status
Each item from docs/security/service-checklist.md:
- Secrets in vault; no default creds; nothing secret in git/images — ✅ n/a: HTTP-01
needs no credentials; the only config input is
reverse_proxy__acme_email(not secret). - Non-root; no
privileged/host-network unless justified; minimal mounts; caps dropped — ⚠️ officialcaddy:2runs as root (to bind 80/443); noprivileged, no host network (bridgeboma); mounts are the read-only Caddyfile + two named volumes. Root inside the container is the upstream default; revisit if Caddy ships a rootless variant. - Ports declared in
group_vars; behind reverse proxy + auth if exposed; least-privilege inter-service reach — ✅ 80/443 in the catalog; Caddy is the proxy; upstreams are not published, only reachable on thebomanetwork. - Image pinned (tag/digest), update path known — ⚠️ pinned to the
caddy:2major tag (stateless tier, ADR-011/ADR-004), not a digest; refreshed deliberately and watched by DIUN. Tighten totag@digestif the proxy is reclassified as stateful. - Logs reviewable; backup/restore covered if stateful — ✅ stateless (no backup
needed); logs via
docker logs caddynow, Loki labels declared for the ADR-018 pipeline.
Service-specific hardening
- HTTP-01 only, no DNS token: vanilla
caddy:2, nocaddy-dns/gandiplugin and no Gandi API token on the host — removes a credential and a custom-image supply chain (ADR-024 revised Status). - Caddyfile is read-only in the container (
:romount); rendered solely by Ansible from thegroup_varsroute catalog — no dynamic label discovery, so no route exists that wasn't declared (the reason Caddy was chosen over Traefik, ADR-024 §1). - Admin API not exposed: Caddy's admin endpoint stays on container-localhost
:2019; never published, never in the firewall catalog (access__api.enabled: false). - Automatic HTTPS: HTTP is redirected to HTTPS and modern TLS defaults are Caddy's out-of-the-box behaviour (no manual cipher config needed).
Residual / accepted risks
- Container runs as root — upstream
caddy:2default (needs to bind low ports). Rationale: official image, no rootless variant wired yet; blast radius limited to the proxy container. Revisit: adopt a rootless Caddy image if upstream stabilises one. - Image pinned to a major tag, not a digest — accepted for the stateless tier (ADR-011). Revisit if the role gains state.
- ACME re-issuance vs Let's Encrypt rate limits — losing
caddy_datatriggers re-issuance; rapid repeated rebuilds could hit LE rate limits. Acceptable for a handful of askari hostnames; noted in the backup rationale.