# Security — reverse_proxy (Caddy) ## Exposure - **Published ports:** `80/tcp` + `443/tcp` (HTTP→HTTPS redirect + TLS). Both are declared in the `group_vars` firewall catalog as the askari `public_web` opens (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 static `respond` test 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 `boma` Docker network and are reached by name, not published directly. - **Data sensitivity:** none persistent worth protecting — only ACME account keys + issued certificates in the `caddy_data` volume, 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`: - [x] 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). - [x] Non-root; no `privileged`/host-network unless justified; minimal mounts; caps dropped — ⚠️ official `caddy:2` runs as root (to bind 80/443); no `privileged`, no host network (bridge `boma`); mounts are the read-only Caddyfile + two named volumes. Root inside the container is the upstream default; revisit if Caddy ships a rootless variant. - [x] 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 the `boma` network. - [x] Image pinned (tag/digest), update path known — ⚠️ pinned to the `caddy:2` major tag (stateless tier, ADR-011/ADR-004), not a digest; refreshed deliberately and watched by DIUN. Tighten to `tag@digest` if the proxy is reclassified as stateful. - [x] Logs reviewable; backup/restore covered if stateful — ✅ stateless (no backup needed); logs via `docker logs caddy` now, Loki labels declared for the ADR-018 pipeline. ## Service-specific hardening - **HTTP-01 only, no DNS token:** vanilla `caddy:2`, no `caddy-dns/gandi` plugin 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 (`:ro` mount); rendered solely by Ansible from the `group_vars` route 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:2` default (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_data` triggers re-issuance; rapid repeated rebuilds could hit LE rate limits. Acceptable for a handful of askari hostnames; noted in the backup rationale.