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) <noreply@anthropic.com>
This commit is contained in:
parent
9c169561d7
commit
b7e919d6b3
12 changed files with 51 additions and 115 deletions
7
Makefile
7
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:
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
GANDI_BEARER_TOKEN={{ vault.gandi.pat }}
|
||||
Loading…
Add table
Reference in a new issue