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>
61 lines
3.8 KiB
Markdown
61 lines
3.8 KiB
Markdown
# 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.
|