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>
This commit is contained in:
parent
456c27d12b
commit
50b6445bdd
15 changed files with 296 additions and 1 deletions
7
Makefile
7
Makefile
|
|
@ -33,7 +33,8 @@ endif
|
||||||
.PHONY: help setup collections lint test test-all check deploy encrypt decrypt \
|
.PHONY: help setup collections lint test test-all check deploy encrypt decrypt \
|
||||||
edit-vault check-vault new-role \
|
edit-vault check-vault new-role \
|
||||||
tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \
|
tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \
|
||||||
molecule-image molecule-image-push
|
molecule-image molecule-image-push \
|
||||||
|
caddy-image
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
@ -63,6 +64,7 @@ help:
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make molecule-image Build the Molecule test image locally"
|
@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 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 ""
|
@echo ""
|
||||||
|
|
||||||
# ── Environment setup ─────────────────────────────────────────────────────────
|
# ── Environment setup ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -143,6 +145,9 @@ molecule-image:
|
||||||
molecule-image-push: molecule-image
|
molecule-image-push: molecule-image
|
||||||
docker 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 ─────────────────────────────────────────────────────────────────
|
# ── Terraform ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
tf-init:
|
tf-init:
|
||||||
|
|
|
||||||
6
inventories/production/group_vars/all/reverse_proxy.yml
Normal file
6
inventories/production/group_vars/all/reverse_proxy.yml
Normal file
|
|
@ -0,0 +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
|
||||||
|
reverse_proxy__acme_email: admin@wingu.me
|
||||||
|
reverse_proxy__routes: [] # M4b appends: {host: netbird.askari.wingu.me, upstream: "..."}
|
||||||
|
|
@ -16,3 +16,8 @@ collections:
|
||||||
# LiveDNS). PAT auth requires >= 9.0.0.
|
# LiveDNS). PAT auth requires >= 9.0.0.
|
||||||
- name: community.general
|
- name: community.general
|
||||||
version: ">=9.0.0"
|
version: ">=9.0.0"
|
||||||
|
|
||||||
|
# community.docker — docker_image (build the Caddy image on-host) + docker_compose_v2
|
||||||
|
# (reverse_proxy role).
|
||||||
|
- name: community.docker
|
||||||
|
version: ">=3.0.0"
|
||||||
|
|
|
||||||
82
roles/reverse_proxy/README.md
Normal file
82
roles/reverse_proxy/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# 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.
|
||||||
8
roles/reverse_proxy/defaults/main.yml
Normal file
8
roles/reverse_proxy/defaults/main.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
# Caddy reverse proxy (ADR-024). TLS via ACME DNS-01 against Gandi.
|
||||||
|
reverse_proxy__image: "boma/caddy-gandi:latest"
|
||||||
|
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
|
||||||
8
roles/reverse_proxy/files/Dockerfile
Normal file
8
roles/reverse_proxy/files/Dockerfile
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# 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
roles/reverse_proxy/handlers/main.yml
Normal file
1
roles/reverse_proxy/handlers/main.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
---
|
||||||
13
roles/reverse_proxy/meta/main.yml
Normal file
13
roles/reverse_proxy/meta/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: sjat
|
||||||
|
description: >-
|
||||||
|
Caddy reverse proxy with ACME DNS-01 TLS via Gandi (ADR-024). Builds the
|
||||||
|
custom image on-host (caddy-dns/gandi) and manages it via Docker Compose.
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.17"
|
||||||
|
platforms:
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- trixie
|
||||||
|
dependencies: []
|
||||||
18
roles/reverse_proxy/molecule/default/converge.yml
Normal file
18
roles/reverse_proxy/molecule/default/converge.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: reverse_proxy
|
||||||
31
roles/reverse_proxy/molecule/default/molecule.yml
Normal file
31
roles/reverse_proxy/molecule/default/molecule.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
dependency:
|
||||||
|
name: galaxy
|
||||||
|
options:
|
||||||
|
requirements-file: ../../requirements.yml
|
||||||
|
|
||||||
|
driver:
|
||||||
|
name: docker
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: instance
|
||||||
|
# Project-owned image built from .docker/molecule-debian13/Dockerfile
|
||||||
|
# and hosted in the Forgejo container registry.
|
||||||
|
# Build/push with: make molecule-image / make molecule-image-push
|
||||||
|
image: forgejo.nyumbani.baobab.band/sjat/molecule-debian13:latest
|
||||||
|
pre_build_image: true
|
||||||
|
privileged: true # required for systemd
|
||||||
|
cgroupns_mode: host
|
||||||
|
volumes:
|
||||||
|
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||||
|
command: /lib/systemd/systemd
|
||||||
|
|
||||||
|
provisioner:
|
||||||
|
name: ansible
|
||||||
|
inventory:
|
||||||
|
host_vars:
|
||||||
|
instance:
|
||||||
|
ansible_user: root
|
||||||
|
|
||||||
|
verifier:
|
||||||
|
name: ansible
|
||||||
22
roles/reverse_proxy/molecule/default/verify.yml
Normal file
22
roles/reverse_proxy/molecule/default/verify.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
- name: Verify
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Slurp the rendered Caddyfile
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: /opt/services/reverse_proxy/Caddyfile
|
||||||
|
register: _caddyfile
|
||||||
|
tags: [verify]
|
||||||
|
|
||||||
|
- name: Assert Caddyfile exists and contains expected content
|
||||||
|
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)"
|
||||||
|
fail_msg: "Caddyfile is missing expected content"
|
||||||
|
success_msg: "Caddyfile rendered correctly"
|
||||||
|
tags: [verify]
|
||||||
54
roles/reverse_proxy/tasks/main.yml
Normal file
54
roles/reverse_proxy/tasks/main.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
- name: Ensure the service directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ reverse_proxy__base_dir }}"
|
||||||
|
state: directory
|
||||||
|
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"
|
||||||
|
tags: [config]
|
||||||
|
|
||||||
|
- name: Render the compose file
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: docker-compose.yml.j2
|
||||||
|
dest: "{{ reverse_proxy__base_dir }}/docker-compose.yml"
|
||||||
|
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 }}"
|
||||||
|
state: present
|
||||||
|
when: reverse_proxy__manage | bool
|
||||||
|
tags: [deploy]
|
||||||
18
roles/reverse_proxy/templates/Caddyfile.j2
Normal file
18
roles/reverse_proxy/templates/Caddyfile.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
23
roles/reverse_proxy/templates/docker-compose.yml.j2
Normal file
23
roles/reverse_proxy/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: {{ reverse_proxy__image }}
|
||||||
|
container_name: caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
env_file: ./.env
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- boma
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
boma:
|
||||||
|
name: boma
|
||||||
1
roles/reverse_proxy/templates/env.j2
Normal file
1
roles/reverse_proxy/templates/env.j2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
GANDI_BEARER_TOKEN={{ vault.gandi.pat }}
|
||||||
Loading…
Add table
Reference in a new issue