From b7e919d6b3977fa05c11bd48f3019a5dc47e9487 Mon Sep 17 00:00:00 2001 From: sjat Date: Sun, 14 Jun 2026 18:11:20 +0200 Subject: [PATCH] refactor(reverse_proxy): vanilla Caddy + HTTP-01 (drop DNS-01 custom image) Switch from a custom caddy-dns/gandi image built on-host to the official caddy:2 image with per-host ACME HTTP-01 certificates. Removes the Dockerfile, env.j2 (Gandi token), on-host image build/ship/load tasks, the caddy-image Makefile target, and the wildcard DNS-01 Caddyfile. Each route now gets its own server block and automatic certificate. Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 7 +- .../group_vars/all/reverse_proxy.yml | 8 +-- roles/reverse_proxy/README.md | 68 +++++++------------ roles/reverse_proxy/defaults/main.yml | 8 +-- roles/reverse_proxy/files/Dockerfile | 8 --- roles/reverse_proxy/handlers/main.yml | 6 ++ .../molecule/default/converge.yml | 6 +- .../reverse_proxy/molecule/default/verify.yml | 4 +- roles/reverse_proxy/tasks/main.yml | 27 +------- roles/reverse_proxy/templates/Caddyfile.j2 | 20 ++---- .../templates/docker-compose.yml.j2 | 3 +- roles/reverse_proxy/templates/env.j2 | 1 - 12 files changed, 51 insertions(+), 115 deletions(-) delete mode 100644 roles/reverse_proxy/files/Dockerfile delete mode 100644 roles/reverse_proxy/templates/env.j2 diff --git a/Makefile b/Makefile index 6c3986b..7554245 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,7 @@ endif .PHONY: help setup collections lint test test-all check deploy encrypt decrypt \ edit-vault check-vault new-role \ tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \ - molecule-image molecule-image-push \ - caddy-image + molecule-image molecule-image-push help: @echo "" @@ -64,7 +63,6 @@ help: @echo "" @echo " make molecule-image Build the Molecule test image locally" @echo " make molecule-image-push Push the test image to the Forgejo registry" - @echo " make caddy-image Build the custom Caddy image (caddy-dns/gandi) locally" @echo "" # ── Environment setup ───────────────────────────────────────────────────────── @@ -145,9 +143,6 @@ molecule-image: molecule-image-push: molecule-image docker push $(MOLECULE_IMAGE) -caddy-image: - docker build -t boma/caddy-gandi:latest -f roles/reverse_proxy/files/Dockerfile roles/reverse_proxy/files/ - # ── Terraform ───────────────────────────────────────────────────────────────── tf-init: diff --git a/inventories/production/group_vars/all/reverse_proxy.yml b/inventories/production/group_vars/all/reverse_proxy.yml index f087c4c..83b8e62 100644 --- a/inventories/production/group_vars/all/reverse_proxy.yml +++ b/inventories/production/group_vars/all/reverse_proxy.yml @@ -1,6 +1,6 @@ --- -# Caddy reverse proxy on askari (ADR-024). Custom image built on-host (caddy-dns/gandi). -# TLS: ACME DNS-01 against Gandi using vault.gandi.pat. Routes appended as services land. -reverse_proxy__acme_domain: askari.wingu.me # wildcard *.askari.wingu.me +# Caddy reverse proxy on askari (ADR-024). Vanilla Caddy, ACME HTTP-01 (public host). reverse_proxy__acme_email: admin@wingu.me -reverse_proxy__routes: [] # M4b appends: {host: netbird.askari.wingu.me, upstream: "..."} +reverse_proxy__routes: + - {host: test.askari.wingu.me, respond: "boma reverse proxy"} + # M4b appends: {host: netbird.askari.wingu.me, upstream: "netbird-dashboard:80"} diff --git a/roles/reverse_proxy/README.md b/roles/reverse_proxy/README.md index 0e69f28..11e979e 100644 --- a/roles/reverse_proxy/README.md +++ b/roles/reverse_proxy/README.md @@ -1,18 +1,18 @@ # 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. +Hetzner host) and terminates TLS for all public-facing services via ACME HTTP-01. +Uses the official `caddy:2` image — no custom build, no DNS plugin, no token required. ## 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. +Caddy obtains per-hostname certificates using the ACME HTTP-01 challenge. Port 80 +must be reachable from the internet for the challenge to succeed. Each `host` in +`reverse_proxy__routes` gets its own certificate automatically. + +> **DNS-01 (for mesh/LAN-only cluster services) is deferred to Phase 2.** The +> `caddy-dns/gandi` plugin failed to issue certificates during M4a and needs +> investigation before it can be used. ## Route catalog — `reverse_proxy__routes` @@ -21,46 +21,31 @@ Services register themselves as routes by appending an entry to ```yaml reverse_proxy__routes: - - host: netbird.askari.wingu.me - upstream: "netbird-management:443" - - host: dashboard.askari.wingu.me - upstream: "dashboard:8080" + - {host: app.askari.wingu.me, upstream: "app:8080"} + - {host: health.askari.wingu.me, respond: "ok"} ``` -Each entry renders a named matcher and a `handle` block in the Caddyfile: +Each entry renders a separate server block in the Caddyfile: ``` -@netbird_askari_wingu_me host netbird.askari.wingu.me -handle @netbird_askari_wingu_me { - reverse_proxy netbird-management:443 +app.askari.wingu.me { + reverse_proxy app:8080 +} + +health.askari.wingu.me { + respond "ok" 200 } ``` -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`. +Use `upstream` to proxy to a Docker service, or `respond` to return a static string. ## 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__routes` | `[]` | List of `{host, upstream}` or `{host, respond}` entries | | `reverse_proxy__manage` | `true` | Set `false` in Molecule to skip Docker tasks | Production overrides live in @@ -68,15 +53,10 @@ Production overrides live in ## `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. +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 -| 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. +None. HTTP-01 requires no credentials. diff --git a/roles/reverse_proxy/defaults/main.yml b/roles/reverse_proxy/defaults/main.yml index 1cba589..7060df3 100644 --- a/roles/reverse_proxy/defaults/main.yml +++ b/roles/reverse_proxy/defaults/main.yml @@ -1,8 +1,6 @@ --- -# Caddy reverse proxy (ADR-024). TLS via ACME DNS-01 against Gandi. -reverse_proxy__image: "boma/caddy-gandi:latest" +# Caddy reverse proxy (ADR-024). Vanilla Caddy; TLS via ACME HTTP-01 (public hosts). reverse_proxy__base_dir: /opt/services/reverse_proxy -reverse_proxy__acme_domain: example.test reverse_proxy__acme_email: admin@example.test -reverse_proxy__routes: [] -reverse_proxy__manage: true # set false in Molecule to render templates without Docker +reverse_proxy__routes: [] # each: {host: x, upstream: "svc:port"} OR {host: x, respond: "text"} +reverse_proxy__manage: true # set false in Molecule to render without Docker diff --git a/roles/reverse_proxy/files/Dockerfile b/roles/reverse_proxy/files/Dockerfile deleted file mode 100644 index 87f9e8b..0000000 --- a/roles/reverse_proxy/files/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# Custom Caddy with the Gandi DNS-01 provider (ADR-024). Built locally (make caddy-image) -# and on askari by the reverse_proxy role (askari can't reach the internal registry -# pre-mesh, so the image is built on the host). -FROM caddy:2-builder AS build -RUN xcaddy build --with github.com/caddy-dns/gandi - -FROM caddy:2 -COPY --from=build /usr/bin/caddy /usr/bin/caddy diff --git a/roles/reverse_proxy/handlers/main.yml b/roles/reverse_proxy/handlers/main.yml index ed97d53..baaa395 100644 --- a/roles/reverse_proxy/handlers/main.yml +++ b/roles/reverse_proxy/handlers/main.yml @@ -1 +1,7 @@ --- +- name: Reload caddy + listen: reload caddy + community.docker.docker_container_exec: + container: caddy + command: caddy reload --config /etc/caddy/Caddyfile + when: reverse_proxy__manage | bool diff --git a/roles/reverse_proxy/molecule/default/converge.yml b/roles/reverse_proxy/molecule/default/converge.yml index 3deb177..c521771 100644 --- a/roles/reverse_proxy/molecule/default/converge.yml +++ b/roles/reverse_proxy/molecule/default/converge.yml @@ -5,14 +5,12 @@ vars: reverse_proxy__manage: false - reverse_proxy__acme_domain: example.test reverse_proxy__acme_email: admin@example.test reverse_proxy__routes: - host: app.example.test upstream: "app:80" - vault: - gandi: - pat: "molecule-dummy" + - host: t.example.test + respond: "ok" roles: - role: reverse_proxy diff --git a/roles/reverse_proxy/molecule/default/verify.yml b/roles/reverse_proxy/molecule/default/verify.yml index a6f2799..b239c56 100644 --- a/roles/reverse_proxy/molecule/default/verify.yml +++ b/roles/reverse_proxy/molecule/default/verify.yml @@ -14,9 +14,9 @@ ansible.builtin.assert: that: - _caddyfile.content | b64decode | length > 0 - - "'dns gandi' in (_caddyfile.content | b64decode)" - - "'respond \"boma reverse proxy\"' in (_caddyfile.content | b64decode)" - "'app.example.test' in (_caddyfile.content | b64decode)" + - "'reverse_proxy app:80' in (_caddyfile.content | b64decode)" + - "'respond \"ok\" 200' in (_caddyfile.content | b64decode)" fail_msg: "Caddyfile is missing expected content" success_msg: "Caddyfile rendered correctly" tags: [verify] diff --git a/roles/reverse_proxy/tasks/main.yml b/roles/reverse_proxy/tasks/main.yml index f4f05ae..fb53090 100644 --- a/roles/reverse_proxy/tasks/main.yml +++ b/roles/reverse_proxy/tasks/main.yml @@ -6,26 +6,12 @@ mode: "0750" tags: [config] -- name: Copy the Caddy image Dockerfile - ansible.builtin.copy: - src: Dockerfile - dest: "{{ reverse_proxy__base_dir }}/Dockerfile" - mode: "0644" - register: _caddy_dockerfile - tags: [config] - - name: Render the Caddyfile ansible.builtin.template: src: Caddyfile.j2 dest: "{{ reverse_proxy__base_dir }}/Caddyfile" mode: "0644" - tags: [config] - -- name: Render the env file (Gandi token) - ansible.builtin.template: - src: env.j2 - dest: "{{ reverse_proxy__base_dir }}/.env" - mode: "0600" + notify: reload caddy tags: [config] - name: Render the compose file @@ -35,17 +21,6 @@ mode: "0644" tags: [config] -- name: Build the custom Caddy image (caddy-dns/gandi) on the host - community.docker.docker_image: - name: "{{ reverse_proxy__image }}" - source: build - build: - path: "{{ reverse_proxy__base_dir }}" - state: present - force_source: "{{ _caddy_dockerfile.changed }}" - when: reverse_proxy__manage | bool - tags: [deploy] - - name: Bring the reverse proxy up community.docker.docker_compose_v2: project_src: "{{ reverse_proxy__base_dir }}" diff --git a/roles/reverse_proxy/templates/Caddyfile.j2 b/roles/reverse_proxy/templates/Caddyfile.j2 index 30bbb15..3981065 100644 --- a/roles/reverse_proxy/templates/Caddyfile.j2 +++ b/roles/reverse_proxy/templates/Caddyfile.j2 @@ -1,18 +1,12 @@ { email {{ reverse_proxy__acme_email }} } - -*.{{ reverse_proxy__acme_domain }} { - tls { - dns gandi {env.GANDI_BEARER_TOKEN} - } {% for r in reverse_proxy__routes %} - @{{ r.host | replace('.', '_') }} host {{ r.host }} - handle @{{ r.host | replace('.', '_') }} { - reverse_proxy {{ r.upstream }} - } -{% endfor %} - handle { - respond "boma reverse proxy" 200 - } +{{ r.host }} { +{% if r.upstream is defined %} + reverse_proxy {{ r.upstream }} +{% else %} + respond "{{ r.respond | default('boma') }}" 200 +{% endif %} } +{% endfor %} diff --git a/roles/reverse_proxy/templates/docker-compose.yml.j2 b/roles/reverse_proxy/templates/docker-compose.yml.j2 index b3d50d9..ae8f676 100644 --- a/roles/reverse_proxy/templates/docker-compose.yml.j2 +++ b/roles/reverse_proxy/templates/docker-compose.yml.j2 @@ -1,12 +1,11 @@ services: caddy: - image: {{ reverse_proxy__image }} + image: caddy:2 container_name: caddy restart: unless-stopped ports: - "80:80" - "443:443" - env_file: ./.env volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data diff --git a/roles/reverse_proxy/templates/env.j2 b/roles/reverse_proxy/templates/env.j2 deleted file mode 100644 index a41f0f3..0000000 --- a/roles/reverse_proxy/templates/env.j2 +++ /dev/null @@ -1 +0,0 @@ -GANDI_BEARER_TOKEN={{ vault.gandi.pat }}