boma/roles/reverse_proxy/README.md
sjat 50b6445bdd feat(reverse_proxy): Caddy role (Gandi DNS-01, on-host image build, route catalog)
Implements the Caddy reverse proxy role (ADR-024): builds boma/caddy-gandi:latest
on-host (caddy-dns/gandi plugin), renders Caddyfile from route catalog, brings
Compose project up. Adds community.docker to requirements.yml, production group_vars,
and a caddy-image Makefile target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:36:58 +02:00

82 lines
3.1 KiB
Markdown

# reverse_proxy
Boma's standard Caddy reverse proxy (ADR-024). Runs on `askari` (the off-site
Hetzner host) and terminates TLS for all public-facing services via ACME DNS-01
against Gandi LiveDNS. The custom Caddy image (with the `caddy-dns/gandi` plugin)
is built directly on the host because `askari` cannot reach the internal registry
before the mesh VPN is established.
## How TLS works
Caddy obtains wildcard certificates for `*.{{ reverse_proxy__acme_domain }}` using
the ACME DNS-01 challenge. The Gandi PAT (`vault.gandi.pat`) is injected into
the container at runtime via an `.env` file as `GANDI_BEARER_TOKEN`. Caddy reads
it with `{env.GANDI_BEARER_TOKEN}` in the Caddyfile so the secret never lands in a
config file on disk.
## 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: netbird.askari.wingu.me
upstream: "netbird-management:443"
- host: dashboard.askari.wingu.me
upstream: "dashboard:8080"
```
Each entry renders a named matcher and a `handle` block in the Caddyfile:
```
@netbird_askari_wingu_me host netbird.askari.wingu.me
handle @netbird_askari_wingu_me {
reverse_proxy netbird-management:443
}
```
Requests that match no route receive a plain `200 "boma reverse proxy"` response.
## On-host image build
The Dockerfile at `roles/reverse_proxy/files/Dockerfile` builds a two-stage image:
1. `caddy:2-builder` — uses `xcaddy` to compile Caddy with `caddy-dns/gandi`.
2. `caddy:2` — copies the compiled binary into the slim runtime image.
The Ansible role copies the Dockerfile to `{{ reverse_proxy__base_dir }}` and
calls `community.docker.docker_image` to build `boma/caddy-gandi:latest` on the
host. The build is only re-triggered when the Dockerfile changes
(`force_source: "{{ _caddy_dockerfile.changed }}"`).
For local dev or CI: `make caddy-image`.
## Variables
| Variable | Default | Description |
|---|---|---|
| `reverse_proxy__image` | `boma/caddy-gandi:latest` | Docker image name (built on-host) |
| `reverse_proxy__base_dir` | `/opt/services/reverse_proxy` | Working directory for Compose project |
| `reverse_proxy__acme_domain` | `example.test` | Wildcard domain for ACME cert |
| `reverse_proxy__acme_email` | `admin@example.test` | ACME registration email |
| `reverse_proxy__routes` | `[]` | List of `{host, upstream}` route entries |
| `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 (image build, `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
| Secret path | Usage |
|---|---|
| `vault.gandi.pat` | Gandi PAT injected as `GANDI_BEARER_TOKEN` |
Stored encrypted in `inventories/production/group_vars/all/vault.yml`. Decrypt
with `make decrypt FILE=...` before editing; re-encrypt after.