boma/roles/reverse_proxy/README.md
sjat 6e38693499 feat(reverse_proxy): optional ACME DNS-01 via Gandi (wildcard / LAN-only)
Adds a per-instance DNS-01 mode to the Caddy role for mesh/LAN-only hosts that
cannot satisfy HTTP-01. Default behaviour (vanilla caddy:2 + HTTP-01, what askari
runs) is unchanged.

  - reverse_proxy__acme_dns_provider: "" (HTTP-01) | "gandi" (DNS-01)
  - reverse_proxy__image: override to the custom caddy-gandi image for DNS-01
  - Caddyfile gains a global `acme_dns gandi {env.GANDI_BEARER_TOKEN}` block
  - the PAT (vault.gandi.pat) renders into a host-only 0600 env file (no_log),
    loaded by compose only when DNS-01 is enabled

Verified: the custom image issues a real wildcard cert (*.dns01test.wingu.me)
end-to-end against LE staging via Gandi DNS-01; `caddy validate` accepts
`acme_dns gandi` on the custom image and rejects it on vanilla caddy:2. Molecule
(HTTP-01 default path) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:57:47 +02:00

3.6 KiB

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.