boma/roles/reverse_proxy
sjat 83983d739c fix(reverse_proxy): plain {% %} tags so the Caddyfile renders under ansible trim_blocks
The tls-internal/acme_ca knobs used {%- -%} trims validated only against raw jinja2; ansible (trim_blocks=True) double-stripped newlines and collapsed the Caddyfile onto single lines, crash-looping caddy. Match the role's existing plain {% %} style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:35:15 +02:00
..
defaults feat(reverse_proxy): tls-internal + acme_ca knobs for integration/staging (ADR-025) 2026-06-18 12:30:49 +02:00
handlers refactor(reverse_proxy): vanilla Caddy + HTTP-01 (drop DNS-01 custom image) 2026-06-14 18:11:20 +02:00
meta docs(review): 2026-06-14 repo audit — M4a doc drift + Traefik→Caddy lag 2026-06-14 18:37:54 +02:00
molecule/default fix(reverse_proxy): bind-mount the Caddy config dir so reload sees changes 2026-06-16 07:44:45 +02:00
tasks fix(reverse_proxy,netbird_coordinator): create scaffold dirs in check mode 2026-06-17 17:49:47 +02:00
templates fix(reverse_proxy): plain {% %} tags so the Caddyfile renders under ansible trim_blocks 2026-06-18 16:35:15 +02:00
ACCESS.md docs(reverse_proxy): service-role SECURITY/VERIFY/ACCESS records (O12) 2026-06-14 19:06:23 +02:00
README.md feat(reverse_proxy): optional ACME DNS-01 via Gandi (wildcard / LAN-only) 2026-06-15 06:57:47 +02:00
SECURITY.md docs(reverse_proxy): service-role SECURITY/VERIFY/ACCESS records (O12) 2026-06-14 19:06:23 +02:00
VERIFY.md docs(reverse_proxy): service-role SECURITY/VERIFY/ACCESS records (O12) 2026-06-14 19:06:23 +02:00

reverse_proxy

Boma's standard Caddy reverse proxy (ADR-024). Runs on askari (the off-site Hetzner host) and terminates TLS for services. It supports two ACME challenge types, chosen per proxy instance by exposure:

  • HTTP-01 (default) — public hosts with an A-record (askari). Official caddy:2 image; no plugin, no token.
  • DNS-01 via Gandi — mesh/LAN-only hosts with no public A-record (the cluster), where HTTP-01 is impossible. Needs the custom caddy-gandi image and the Gandi PAT.

How TLS works

HTTP-01 (default). Caddy obtains per-hostname certificates using the ACME HTTP-01 challenge. Port 80 must be reachable from the internet. Each host in reverse_proxy__routes gets its own certificate automatically.

DNS-01 (Gandi). Set reverse_proxy__acme_dns_provider: gandi and point reverse_proxy__image at the custom Caddy image (make caddy-image, built on ubongo and pushed to the Forgejo registry — see .docker/caddy-gandi/). Caddy then proves domain control by writing ACME TXT records through the Gandi LiveDNS API, so it can issue certs — including wildcards — for hosts that are never publicly reachable. The token (vault.gandi.pat) is injected as GANDI_BEARER_TOKEN via a host-only env file (mode 0600) and sent as a Bearer PAT (the legacy Apikey scheme is gone).

Verified (2026-06-15): the custom image issues a real wildcard cert (*.dns01test.wingu.me) end-to-end against Let's Encrypt staging via Gandi DNS-01; caddy validate accepts the acme_dns gandi directive on the custom image and rejects it on vanilla caddy:2 (module not registered: dns.providers.gandi). The original M4a failure was version skew (a pre-Bearer libdns/gandi that sent the deprecated Apikey header) plus building the image on a Hetzner IP (Go proxy 403).

Route catalog — reverse_proxy__routes

Services register themselves as routes by appending an entry to reverse_proxy__routes in group_vars/all/reverse_proxy.yml:

reverse_proxy__routes:
  - {host: app.askari.wingu.me, upstream: "app:8080"}
  - {host: health.askari.wingu.me, respond: "ok"}

Each entry renders a separate server block in the Caddyfile:

app.askari.wingu.me {
  reverse_proxy app:8080
}

health.askari.wingu.me {
  respond "ok" 200
}

Use upstream to proxy to a Docker service, or respond to return a static string.

Variables

Variable Default Description
reverse_proxy__base_dir /opt/services/reverse_proxy Working directory for Compose project
reverse_proxy__acme_email admin@example.test ACME registration email
reverse_proxy__routes [] List of {host, upstream} or {host, respond} entries
reverse_proxy__image caddy:2 Container image. DNS-01 hosts override to the custom caddy-gandi image
reverse_proxy__acme_dns_provider "" "" = HTTP-01; "gandi" = ACME DNS-01 via the Gandi PAT
reverse_proxy__manage true Set false in Molecule to skip Docker tasks

Production overrides live in inventories/production/group_vars/all/reverse_proxy.yml.

reverse_proxy__manage toggle

Docker operations (docker compose up) are gated on reverse_proxy__manage | bool. Set it to false in Molecule so the role can be tested (template rendering, directory creation) without a Docker daemon.

Secrets

  • HTTP-01 (default): none — the challenge requires no credentials.
  • DNS-01 (reverse_proxy__acme_dns_provider: gandi): the Gandi PAT (vault.gandi.pat, the same token public_dns uses). Rendered host-side into {{ reverse_proxy__base_dir }}/env (mode 0600, no_log); never committed.