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>
83 lines
3.6 KiB
Markdown
83 lines
3.6 KiB
Markdown
# 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.
|