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>
|
||
|---|---|---|
| .. | ||
| defaults | ||
| handlers | ||
| meta | ||
| molecule/default | ||
| tasks | ||
| templates | ||
| ACCESS.md | ||
| README.md | ||
| SECURITY.md | ||
| VERIFY.md | ||
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:2image; 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-gandiimage 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 validateaccepts theacme_dns gandidirective on the custom image and rejects it on vanillacaddy:2(module not registered: dns.providers.gandi). The original M4a failure was version skew (a pre-Bearerlibdns/gandithat 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 tokenpublic_dnsuses). Rendered host-side into{{ reverse_proxy__base_dir }}/env(mode 0600,no_log); never committed.