# 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`: ```yaml 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.