ntfy, datum-gateway, headscale: use the caddy_site role

Completes Stage 3. No hand-rolled Caddy plumbing remains anywhere:
`grep sites-enabled` outside roles/ returns nothing, and so does
`grep "systemctl reload caddy"`.

ntfy uses caddy_site_body for its plain-HTTP listener and @httpget redirect.
Verified ok/unchanged against watchtower; the one other changed task is a
pre-existing "Update APT cache".

datum-gateway keeps a whole-Caddyfile validate after the role call. The role
validates its own fragment, but only a whole-file validate catches a conflict
between two sites, and this playbook was the only one that ever had it. Its
two debug tasks that echoed command output are gone with the commands.

headscale is the one that mattered. Its playbook wrote
`reverse_proxy localhost:8080`, but spacey is actually running a /admin*
route in front of Headplane behind Caddy basic auth. Running that playbook
would have deleted the admin route and its auth - a hazard that predates this
work. It now renders the config that is really there, verified ok/unchanged
via --start-at-task (the play cannot reach Caddy in check mode: "Install
headscale package" fails because the .deb is not really downloaded, before
and after this edit alike).

Supporting changes for headscale:
  - headscale_ui_password_hash added to infra_secrets.yml and the identical
    group_vars/all/vault.yml, read from the live config on spacey. The vault
    already had headscale_ui_username (= counterweight, confirmed) and
    headscale_ui_password; I did not verify the password is the plaintext of
    this hash.
  - headplane_port added to headscale_vars.yml.
  - The role's handler now sets become: true. Handlers do not inherit become
    from the task that notified them, and this play runs become: no.
  - The include uses `apply: become: yes`; `become:` on an include_role is
    rejected outright.

All 14 site files on all 3 hosts still byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
counterweight 2026-09-11 23:37:38 +02:00
parent 82fca48b08
commit 16cbd189b8
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
7 changed files with 184 additions and 208 deletions

View file

@ -8,7 +8,6 @@
- ./headscale_vars.yml
vars:
headscale_subdomain: "{{ subdomains.headscale }}"
caddy_sites_dir: "{{ caddy_sites_dir }}"
headscale_domain: "{{ headscale_subdomain }}.{{ root_domain }}"
headscale_base_domain: "tailnet.{{ root_domain }}"
headscale_namespace: "{{ service_settings.headscale.namespace }}"
@ -230,39 +229,34 @@
port: '3478'
proto: udp
- name: Ensure Caddy sites-enabled directory exists
become: yes
file:
path: "{{ caddy_sites_dir }}"
state: directory
owner: root
group: root
mode: '0755'
- name: Ensure Caddyfile includes import directive for sites-enabled
become: yes
lineinfile:
path: /etc/caddy/Caddyfile
line: 'import sites-enabled/*'
insertafter: EOF
state: present
backup: yes
- name: Create Caddy reverse proxy configuration for headscale
become: yes
copy:
dest: "{{ caddy_sites_dir }}/headscale.conf"
content: |
{{ headscale_domain }} {
reverse_proxy localhost:{{ headscale_port }}
- name: Publish headscale through Caddy
ansible.builtin.include_role:
name: caddy_site
# This play is become: no and elevates per task. `apply` is how an
# include_role passes become down to the role's tasks - `become:` on
# the include itself is rejected. The role's handler sets its own.
apply:
become: yes
vars:
caddy_site_name: headscale
caddy_site_domain: "{{ headscale_domain }}"
# Raw body, and it must stay raw: the /admin* route in front of
# Headplane is not expressible as a plain reverse_proxy. The previous
# version of this task wrote only `reverse_proxy localhost:8080`, which
# would have deleted the admin route and its auth on the next run.
caddy_site_body: |
@headplane {
path /admin*
}
owner: root
group: root
mode: '0644'
- name: Reload Caddy to apply new config
become: yes
command: systemctl reload caddy
handle @headplane {
basicauth {
{{ headscale_ui_username }} {{ headscale_ui_password_hash }}
}
reverse_proxy http://localhost:{{ headplane_port }}
}
# Headscale API is protected by its own API key authentication
# All API operations require a valid Bearer token in the Authorization header
reverse_proxy * http://localhost:{{ headscale_port }}
# ═════════════════════════════════════════════════════════════════════════
# DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11.