From 8a3fddbe4967f34e43af1265bfeecb6cd22f908c Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 22:43:56 +0200 Subject: [PATCH 01/10] docs: mark Uptime Kuma as decommissioned README, both setup guides and the forgejo-runner notes now point at archive/uptime_kuma/ instead of describing a live service. Co-Authored-By: Claude Opus 5 (1M context) --- 01_infra_setup.md | 6 ++++++ 02_vps_core_services_setup.md | 6 ++++++ README.md | 2 +- ansible/services/forgejo-runner/SETUP.md | 4 +++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/01_infra_setup.md b/01_infra_setup.md index 6ef0978..a8ba9c0 100644 --- a/01_infra_setup.md +++ b/01_infra_setup.md @@ -162,6 +162,12 @@ Note that, by applying these playbooks, both the root user and the `counterweigh ```bash cp ansible/infra_secrets.yml.example ansible/infra_secrets.yml ``` + > **DEPRECATED (2026-09-11).** Uptime Kuma has been decommissioned. The server + > deployment was removed from this repo; what it monitored and how it was set up is + > preserved in [`archive/uptime_kuma/`](archive/uptime_kuma/). The monitoring blocks in + > the playbooks are kept but inert (`uptime_kuma_enabled: false`) so the check logic + > survives for whatever replaces it. The credentials below no longer exist in the vault. + * Edit `ansible/infra_secrets.yml` and add your Uptime Kuma credentials: ```yaml uptime_kuma_username: "admin" diff --git a/02_vps_core_services_setup.md b/02_vps_core_services_setup.md index 19cd122..5d75de9 100644 --- a/02_vps_core_services_setup.md +++ b/02_vps_core_services_setup.md @@ -49,6 +49,12 @@ Checklist: ## Uptime Kuma +> **DEPRECATED (2026-09-11).** Uptime Kuma has been decommissioned. The server +> deployment was removed from this repo; what it monitored and how it was set up is +> preserved in [`archive/uptime_kuma/`](archive/uptime_kuma/). The monitoring blocks in +> the playbooks are kept but inert (`uptime_kuma_enabled: false`) so the check logic +> survives for whatever replaces it. The credentials below no longer exist in the vault. + Uptime Kuma gets used to monitor the availability of services, keep track of their uptime and notify issues. ### Deploy diff --git a/README.md b/README.md index 80dbaf6..89ef092 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Assumes that you've set `ansible/.vault_pass` with `chmod 600`. + Plan install + File based config + Crossbackup to Desky via rsync -* Uptime Kuma +* ~~Uptime Kuma~~ — decommissioned 2026-09-11, see `archive/uptime_kuma/` + Deployed on Vipy + Crossbackup to Desky via rsync * Vaultwarden diff --git a/ansible/services/forgejo-runner/SETUP.md b/ansible/services/forgejo-runner/SETUP.md index a66d295..5cb2a3f 100644 --- a/ansible/services/forgejo-runner/SETUP.md +++ b/ansible/services/forgejo-runner/SETUP.md @@ -25,4 +25,6 @@ ansible-playbook ansible/services/forgejo-runner/deploy_forgejo_runner_playbook. 1. On the VM: `systemctl status forgejo-runner` should show active 2. In Forgejo: **Site Administration** > **Actions** > **Runners** should show the runner as online -3. In Uptime Kuma: the `forgejo-runner-healthcheck` push monitor should be receiving pings +3. ~~In Uptime Kuma: the `forgejo-runner-healthcheck` push monitor should be receiving pings~~ + *(Uptime Kuma was decommissioned 2026-09-11 — this check no longer applies. The + healthcheck timer still runs on the host but pushes nowhere. See `archive/uptime_kuma/`.)* From b07ed72a92852a8da35d86f52db98f7160b361fb Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:03:25 +0200 Subject: [PATCH 02/10] caddy: add a [caddy] role group and target it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit services/caddy_playbook.yml was the one play still targeting a location group (vps) rather than a role group. The two coincide today — vps is exactly vipy, watchtower and spacey, the three hosts with /etc/caddy/sites-enabled — but adding a fourth VPS that does not run Caddy would have silently pulled it into the play. [caddy:children] is edge + monitoring + vpn_control. Verified the play selects the same three machines before and after. Co-Authored-By: Claude Opus 5 (1M context) --- ansible/inventory.ini | 8 +++++++- ansible/services/caddy_playbook.yml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ansible/inventory.ini b/ansible/inventory.ini index a061637..4643dbb 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -58,4 +58,10 @@ localhost [managed:children] vps nodito_host -nodito_vms \ No newline at end of file +nodito_vms + +# Hosts that run Caddy and therefore have /etc/caddy/sites-enabled. +[caddy:children] +edge +monitoring +vpn_control diff --git a/ansible/services/caddy_playbook.yml b/ansible/services/caddy_playbook.yml index de98c8f..29e74b2 100644 --- a/ansible/services/caddy_playbook.yml +++ b/ansible/services/caddy_playbook.yml @@ -1,5 +1,5 @@ - name: Install and configure Caddy on Debian 12 - hosts: vps + hosts: caddy become: yes tasks: From cc9340b7cc0801c4dd080a82a4044dcfeacae439 Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:10:43 +0200 Subject: [PATCH 03/10] caddy: add the caddy_site role Replaces the four-task Caddy vhost block currently copy-pasted into 10 playbooks. Nothing calls it yet; this commit only adds the role. Verified by rendering all 10 sites through the template and diffing against what the current playbooks produce: 9 of 10 byte-identical. The tenth is datum-gateway, where the resolvers comment is standardised, rewriting one comment line Caddy ignores. Then dry-run against the live hosts (--check, nothing written): - vipy: forgejo, vaultwarden, lnbits, personal-blog, ntfy-emergency-app all report ok/unchanged against the real files - watchtower: ntfy renders identical via caddy_site_body, blank line and {host}{uri} placeholders intact - spacey: headscale renders identical when given the config that is actually running - memos, mempool, datum-gateway report changed - the comment, as expected All 14 site files on all 3 hosts confirmed unchanged afterwards. Two things the build turned up: - Ansible does not template dict *keys*, so caddy_site_basic_auth is a list of {user, hash}. As a dict, a Jinja username passes through literally. The assert refuses a mapping. - `caddy validate` does accept a single site fragment - rc=0 on a good one, rc=1 with a line number on a broken one. This was the plan's one untested claim. A failed validate leaves the live file untouched. The reload is now a handler, so it fires once at end of play rather than immediately; anything needing the new config live mid-play must flush_handlers first. Co-Authored-By: Claude Opus 5 (1M context) --- ansible/roles/caddy_site/README.md | 94 +++++++++++++++++++ ansible/roles/caddy_site/defaults/main.yml | 23 +++++ ansible/roles/caddy_site/handlers/main.yml | 8 ++ ansible/roles/caddy_site/tasks/main.yml | 46 +++++++++ .../roles/caddy_site/templates/site.conf.j2 | 34 +++++++ 5 files changed, 205 insertions(+) create mode 100644 ansible/roles/caddy_site/README.md create mode 100644 ansible/roles/caddy_site/defaults/main.yml create mode 100644 ansible/roles/caddy_site/handlers/main.yml create mode 100644 ansible/roles/caddy_site/tasks/main.yml create mode 100644 ansible/roles/caddy_site/templates/site.conf.j2 diff --git a/ansible/roles/caddy_site/README.md b/ansible/roles/caddy_site/README.md new file mode 100644 index 0000000..6891070 --- /dev/null +++ b/ansible/roles/caddy_site/README.md @@ -0,0 +1,94 @@ +# `caddy_site` + +Writes one Caddy site file into `{{ caddy_sites_dir }}`, makes sure the main +Caddyfile imports that directory, validates the result, and reloads Caddy once. + +Replaces the four-task block that was copy-pasted into 10 playbooks. + +Runs on any host in the `[caddy]` group — `edge` (vipy), `monitoring` +(watchtower) and `vpn_control` (spacey). + +## Usage + +```yaml +- ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: forgejo # -> forgejo.conf + caddy_site_domain: "{{ forgejo_domain }}" + caddy_site_upstream: "localhost:{{ forgejo_port }}" +``` + +Use `include_role`, not a `roles:` block, so the call stays in task order next +to the tasks it depends on. Variables passed this way are scoped to the include +and do not leak into later calls — so **every call must pass everything it +needs**; nothing carries over. + +## Shapes + +Pick exactly one of `caddy_site_upstream`, `caddy_site_root`, `caddy_site_body`. + +| Want | Set | +|---|---| +| `reverse_proxy host:port` | `caddy_site_upstream` | +| static `root *` + `file_server` | `caddy_site_root` | +| anything else | `caddy_site_body` (raw, indented 4 for you) | + +`caddy_site_upstream` accepts two modifiers, which add a block to the +`reverse_proxy`: + +- `caddy_site_headers_up: {"X-Forwarded-Host": "..."}` +- `caddy_site_resolvers: "100.100.100.100"` — Tailscale MagicDNS + +and `caddy_site_basic_auth` wraps the site in a `basic_auth` block. + +## `caddy_site_basic_auth` is a LIST, not a dict + +```yaml +caddy_site_basic_auth: + - user: "{{ datum_dashboard_username }}" + hash: "{{ datum_dashboard_password_hash }}" +``` + +**Ansible does not template dictionary keys.** With `{ "{{ user }}": "hash" }` +the value is rendered and the key is not, so the literal string +`{{ datum_dashboard_username }}` lands in the config file. Found while building +this role; the `assert` refuses a mapping so it cannot happen again. + +## Secrets and `--diff` + +Rendered site files can carry credentials — `datum-gateway.conf` holds a bcrypt +hash — and `--diff` prints rendered content. The template task therefore sets +`diff: "{{ caddy_site_reveal | bool }}"`, default `false`, so `--diff` runs are +safe everywhere. Pass `-e caddy_site_reveal=true` to see what moved on a site +you know is not secret. + +## Validation + +`validate: "caddy validate --adapter caddyfile --config %s"` runs against the +rendered temp file before it is moved into place. Verified on vipy that a single +site fragment validates cleanly (rc=0, `Valid configuration`) and that a +malformed one is rejected (rc=1, with the syntax error and line number). A +failed validate leaves the live file untouched, so a broken config can no longer +reach a running Caddy. + +What it cannot catch is a conflict with the global `/etc/caddy/Caddyfile`. + +## The reload is a handler + +`Reload caddy` fires **once, at the end of the play**, however many sites +notified it. The code this replaced ran `command: systemctl reload caddy` +immediately, mid-play. If a later task in the same play needs the new config to +be live, flush first: + +```yaml +- ansible.builtin.meta: flush_handlers +``` + +## Known intentional difference + +The `resolvers` block is commented `# Use Tailscale MagicDNS to resolve the +upstream hostname` in every case. `datum-gateway` previously said `# Resolve via +Tailscale MagicDNS`. Migrating it therefore rewrites one comment line, which +Caddy ignores. Every other site renders byte-identical to what its playbook +produced. diff --git a/ansible/roles/caddy_site/defaults/main.yml b/ansible/roles/caddy_site/defaults/main.yml new file mode 100644 index 0000000..cafa3dc --- /dev/null +++ b/ansible/roles/caddy_site/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# Required +caddy_site_name: "" # file basename -> .conf +caddy_site_domain: "" # site address line; may hold several, comma separated + +# Pick exactly one shape +caddy_site_upstream: "" # "localhost:3000" -> reverse_proxy +caddy_site_root: "" # filesystem path -> root * + file_server +caddy_site_body: "" # raw escape hatch for one-off sites; wins over both + +# reverse_proxy modifiers +caddy_site_resolvers: "" # "100.100.100.100" for Tailscale MagicDNS +caddy_site_headers_up: {} # {"X-Forwarded-Host": "wallet.example.com"} +# A LIST, not a dict: Ansible does not template dict *keys*, so a Jinja +# expression for the username silently passes through as literal text. +caddy_site_basic_auth: [] # [{user: "{{ x_user }}", hash: "{{ x_hash }}"}] + +# Placement. caddy_sites_dir comes from services_config.yml; this is the fallback. +caddy_sites_dir: /etc/caddy/sites-enabled + +# Rendered site files can carry credentials (basic_auth hashes), so --diff is +# suppressed by default. Pass -e caddy_site_reveal=true to see what moved. +caddy_site_reveal: false diff --git a/ansible/roles/caddy_site/handlers/main.yml b/ansible/roles/caddy_site/handlers/main.yml new file mode 100644 index 0000000..fb57280 --- /dev/null +++ b/ansible/roles/caddy_site/handlers/main.yml @@ -0,0 +1,8 @@ +--- +# Fires once at the end of the play, however many sites notified it. +# Anything later in the same play that needs the new config live must be +# preceded by `- ansible.builtin.meta: flush_handlers`. +- name: Reload caddy + ansible.builtin.systemd: + name: caddy + state: reloaded diff --git a/ansible/roles/caddy_site/tasks/main.yml b/ansible/roles/caddy_site/tasks/main.yml new file mode 100644 index 0000000..5ad3976 --- /dev/null +++ b/ansible/roles/caddy_site/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Assert caddy_site parameters are sane + ansible.builtin.assert: + that: + - caddy_site_name | length > 0 + - caddy_site_domain | length > 0 + - (caddy_site_upstream | length > 0) or (caddy_site_root | length > 0) or (caddy_site_body | length > 0) + - caddy_site_basic_auth is not mapping + fail_msg: >- + caddy_site: '{{ caddy_site_name | default("") }}' needs a name, a domain and + one of caddy_site_upstream / caddy_site_root / caddy_site_body. + caddy_site_basic_auth must be a LIST of {user, hash} — Ansible does not template dict keys. + quiet: true + +- name: Ensure Caddy sites-enabled directory exists + ansible.builtin.file: + path: "{{ caddy_sites_dir }}" + state: directory + owner: root + group: root + mode: '0755' + +- name: Ensure Caddyfile imports sites-enabled + ansible.builtin.lineinfile: + path: /etc/caddy/Caddyfile + line: 'import sites-enabled/*' + insertafter: EOF + state: present + create: yes + mode: '0644' + backup: yes + +# `validate` runs `caddy validate` against the rendered temp file before it is +# moved into place: verified on vipy that a single site fragment validates +# cleanly (rc=0, "Valid configuration") and that a malformed one is rejected +# (rc=1). A failed validate leaves the live file untouched. +- name: "Write Caddy site '{{ caddy_site_name }}'" + ansible.builtin.template: + src: site.conf.j2 + dest: "{{ caddy_sites_dir }}/{{ caddy_site_name }}.conf" + owner: root + group: root + mode: '0644' + validate: "caddy validate --adapter caddyfile --config %s" + diff: "{{ caddy_site_reveal | bool }}" + notify: Reload caddy diff --git a/ansible/roles/caddy_site/templates/site.conf.j2 b/ansible/roles/caddy_site/templates/site.conf.j2 new file mode 100644 index 0000000..5d34c01 --- /dev/null +++ b/ansible/roles/caddy_site/templates/site.conf.j2 @@ -0,0 +1,34 @@ +{{ caddy_site_domain }} { +{% if caddy_site_body %} +{{ caddy_site_body | trim | indent(4, first=True) }} +{% else %} +{% if caddy_site_basic_auth %} + basic_auth { +{% for cred in caddy_site_basic_auth %} + {{ cred.user }} {{ cred.hash }} +{% endfor %} + } +{% endif %} +{% if caddy_site_root %} + root * {{ caddy_site_root }} + file_server +{% endif %} +{% if caddy_site_upstream %} +{% if caddy_site_headers_up or caddy_site_resolvers %} + reverse_proxy {{ caddy_site_upstream }} { +{% for key, value in caddy_site_headers_up.items() %} + header_up {{ key }} {{ value }} +{% endfor %} +{% if caddy_site_resolvers %} + # Use Tailscale MagicDNS to resolve the upstream hostname + transport http { + resolvers {{ caddy_site_resolvers }} + } +{% endif %} + } +{% else %} + reverse_proxy {{ caddy_site_upstream }} +{% endif %} +{% endif %} +{% endif %} +} From 39a1b43577fb6e46b7741ed58134731132dca2de Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:19:01 +0200 Subject: [PATCH 04/10] personal-blog: use the caddy_site role First service on the role. 31 lines of copy-pasted Caddy plumbing become 7. Verified: --check before and after the edit reports the same three unrelated tasks as changed, so the edit introduces nothing. Real run leaves all 14 site files on all 3 hosts byte-identical, and the blog still answers HTTP 200. A second consecutive run reports the site task ok with the handler not firing. Side effect worth noting: the playbook no longer has a perpetually-changed task. `command: systemctl reload caddy` always reported changed; the role's handler only fires when the file actually moves. Co-Authored-By: Claude Opus 5 (1M context) --- .../deploy_personal_blog_playbook.yml | 38 ++++--------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/ansible/services/personal-blog/deploy_personal_blog_playbook.yml b/ansible/services/personal-blog/deploy_personal_blog_playbook.yml index af7a1f3..96d030f 100644 --- a/ansible/services/personal-blog/deploy_personal_blog_playbook.yml +++ b/ansible/services/personal-blog/deploy_personal_blog_playbook.yml @@ -8,7 +8,6 @@ - ./personal_blog_vars.yml vars: personal_blog_subdomain: "{{ subdomains.personal_blog }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" personal_blog_domain: "{{ personal_blog_subdomain }}.{{ root_domain }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" @@ -51,36 +50,13 @@ group: www-data mode: '0664' - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy file server configuration for personal blog - copy: - dest: "{{ caddy_sites_dir }}/personal-blog.conf" - content: | - {{ personal_blog_domain }} { - root * {{ personal_blog_web_root }} - file_server - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish the blog through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: personal-blog + caddy_site_domain: "{{ personal_blog_domain }}" + caddy_site_root: "{{ personal_blog_web_root }}" # ═════════════════════════════════════════════════════════════════════════ # DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11. From 4bee18297892196ffed1cabd0d2f7cbb28d36a4a Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:24:57 +0200 Subject: [PATCH 05/10] ntfy-emergency-app, vaultwarden, forgejo: use the caddy_site role The plain reverse_proxy shape. All three removed a byte-identical 23-line block (verified by md5 of the diff with the service name normalised) and gained the same 7-line include_role call. The caddy_sites_dir self-reference goes with it. Verified in check mode, nothing applied to the hosts yet: - ntfy-emergency-app: site task ok, changed=0 - vaultwarden: site task ok; the one changed task is a pre-existing always-restarts fail2ban step, identical before the edit - forgejo: check mode cannot run this playbook at all - get_url does not download in check mode so the next task fails on "Source /tmp/forgejo not found". Confirmed identical before the edit. Covered instead by the Stage 2 dry-run, which ran the role against vipy with forgejo's real parameters and reported ok/unchanged. All 14 site files on all 3 hosts still byte-identical. Real runs for these three are still outstanding. Co-Authored-By: Claude Opus 5 (1M context) --- .../forgejo/deploy_forgejo_playbook.yml | 37 ++++--------------- .../deploy_ntfy_emergency_app_playbook.yml | 37 ++++--------------- .../deploy_vaultwarden_playbook.yml | 37 ++++--------------- 3 files changed, 21 insertions(+), 90 deletions(-) diff --git a/ansible/services/forgejo/deploy_forgejo_playbook.yml b/ansible/services/forgejo/deploy_forgejo_playbook.yml index acb1473..db78e95 100644 --- a/ansible/services/forgejo/deploy_forgejo_playbook.yml +++ b/ansible/services/forgejo/deploy_forgejo_playbook.yml @@ -8,7 +8,6 @@ - ./forgejo_vars.yml vars: forgejo_subdomain: "{{ subdomains.forgejo }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" forgejo_domain: "{{ forgejo_subdomain }}.{{ root_domain }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" @@ -88,35 +87,13 @@ enabled: yes state: started - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy reverse proxy configuration for forgejo - copy: - dest: "{{ caddy_sites_dir }}/forgejo.conf" - content: | - {{ forgejo_domain }} { - reverse_proxy localhost:{{ forgejo_port }} - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish Forgejo through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: forgejo + caddy_site_domain: "{{ forgejo_domain }}" + caddy_site_upstream: "localhost:{{ forgejo_port }}" # ═════════════════════════════════════════════════════════════════════════ # DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11. diff --git a/ansible/services/ntfy-emergency-app/deploy_ntfy_emergency_app_playbook.yml b/ansible/services/ntfy-emergency-app/deploy_ntfy_emergency_app_playbook.yml index a8d97a6..7379d5f 100644 --- a/ansible/services/ntfy-emergency-app/deploy_ntfy_emergency_app_playbook.yml +++ b/ansible/services/ntfy-emergency-app/deploy_ntfy_emergency_app_playbook.yml @@ -8,7 +8,6 @@ - ./ntfy_emergency_app_vars.yml vars: ntfy_emergency_app_subdomain: "{{ subdomains.ntfy_emergency_app }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" ntfy_emergency_app_domain: "{{ ntfy_emergency_app_subdomain }}.{{ root_domain }}" ntfy_service_domain: "{{ subdomains.ntfy }}.{{ root_domain }}" ntfy_emergency_app_ntfy_url: "https://{{ ntfy_service_domain }}" @@ -49,35 +48,13 @@ args: chdir: "{{ ntfy_emergency_app_dir }}" - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy reverse proxy configuration for ntfy-emergency-app - copy: - dest: "{{ caddy_sites_dir }}/ntfy-emergency-app.conf" - content: | - {{ ntfy_emergency_app_domain }} { - reverse_proxy localhost:{{ ntfy_emergency_app_port }} - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish ntfy-emergency-app through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: ntfy-emergency-app + caddy_site_domain: "{{ ntfy_emergency_app_domain }}" + caddy_site_upstream: "localhost:{{ ntfy_emergency_app_port }}" # ═════════════════════════════════════════════════════════════════════════ # DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11. diff --git a/ansible/services/vaultwarden/deploy_vaultwarden_playbook.yml b/ansible/services/vaultwarden/deploy_vaultwarden_playbook.yml index 9868d13..74e87d8 100644 --- a/ansible/services/vaultwarden/deploy_vaultwarden_playbook.yml +++ b/ansible/services/vaultwarden/deploy_vaultwarden_playbook.yml @@ -8,7 +8,6 @@ - ./vaultwarden_vars.yml vars: vaultwarden_subdomain: "{{ subdomains.vaultwarden }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" vaultwarden_domain: "{{ vaultwarden_subdomain }}.{{ root_domain }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" @@ -81,35 +80,13 @@ name: fail2ban state: restarted - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy reverse proxy configuration for vaultwarden - copy: - dest: "{{ caddy_sites_dir }}/vaultwarden.conf" - content: | - {{ vaultwarden_domain }} { - reverse_proxy localhost:{{ vaultwarden_port }} - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish Vaultwarden through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: vaultwarden + caddy_site_domain: "{{ vaultwarden_domain }}" + caddy_site_upstream: "localhost:{{ vaultwarden_port }}" # ═════════════════════════════════════════════════════════════════════════ # DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11. From 82fca48b0877008c5c2668ae0b5047d24a02eabb Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:29:06 +0200 Subject: [PATCH 06/10] lnbits, memos, mempool: use the caddy_site role lnbits is the header_up shape; memos and mempool are the Tailscale MagicDNS shape. 108 lines removed, 25 added. mempool was the one playbook already reloading Caddy correctly (systemd: state: reloaded rather than command: systemctl reload caddy), so its end marker differed - the role's handler does the same thing. Verified: - lnbits: full --check, site task ok, byte-identical to the live file - memos, mempool: --check --diff via --limit edge shows exactly one added line each, the standardised MagicDNS comment. Both playbooks fail earlier in check mode on their VM play ("Extract memos binary", the same download-does-not-happen-in-check-mode artifact as forgejo), but the edits are confined to the hosts: edge play - memos at line 169+, play 2 starts at 159; mempool at 617+, play 2 starts at 606. The added comment means the next real run of memos/mempool rewrites one comment line. Those two host files were already stale against their playbooks before this change. All 14 site files on all 3 hosts still byte-identical. Co-Authored-By: Claude Opus 5 (1M context) --- .../lnbits/deploy_lnbits_playbook.yml | 43 ++++------------- .../services/memos/deploy_memos_playbook.yml | 43 ++++------------- .../mempool/deploy_mempool_playbook.yml | 47 ++++--------------- 3 files changed, 25 insertions(+), 108 deletions(-) diff --git a/ansible/services/lnbits/deploy_lnbits_playbook.yml b/ansible/services/lnbits/deploy_lnbits_playbook.yml index d596052..65bdfbf 100644 --- a/ansible/services/lnbits/deploy_lnbits_playbook.yml +++ b/ansible/services/lnbits/deploy_lnbits_playbook.yml @@ -8,7 +8,6 @@ - ./lnbits_vars.yml vars: lnbits_subdomain: "{{ subdomains.lnbits }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" lnbits_domain: "{{ lnbits_subdomain }}.{{ root_domain }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" @@ -147,39 +146,15 @@ enabled: yes state: started - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - create: yes - mode: '0644' - - - name: Create Caddy reverse proxy configuration for lnbits - copy: - dest: "{{ caddy_sites_dir }}/lnbits.conf" - content: | - {{ lnbits_domain }} { - reverse_proxy localhost:{{ lnbits_port }} { - header_up X-Forwarded-Host {{ lnbits_domain }} - } - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish LNBits through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: lnbits + caddy_site_domain: "{{ lnbits_domain }}" + caddy_site_upstream: "localhost:{{ lnbits_port }}" + caddy_site_headers_up: + X-Forwarded-Host: "{{ lnbits_domain }}" # ═════════════════════════════════════════════════════════════════════════ # DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11. diff --git a/ansible/services/memos/deploy_memos_playbook.yml b/ansible/services/memos/deploy_memos_playbook.yml index 5ab254e..83c187e 100644 --- a/ansible/services/memos/deploy_memos_playbook.yml +++ b/ansible/services/memos/deploy_memos_playbook.yml @@ -166,45 +166,18 @@ - ./memos_vars.yml vars: memos_subdomain: "{{ subdomains.memos }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" memos_domain: "{{ memos_subdomain }}.{{ root_domain }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy reverse proxy configuration for memos (via Tailscale) - copy: - dest: "{{ caddy_sites_dir }}/memos.conf" - content: | - {{ memos_domain }} { - reverse_proxy {{ memos_tailscale_hostname }}:{{ memos_port }} { - # Use Tailscale MagicDNS to resolve the upstream hostname - transport http { - resolvers 100.100.100.100 - } - } - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + - name: Publish Memos through Caddy (via Tailscale) + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: memos + caddy_site_domain: "{{ memos_domain }}" + caddy_site_upstream: "{{ memos_tailscale_hostname }}:{{ memos_port }}" + caddy_site_resolvers: "100.100.100.100" - name: Create Uptime Kuma monitor setup script for Memos when: uptime_kuma_enabled | default(false) diff --git a/ansible/services/mempool/deploy_mempool_playbook.yml b/ansible/services/mempool/deploy_mempool_playbook.yml index 1dfea1b..ba3e9e9 100644 --- a/ansible/services/mempool/deploy_mempool_playbook.yml +++ b/ansible/services/mempool/deploy_mempool_playbook.yml @@ -614,47 +614,16 @@ vars: mempool_subdomain: "{{ subdomains.mempool }}" mempool_domain: "{{ mempool_subdomain }}.{{ root_domain }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" tasks: - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - create: yes - mode: '0644' - - - name: Create Caddy reverse proxy configuration for Mempool - copy: - dest: "{{ caddy_sites_dir }}/mempool.conf" - content: | - {{ mempool_domain }} { - reverse_proxy mempool-box:{{ mempool_frontend_port }} { - # Use Tailscale MagicDNS to resolve the upstream hostname - transport http { - resolvers 100.100.100.100 - } - } - } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - systemd: - name: caddy - state: reloaded + - name: Publish Mempool through Caddy (via Tailscale) + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: mempool + caddy_site_domain: "{{ mempool_domain }}" + caddy_site_upstream: "mempool-box:{{ mempool_frontend_port }}" + caddy_site_resolvers: "100.100.100.100" - name: Display Mempool URL when: uptime_kuma_enabled | default(false) From 16cbd189b8a9b715ad14a92563103533efaa1f4e Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:37:38 +0200 Subject: [PATCH 07/10] 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) --- ansible/group_vars/all/vault.yml | 103 ++++++++++-------- ansible/infra_secrets.yml | 103 ++++++++++-------- ansible/roles/caddy_site/handlers/main.yml | 4 + .../deploy_datum_gateway_playbook.yml | 69 +++--------- .../headscale/deploy_headscale_playbook.yml | 60 +++++----- ansible/services/headscale/headscale_vars.yml | 3 + .../services/ntfy/deploy_ntfy_playbook.yml | 50 +++------ 7 files changed, 184 insertions(+), 208 deletions(-) diff --git a/ansible/group_vars/all/vault.yml b/ansible/group_vars/all/vault.yml index 798e2d8..d1f18ec 100644 --- a/ansible/group_vars/all/vault.yml +++ b/ansible/group_vars/all/vault.yml @@ -1,45 +1,60 @@ $ANSIBLE_VAULT;1.1;AES256 -65343264303166346334396163363362326334626531336363363766326135393462373564313539 -6633346163343664393232363965383334303563396236360a316265623239333431636133306663 -65623237393564323936303936633036343137646637633963396166313462626165616463616665 -3235373562626238640a636163623535376565373835653831666361636563616230353639626131 -38393530363461346334356437613934306330666563326661343465653362313038346535386337 -62623964633764343964653061373137366539326433396231396339313231633632353465323139 -33333866383834376165303964353639386234646535626333363932333764646137663036303530 -32326635343631623535323830333934376466323338353234323464666435383436633139343037 -62666333383231356163383530353339336230373431616665333638633234656231393166303639 -32386134386264613839313564316164336461373738646630636630363731643066633132336636 -64663532663661653539653431353763303436323234383864353064333631643435633733366435 -32656332626633313134623361343965643962663134613363626265353865643738363738316232 -62356136623166303038653830393138646637636335366362323239316333616331383765663861 -33643331323766646463663862396232306233343134326436326235336165616662383636616539 -39373962336264636363303734343666646534343733666632626338393936393032626130643233 -30633963656166343834396431323233363036353165623934336532333662323932336630636235 -63326631366234383732316464383364316337393034303433646365343763633135336336376161 -33326436303863326666616234396438623864333538313363623261353962336430316132343830 -36353761326133666166393930343233643861366138363930623362336566386230316236353365 -65663632383436333536636161393133316334376262653330303238636431353966646666663665 -30386137623066343135616534323336313162643066636330323934646162303362373838313532 -61386163356537613334616537656134373835326265393765303436396233383239393365336136 -64623130383538306331323939386634316663663263306362363830383930346232313965613035 -33353235663230663635316136666335343233646539613832643431393134303865616232636435 -66366437353863336235646438353431616465396365373562636461623938303663326461656134 -66353735343237363164326236663363376333316163353462626234393861663535323463623233 -66623934336662323830616362653035353563343564636431383837643961303637366131633132 -33376133376465623030643935643737636339333236316566636663323934626265633863343963 -38626563323166343264643465626264393838643330393739386330656461656561306539653232 -63393838626233386531353063366131366237633162393537363963633361386238643232336230 -36363062386262303364303736373061316265373730366437326166336163386335623665323234 -35383865646466363534383833653764303833323230613863313735393264336132303934633465 -65616138636134613361316539343739353735666538653438333264623034306633663634306438 -33643165353534623039626536636433373963383866303535333931633633393138663464336562 -36643432366233323235613139626236346135633237613265343933646434373939396265626366 -34313835346463303132326537323166363032306133623662343736393435383330356433306138 -65653931613832313832323936353638303937613863646430633266306330656135386533306462 -30663366396566393134653663356531316639303635343236666333363637636433336533356663 -35346261363134326165316463626439306130653165636134616434663736616666643963336464 -61643834613164336234386237393333636234306162306563363430646262363963366666653666 -64323036323731626236343361613566643565666138386631633462613836613766626562353934 -32363936626466346464356131653232326330386239666638626333346238623964383666393961 -38393264663636353161663636393733396332333838333962393439346533353332653362356230 -62613039376539636363 +61356165613635386631393135656434646436303665313031346566323336313138353433316463 +3363323534613064643132663335623238366431393062340a346538396662306537663163623366 +38626166383933616331623231373137306562623637313263333237633661663436666266616433 +3862346438643638650a306634333535653633613534646630386131333236366538333765323333 +38656163303837303732663561373232393132343331663164656262393730326434373731333636 +36613538646431396536363936336562616431656665653965373864633366663836353434626434 +34383932373461333564303439623565383661646365386665393831383463663662356536356236 +36373964666236626465366161636135393734356536633466383262326537343833636630343738 +62353066316131613737373162643363653662656261363465386364323962656537373061373032 +33343763353464383438363438343965653532393831343930393562633630383932653862623637 +38386239353237356631646436356166373961333464396639383538383662326534343339313330 +65373339356364636634616532363832386631323062363530313861336238353261353334306235 +62616338396431316537346638656365356564346666366366343638356261623664393263323937 +30343261363562383332323462336435376664386134646562643836363834313237373631353731 +35653535663864643266313332356635363262363533663232656531373130336539633066376139 +32316665393831663035623962656364363831333563366135636164346335383738363336663566 +63356532643563393939383635386462663561386434323939303431653438653131363538383034 +33623933333464363032656636643033326162626163353633343062633966343332383138363963 +36383831663562616533316436366566323061386535343538393861383462333166343562316633 +30623665623035393537393965626363323132656433313339396233356666346634316332616336 +37363635363330323230373332326565343530653335383437373230366563366237633665626331 +66623336626230663361636439316337393865383035326136653264666438666566646132353036 +38616264313833316536623238633339373466613866626366383835656638623863323838653030 +63303938376164653966356435386333363731656666313234663535666165646233313137343563 +36386437393139656438333262383437656666343831313239323961373637653163643664356565 +62346431393133656530316262303763646165643336396661666431383436323562336137653031 +62626538373839613734396366653065306534636630346338316237616161613037616364356431 +38643965376133616161336633383664326230383435363334353137303162663738313331346238 +39356161616533616134356231323530306338333162343363353531303263636632613036386638 +63633136386232306234323936303563646466313935326631396565383432386130656638616266 +32383334363237336539396665336366643764633131643663643137376438323666326435626461 +64346633636431393137633537306431646564386565303933636434386462346630626537346438 +32623333666133303061646564366366326665363163396262633164323631636337346130303239 +38373936663337356134666132303165393365663763396362623434633737373538653566646134 +62316164396438303532616266313062326666633130656338653139376634306664633031333037 +31636166306565353334633435656233336233363664306264626237623366336161303134353433 +63383739666462623336386537346662633666626466393039653439346436653937633537396436 +35393339383066326630353066623132333034656539363561346462626265363263303535343961 +39316461616630326539613731303039613736393633373338646266323938326162373831346336 +30656130343463366534323030646238313465306266383034623065636665623366333063383736 +38373063393837306462303564643962373334343139626338623935336435643730646532633630 +66363730386636633639346463363365343239373265303738353732653633653437636130636664 +39386365353334653765343335303263363461313965383664326563333734626533376436626530 +63353163323637303730353564383733653365613635353764333266393532653663326533646132 +39313763373735323835626437306435373238653432393936643165663663656665316132653330 +35383038353532656434366336346235363563636264303734633138323963396562306232646236 +38366536306561653937336333373434336164663336613839353439356435333833396363636437 +38323934613735643363656233333037336465336564313966623063376566663030303230323262 +65643564666534326234306164343365383632333061316238623565353538313538396364313337 +35336230313339643736653238386231343661623337306236383665356632366236356335323530 +66373831616239376231636361333430343433303233393066323865663434643433303832373262 +36366534646161323130393931626362626139663139643263366639656531613436313533363130 +65643736363833333939613566663339623964333262643863333030623138633464386238613934 +31323639333234336264313663376465323737353766643839303665313737336534386665363034 +30326439666433373232306136306365343764643434306561343339353132346430646436343362 +32333765616262363930353435616563333736313533653339656231316230346166363335356638 +37333561366339613136306438306130343230663732333862663838396463623661303961336433 +65646332373139303462303633346432366530643130366133363937653739653036366136373434 +343864373734666431326239373866313734 diff --git a/ansible/infra_secrets.yml b/ansible/infra_secrets.yml index 798e2d8..4803d86 100644 --- a/ansible/infra_secrets.yml +++ b/ansible/infra_secrets.yml @@ -1,45 +1,60 @@ $ANSIBLE_VAULT;1.1;AES256 -65343264303166346334396163363362326334626531336363363766326135393462373564313539 -6633346163343664393232363965383334303563396236360a316265623239333431636133306663 -65623237393564323936303936633036343137646637633963396166313462626165616463616665 -3235373562626238640a636163623535376565373835653831666361636563616230353639626131 -38393530363461346334356437613934306330666563326661343465653362313038346535386337 -62623964633764343964653061373137366539326433396231396339313231633632353465323139 -33333866383834376165303964353639386234646535626333363932333764646137663036303530 -32326635343631623535323830333934376466323338353234323464666435383436633139343037 -62666333383231356163383530353339336230373431616665333638633234656231393166303639 -32386134386264613839313564316164336461373738646630636630363731643066633132336636 -64663532663661653539653431353763303436323234383864353064333631643435633733366435 -32656332626633313134623361343965643962663134613363626265353865643738363738316232 -62356136623166303038653830393138646637636335366362323239316333616331383765663861 -33643331323766646463663862396232306233343134326436326235336165616662383636616539 -39373962336264636363303734343666646534343733666632626338393936393032626130643233 -30633963656166343834396431323233363036353165623934336532333662323932336630636235 -63326631366234383732316464383364316337393034303433646365343763633135336336376161 -33326436303863326666616234396438623864333538313363623261353962336430316132343830 -36353761326133666166393930343233643861366138363930623362336566386230316236353365 -65663632383436333536636161393133316334376262653330303238636431353966646666663665 -30386137623066343135616534323336313162643066636330323934646162303362373838313532 -61386163356537613334616537656134373835326265393765303436396233383239393365336136 -64623130383538306331323939386634316663663263306362363830383930346232313965613035 -33353235663230663635316136666335343233646539613832643431393134303865616232636435 -66366437353863336235646438353431616465396365373562636461623938303663326461656134 -66353735343237363164326236663363376333316163353462626234393861663535323463623233 -66623934336662323830616362653035353563343564636431383837643961303637366131633132 -33376133376465623030643935643737636339333236316566636663323934626265633863343963 -38626563323166343264643465626264393838643330393739386330656461656561306539653232 -63393838626233386531353063366131366237633162393537363963633361386238643232336230 -36363062386262303364303736373061316265373730366437326166336163386335623665323234 -35383865646466363534383833653764303833323230613863313735393264336132303934633465 -65616138636134613361316539343739353735666538653438333264623034306633663634306438 -33643165353534623039626536636433373963383866303535333931633633393138663464336562 -36643432366233323235613139626236346135633237613265343933646434373939396265626366 -34313835346463303132326537323166363032306133623662343736393435383330356433306138 -65653931613832313832323936353638303937613863646430633266306330656135386533306462 -30663366396566393134653663356531316639303635343236666333363637636433336533356663 -35346261363134326165316463626439306130653165636134616434663736616666643963336464 -61643834613164336234386237393333636234306162306563363430646262363963366666653666 -64323036323731626236343361613566643565666138386631633462613836613766626562353934 -32363936626466346464356131653232326330386239666638626333346238623964383666393961 -38393264663636353161663636393733396332333838333962393439346533353332653362356230 -62613039376539636363 +34616531626537376463326332306434383163393761363536633133363161373631376437653234 +3831656536313531343433376264643261396634633965370a303931336236323766313065636535 +31343338616364386132623232373266663665353563666535383232666262663062323864303932 +3439373734316639650a633732333064633335383833663261326464336538636666323063646331 +66653464636534663935616461636132613162303530663237376661343338396133323431623561 +30303563303465633231306339326561313533333536376433393130376634303932643339656336 +30616130343733626162366561393939393766623138616537393032376665396139333561623830 +33623130336235633034666561383165616439316334323664623661343438663733373833306335 +30396165313731636435636263353133326135346236323030653734353831626238666663656364 +62386164363361373464383730333733626333333736306535336635613634646535383237326332 +37636532663933663739303634333235343137333363316430643263326231613635643633636437 +30336462633233396430633939616661643261666136656363363461343230653738613166346434 +63376363666462346539343063343562623962303130616535306439653134633630353963373337 +64313239353162363862643432666438383330636638333464323731643163643236313535313030 +65656231383738356537326661663163376634613031396436646633376139313237343534653065 +64323532346133313265346334353530393633396339316330366536646565353836396662303866 +65373030393463306664623235626437613965303837313365373632643935656630303936373837 +64663539623764656636613263346638376665626262663430333231633735653563643864343835 +62303637383332653366383066326536316336306539623230353066343739316430356437313365 +33323031613130643832636133636263303565623330306135333762363036633737343933623437 +33633436363062643436376632336235666330656265316333323361316430656631383734343238 +65616366666536363866343361666563643336306532666332643230656264303565353032393932 +36613738326239376161383064646634623639356439663965323237303361343838373737636235 +39656465333063633136643463356564636537326339653033633063366265636138656466646264 +32643961343863656464353439346531633036613138353333353631626665396239326465303632 +39336664333038636435366333623336353963323535316436646136313739373537376537353735 +66333265633232346536313933333836643439656666636236626266323333383935383565616139 +38626133356364613562393664643563346539366364636332613162663337356232333735393431 +30343635393462356431646336636663333735343164616261343836363366616162613563613066 +66333034613234626431356536323832393466363135653231336238356334353638613838626430 +63636630313533363563376161663637353362383431663130646666646433353864373736613662 +62343666633862643839666532656132653939663437373164393536653739376265303130353636 +62616538326330333136626238306133363634326365366637363961333635356133613564373131 +64626139656633663065353862343135393233333231386166346532656534623361623061336434 +37616565626134333863376532393861376132323434656433613731613834633963386635353336 +39653263346637303836663763653264396139383363616461393333333362616332663863363834 +61613832326466646133626337663364333566326633313664613236346135653332373035313034 +62356530313065393737393634313166313039616635363365633031336434396463326462316236 +62353838636533303664663132353838636632353733316639643964666139383539333761663531 +37366530373165653063303032383461326535613336343635626538373635356266396666346133 +61383434323565306438356634646338666232363238393932353365363461376130323363363236 +34643862653937353461383265663933646264373365623465313666643662633334366437646434 +34343332613832366235663563353435323738356339383338333361383561336366376238393237 +38656265663164376666616366623062393530366261623361383365376235643035353838313235 +32396430376139313430316439333539343266393965353030636638656661316137313437663832 +64366661323865383262323036613230316233666566386133363633303931323464663238323861 +62333132346165353830653062353763306232366563323430383933653163373562326536393362 +66353931376262383931613931303034356537363137366235643430323465623162363031373034 +64653563666533363062333831336463376530306639616631303266386136666233653638653732 +31313537393838333730303235363661326333646531653935633935373362303732653565656438 +36626330623063373832333739643666396135653838643164376264393863646433353036643731 +34623636623366353466333237373265323936643134613230326565303663306431373634356334 +37613732393262626135306632623336613364323632316636623037656538313236666562633438 +37306361333130376538366232393637646263656239303431623963626265386634313735373162 +64393733303132383464386330626135336237303563326238633437346164333738646431333730 +62616533363638643261306565373835303832313539656564386132393863373039653938616261 +36343031356361336563316230343433633033353130373933313361326164633765316433616162 +62356338393634653330623933313433343630386264396337373532376335316362333164363963 +363663666365336564663232363462613162 diff --git a/ansible/roles/caddy_site/handlers/main.yml b/ansible/roles/caddy_site/handlers/main.yml index fb57280..4bfa7ff 100644 --- a/ansible/roles/caddy_site/handlers/main.yml +++ b/ansible/roles/caddy_site/handlers/main.yml @@ -2,7 +2,11 @@ # Fires once at the end of the play, however many sites notified it. # Anything later in the same play that needs the new config live must be # preceded by `- ansible.builtin.meta: flush_handlers`. +# become is explicit because handlers do not inherit it from the task that +# notified them. headscale's play runs become: no and elevates per task, so +# without this the reload would run unprivileged and fail. - name: Reload caddy + become: true ansible.builtin.systemd: name: caddy state: reloaded diff --git a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml index 3fa36d4..860b765 100644 --- a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml +++ b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml @@ -498,64 +498,29 @@ vars: datum_gateway_subdomain: "{{ subdomains.datum_gateway }}" datum_gateway_domain: "{{ datum_gateway_subdomain }}.{{ root_domain }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: "0755" + - name: Publish the DATUM Gateway dashboard through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: datum-gateway + caddy_site_domain: "{{ datum_gateway_domain }}" + caddy_site_upstream: "knots-box:{{ datum_gateway_api_port }}" + caddy_site_resolvers: "100.100.100.100" + caddy_site_basic_auth: + - user: "{{ datum_dashboard_username }}" + hash: "{{ datum_dashboard_password_hash }}" - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: "import sites-enabled/*" - insertafter: EOF - state: present - backup: yes - create: yes - mode: "0644" - - - name: Create Caddy reverse proxy config for DATUM Gateway dashboard - copy: - dest: "{{ caddy_sites_dir }}/datum-gateway.conf" - content: | - {{ datum_gateway_domain }} { - basic_auth { - {{ datum_dashboard_username }} {{ datum_dashboard_password_hash }} - } - reverse_proxy knots-box:{{ datum_gateway_api_port }} { - # Resolve via Tailscale MagicDNS - transport http { - resolvers 100.100.100.100 - } - } - } - owner: root - group: root - mode: "0644" - - - name: Validate Caddy config - command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile - register: caddy_validate + # The role validates the site fragment on its own. This re-validates the + # whole assembled Caddyfile, which is the only thing that catches a + # conflict between this site and another. Kept from the hand-rolled + # version; the other nine services never had it. + - name: Validate the assembled Caddyfile + ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile changed_when: false - - name: Display Caddy validation output - debug: - msg: "{{ caddy_validate.stdout_lines + caddy_validate.stderr_lines }}" - - - name: Reload Caddy - command: systemctl reload caddy - register: caddy_reload - - - name: Display Caddy reload output - debug: - msg: "{{ caddy_reload.stdout_lines + caddy_reload.stderr_lines }}" - - name: Display DATUM Gateway dashboard URL when: uptime_kuma_enabled | default(false) debug: diff --git a/ansible/services/headscale/deploy_headscale_playbook.yml b/ansible/services/headscale/deploy_headscale_playbook.yml index 61ba22e..527e4c2 100644 --- a/ansible/services/headscale/deploy_headscale_playbook.yml +++ b/ansible/services/headscale/deploy_headscale_playbook.yml @@ -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. diff --git a/ansible/services/headscale/headscale_vars.yml b/ansible/services/headscale/headscale_vars.yml index 0b0ee50..05fc040 100644 --- a/ansible/services/headscale/headscale_vars.yml +++ b/ansible/services/headscale/headscale_vars.yml @@ -22,3 +22,6 @@ remote_port: "{{ hostvars.get(remote_host_name, {}).get('ansible_port', 22) }}" # Local backup local_backup_dir: "{{ lookup('env', 'HOME') }}/headscale-backups" backup_script_path: "{{ lookup('env', 'HOME') }}/.local/bin/headscale_backup.sh" + +# Headplane (headscale admin UI), proxied at /admin* behind Caddy basic auth +headplane_port: 3000 diff --git a/ansible/services/ntfy/deploy_ntfy_playbook.yml b/ansible/services/ntfy/deploy_ntfy_playbook.yml index d030123..61fafe1 100644 --- a/ansible/services/ntfy/deploy_ntfy_playbook.yml +++ b/ansible/services/ntfy/deploy_ntfy_playbook.yml @@ -8,7 +8,6 @@ - ./ntfy_vars.yml vars: ntfy_subdomain: "{{ subdomains.ntfy }}" - caddy_sites_dir: "{{ caddy_sites_dir }}" ntfy_domain: "{{ ntfy_subdomain }}.{{ root_domain }}" tasks: @@ -76,42 +75,23 @@ shell: | (echo "{{ ntfy_password }}"; echo "{{ ntfy_password }}") | ntfy user add --role=admin "{{ ntfy_username }}" - - name: Ensure Caddy sites-enabled directory exists - file: - path: "{{ caddy_sites_dir }}" - state: directory - owner: root - group: root - mode: '0755' + - name: Publish ntfy through Caddy + ansible.builtin.include_role: + name: caddy_site + vars: + caddy_site_name: ntfy + caddy_site_domain: "{{ ntfy_domain }}, http://{{ ntfy_domain }}" + # Raw body: ntfy needs a plain-HTTP listener for its CLI/app clients, + # with only GETs to the docs and topic paths redirected to HTTPS. + caddy_site_body: | + reverse_proxy 127.0.0.1:{{ ntfy_port }} - - name: Ensure Caddyfile includes import directive for sites-enabled - lineinfile: - path: /etc/caddy/Caddyfile - line: 'import sites-enabled/*' - insertafter: EOF - state: present - backup: yes - - - name: Create Caddy reverse proxy configuration for ntfy - copy: - dest: "{{ caddy_sites_dir }}/ntfy.conf" - content: | - {{ ntfy_domain }}, http://{{ ntfy_domain }} { - reverse_proxy 127.0.0.1:{{ ntfy_port }} - - @httpget { - protocol http - method GET - path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) - } - redir @httpget https://{host}{uri} + @httpget { + protocol http + method GET + path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/) } - owner: root - group: root - mode: '0644' - - - name: Reload Caddy to apply new config - command: systemctl reload caddy + redir @httpget https://{host}{uri} handlers: - name: Restart ntfy From c4094b692fddde6efc53b37cbadab632b1da1b17 Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:50:15 +0200 Subject: [PATCH 08/10] bitcoin-knots, fulcrum, datum-gateway: add and use the socket_proxy role Three near-identical hosts: edge plays become one role plus three short calls. 183 lines removed, 34 added, plus a 111-line role. Verified before touching any playbook: all six live units on vipy reproduced byte-identically. Then --limit edge --check per playbook - bitcoin-knots and fulcrum changed=0; datum-gateway changed=2, both attributable to the already known caddy_site comment line and the Reload caddy handler it triggers. The 6 units and 14 Caddy files on the hosts are byte-identical afterwards. PLAN_4 claimed these three plays had "no behavioural drift at all". That was wrong - it came from a diff truncated by head -60. The live bitcoin-p2p-proxy units carry four settings this playbook never wrote: .socket Documentation=, FreeBind=true .service Documentation=, TimeoutStopSec=5, StandardOutput=journal, StandardError=journal FreeBind is the one that matters: it lets the socket bind to an address that is not up yet, so without it the socket can fail to start on boot. Running the bitcoin-knots playbook would have stripped it. Same class of hazard as headscale. The role expresses all four; bitcoin-p2p is the only caller that passes any. Also: UFW treats the rule comment as part of the rule. datum-stratum's live comment is "DATUM Gateway Stratum public access" but the role's derived default produced "DATUM Stratum public access", which rewrote the rule. Caught in the dry-run; datum now passes the comment explicitly. Two deliberate differences from the original, both documented in the README: ignore_errors: yes on the upstream check became failed_when: false, and the handler restarts the .socket, which drops connections open through it - it fires only when a unit file actually changes. The inert Uptime Kuma TCP monitor blocks stay in the playbooks rather than being pulled into a new role (12/12/18 guarded tasks). Co-Authored-By: Claude Opus 5 (1M context) --- ansible/roles/socket_proxy/README.md | 62 +++++++++++++++ ansible/roles/socket_proxy/defaults/main.yml | 17 +++++ ansible/roles/socket_proxy/handlers/main.yml | 8 ++ ansible/roles/socket_proxy/tasks/main.yml | 54 +++++++++++++ .../socket_proxy/templates/proxy.service.j2 | 18 +++++ .../socket_proxy/templates/proxy.socket.j2 | 14 ++++ .../deploy_bitcoin_knots_playbook.yml | 76 ++++--------------- .../deploy_datum_gateway_playbook.yml | 72 +++--------------- .../fulcrum/deploy_fulcrum_playbook.yml | 69 ++--------------- 9 files changed, 207 insertions(+), 183 deletions(-) create mode 100644 ansible/roles/socket_proxy/README.md create mode 100644 ansible/roles/socket_proxy/defaults/main.yml create mode 100644 ansible/roles/socket_proxy/handlers/main.yml create mode 100644 ansible/roles/socket_proxy/tasks/main.yml create mode 100644 ansible/roles/socket_proxy/templates/proxy.service.j2 create mode 100644 ansible/roles/socket_proxy/templates/proxy.socket.j2 diff --git a/ansible/roles/socket_proxy/README.md b/ansible/roles/socket_proxy/README.md new file mode 100644 index 0000000..cebb95f --- /dev/null +++ b/ansible/roles/socket_proxy/README.md @@ -0,0 +1,62 @@ +# `socket_proxy` + +Exposes a service running on a private Tailscale host through a public TCP port +on an edge machine, using `systemd-socket-proxyd`. Writes a `.socket` and a +`.service` unit, enables the socket, opens the UFW port, and checks the upstream +is reachable. + +## Usage + +```yaml +- ansible.builtin.include_role: + name: socket_proxy + vars: + socket_proxy_name: fulcrum-ssl # -> fulcrum-ssl-proxy.{socket,service} + socket_proxy_description: "Fulcrum SSL" # -> "Fulcrum SSL Proxy Socket" + socket_proxy_listen_port: "{{ fulcrum_ssl_port }}" + socket_proxy_upstream_host: "{{ fulcrum_tailscale_hostname }}" +``` + +`socket_proxy_upstream_port` defaults to `socket_proxy_listen_port`, which is +what all three current callers want. + +## Optional unit settings + +These exist because the **live** `bitcoin-p2p-proxy` units on vipy carried +settings the playbook never wrote. Somebody added them by hand, so running +`deploy_bitcoin_knots_playbook.yml` would have silently removed them: + +| Variable | Emits | Why it matters | +|---|---|---| +| `socket_proxy_free_bind` | `FreeBind=true` in `[Socket]` | Lets the socket bind to an address that is not up yet. Without it the socket can fail to start on boot. | +| `socket_proxy_documentation` | `Documentation=` in both units | Cosmetic. | +| `socket_proxy_timeout_stop_sec` | `TimeoutStopSec=` | Bounds how long a stop can hang. | +| `socket_proxy_log_to_journal` | `StandardOutput=journal` + `StandardError=journal` | Cosmetic on modern systemd, which defaults to the journal anyway. | + +Only `bitcoin-p2p` passes any of them. + +## `socket_proxy_ufw_comment` + +Defaults to `" public access"`, which reproduces the live rule +comment for bitcoin-p2p and fulcrum-ssl. **datum-stratum must pass it +explicitly** — its live comment is `DATUM Gateway Stratum public access` while +the derived default would be `DATUM Stratum public access`, and UFW treats the +comment as part of the rule, so the mismatch rewrites the rule on every run. + +## The upstream check never fails the play + +`wait_for` on the upstream carries `failed_when: false`. The proxy is correctly +configured whether or not the backend happens to be up, and this is the one task +that depends on another machine. The original plays used `ignore_errors: yes`, +which prints a red "ignoring" line; `failed_when: false` is the quieter +equivalent. + +## Restarts + +The handler restarts the `.socket`, not the `.service` — that is what picks up a +changed unit; the service is started by the socket on the next connection. + +**Restarting a socket drops connections that are currently open through it.** +For bitcoin-p2p that means peers reconnect; for datum-stratum it means a mining +client has to reconnect and may lose in-flight shares. The handler only fires +when a unit file actually changes. diff --git a/ansible/roles/socket_proxy/defaults/main.yml b/ansible/roles/socket_proxy/defaults/main.yml new file mode 100644 index 0000000..6a512ea --- /dev/null +++ b/ansible/roles/socket_proxy/defaults/main.yml @@ -0,0 +1,17 @@ +--- +# Required +socket_proxy_name: "" # "bitcoin-p2p" -> bitcoin-p2p-proxy.{socket,service} +socket_proxy_description: "" # "Bitcoin P2P" -> "Bitcoin P2P Proxy Socket" +socket_proxy_listen_port: 0 # public port on the edge host +socket_proxy_upstream_host: "" # Tailscale hostname, e.g. "knots-box" + +# Optional +socket_proxy_upstream_port: "" # defaults to socket_proxy_listen_port +socket_proxy_documentation: "" # Documentation= in both units +socket_proxy_free_bind: false # FreeBind=true: bind before the address is up +socket_proxy_timeout_stop_sec: "" # TimeoutStopSec= +socket_proxy_log_to_journal: false # StandardOutput/StandardError=journal + +# Firewall +socket_proxy_ufw_proto: tcp +socket_proxy_ufw_comment: "" # defaults to " public access" diff --git a/ansible/roles/socket_proxy/handlers/main.yml b/ansible/roles/socket_proxy/handlers/main.yml new file mode 100644 index 0000000..7fdf421 --- /dev/null +++ b/ansible/roles/socket_proxy/handlers/main.yml @@ -0,0 +1,8 @@ +--- +# Restarting the .socket is what picks up a changed unit; the .service is +# started by the socket on the next connection. +- name: Restart socket proxy + ansible.builtin.systemd: + name: "{{ socket_proxy_name }}-proxy.socket" + state: restarted + daemon_reload: yes diff --git a/ansible/roles/socket_proxy/tasks/main.yml b/ansible/roles/socket_proxy/tasks/main.yml new file mode 100644 index 0000000..4315555 --- /dev/null +++ b/ansible/roles/socket_proxy/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: Assert socket_proxy parameters are sane + ansible.builtin.assert: + that: + - socket_proxy_name | length > 0 + - socket_proxy_description | length > 0 + - socket_proxy_listen_port | int > 0 + - socket_proxy_upstream_host | length > 0 + fail_msg: >- + socket_proxy: '{{ socket_proxy_name | default("") }}' needs a name, + a description, a listen port and an upstream host. + quiet: true + +- name: "Create the {{ socket_proxy_name }}-proxy socket unit" + ansible.builtin.template: + src: proxy.socket.j2 + dest: "/etc/systemd/system/{{ socket_proxy_name }}-proxy.socket" + owner: root + group: root + mode: '0644' + notify: Restart socket proxy + +- name: "Create the {{ socket_proxy_name }}-proxy service unit" + ansible.builtin.template: + src: proxy.service.j2 + dest: "/etc/systemd/system/{{ socket_proxy_name }}-proxy.service" + owner: root + group: root + mode: '0644' + notify: Restart socket proxy + +- name: "Enable and start the {{ socket_proxy_name }}-proxy socket" + ansible.builtin.systemd: + name: "{{ socket_proxy_name }}-proxy.socket" + enabled: yes + state: started + daemon_reload: yes + +- name: "Allow the {{ socket_proxy_name }} port through UFW" + community.general.ufw: + rule: allow + port: "{{ socket_proxy_listen_port | string }}" + proto: "{{ socket_proxy_ufw_proto }}" + comment: "{{ socket_proxy_ufw_comment | default(socket_proxy_description ~ ' public access', true) }}" + +# Reachability of the upstream over Tailscale. Deliberately non-fatal: the proxy +# is still correctly configured if the backend happens to be down, and this is +# the one check that depends on another machine being up. +- name: "Verify {{ socket_proxy_upstream_host }} is reachable over Tailscale" + ansible.builtin.wait_for: + host: "{{ socket_proxy_upstream_host }}" + port: "{{ socket_proxy_upstream_port | default(socket_proxy_listen_port, true) }}" + timeout: 10 + failed_when: false diff --git a/ansible/roles/socket_proxy/templates/proxy.service.j2 b/ansible/roles/socket_proxy/templates/proxy.service.j2 new file mode 100644 index 0000000..e59d251 --- /dev/null +++ b/ansible/roles/socket_proxy/templates/proxy.service.j2 @@ -0,0 +1,18 @@ +[Unit] +Description={{ socket_proxy_description }} Proxy to {{ socket_proxy_upstream_host }} +{% if socket_proxy_documentation %} +Documentation={{ socket_proxy_documentation }} +{% endif %} +Requires={{ socket_proxy_name }}-proxy.socket +After=network.target + +[Service] +Type=notify +ExecStart=/lib/systemd/systemd-socket-proxyd {{ socket_proxy_upstream_host }}:{{ socket_proxy_upstream_port | default(socket_proxy_listen_port, true) }} +{% if socket_proxy_timeout_stop_sec %} +TimeoutStopSec={{ socket_proxy_timeout_stop_sec }} +{% endif %} +{% if socket_proxy_log_to_journal %} +StandardOutput=journal +StandardError=journal +{% endif %} diff --git a/ansible/roles/socket_proxy/templates/proxy.socket.j2 b/ansible/roles/socket_proxy/templates/proxy.socket.j2 new file mode 100644 index 0000000..0dc1721 --- /dev/null +++ b/ansible/roles/socket_proxy/templates/proxy.socket.j2 @@ -0,0 +1,14 @@ +[Unit] +Description={{ socket_proxy_description }} Proxy Socket +{% if socket_proxy_documentation %} +Documentation={{ socket_proxy_documentation }} +{% endif %} + +[Socket] +ListenStream={{ socket_proxy_listen_port }} +{% if socket_proxy_free_bind %} +FreeBind=true +{% endif %} + +[Install] +WantedBy=sockets.target diff --git a/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml b/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml index fcc02b4..4e9faa3 100644 --- a/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml +++ b/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml @@ -753,62 +753,21 @@ uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - - name: Create Bitcoin P2P proxy socket unit - copy: - dest: /etc/systemd/system/bitcoin-p2p-proxy.socket - content: | - [Unit] - Description=Bitcoin P2P Proxy Socket - - [Socket] - ListenStream={{ bitcoin_p2p_port }} - - [Install] - WantedBy=sockets.target - owner: root - group: root - mode: '0644' - notify: Restart bitcoin-p2p-proxy socket - - - name: Create Bitcoin P2P proxy service unit - copy: - dest: /etc/systemd/system/bitcoin-p2p-proxy.service - content: | - [Unit] - Description=Bitcoin P2P Proxy to {{ bitcoin_tailscale_hostname }} - Requires=bitcoin-p2p-proxy.socket - After=network.target - - [Service] - Type=notify - ExecStart=/lib/systemd/systemd-socket-proxyd {{ bitcoin_tailscale_hostname }}:{{ bitcoin_p2p_port }} - owner: root - group: root - mode: '0644' - - - name: Reload systemd daemon - systemd: - daemon_reload: yes - - - name: Enable and start Bitcoin P2P proxy socket - systemd: - name: bitcoin-p2p-proxy.socket - enabled: yes - state: started - - - name: Allow Bitcoin P2P port through UFW - ufw: - rule: allow - port: "{{ bitcoin_p2p_port | string }}" - proto: tcp - comment: "Bitcoin P2P public access" - - - name: Verify connectivity to knots-box via Tailscale - wait_for: - host: "{{ bitcoin_tailscale_hostname }}" - port: "{{ bitcoin_p2p_port }}" - timeout: 10 - ignore_errors: yes + - name: Expose Bitcoin P2P through a socket proxy + ansible.builtin.include_role: + name: socket_proxy + vars: + socket_proxy_name: bitcoin-p2p + socket_proxy_description: "Bitcoin P2P" + socket_proxy_listen_port: "{{ bitcoin_p2p_port }}" + socket_proxy_upstream_host: "{{ bitcoin_tailscale_hostname }}" + # These four were added by hand on vipy and were NOT in this playbook; + # writing the unit without them would have dropped FreeBind, which lets + # the socket bind before the address is up. + socket_proxy_documentation: "https://github.com/bitcoin/bitcoin" + socket_proxy_free_bind: true + socket_proxy_timeout_stop_sec: 5 + socket_proxy_log_to_journal: true - name: Display public endpoint when: uptime_kuma_enabled | default(false) @@ -931,8 +890,3 @@ - /tmp/setup_bitcoin_p2p_tcp_monitor.py - /tmp/ansible_bitcoin_p2p_config.yml - handlers: - - name: Restart bitcoin-p2p-proxy socket - systemd: - name: bitcoin-p2p-proxy.socket - state: restarted diff --git a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml index 860b765..1ddc75c 100644 --- a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml +++ b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml @@ -663,62 +663,17 @@ uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - - name: Create Stratum proxy socket unit - copy: - dest: /etc/systemd/system/datum-stratum-proxy.socket - content: | - [Unit] - Description=DATUM Stratum Proxy Socket - - [Socket] - ListenStream={{ datum_gateway_stratum_port }} - - [Install] - WantedBy=sockets.target - owner: root - group: root - mode: "0644" - notify: Restart datum-stratum-proxy socket - - - name: Create Stratum proxy service unit - copy: - dest: /etc/systemd/system/datum-stratum-proxy.service - content: | - [Unit] - Description=DATUM Stratum Proxy to {{ datum_tailscale_hostname }} - Requires=datum-stratum-proxy.socket - After=network.target - - [Service] - Type=notify - ExecStart=/lib/systemd/systemd-socket-proxyd {{ datum_tailscale_hostname }}:{{ datum_gateway_stratum_port }} - owner: root - group: root - mode: "0644" - - - name: Reload systemd daemon - systemd: - daemon_reload: yes - - - name: Enable and start Stratum proxy socket - systemd: - name: datum-stratum-proxy.socket - enabled: yes - state: started - - - name: Allow Stratum port through UFW - ufw: - rule: allow - port: "{{ datum_gateway_stratum_port | string }}" - proto: tcp - comment: "DATUM Gateway Stratum public access" - - - name: Verify connectivity to knots-box Stratum via Tailscale - wait_for: - host: "{{ datum_tailscale_hostname }}" - port: "{{ datum_gateway_stratum_port }}" - timeout: 10 - ignore_errors: yes + - name: Expose the DATUM Stratum port through a socket proxy + ansible.builtin.include_role: + name: socket_proxy + vars: + socket_proxy_name: datum-stratum + socket_proxy_description: "DATUM Stratum" + socket_proxy_listen_port: "{{ datum_gateway_stratum_port }}" + socket_proxy_upstream_host: "{{ datum_tailscale_hostname }}" + # Matches the UFW comment already on vipy; the derived default would + # have said "DATUM Stratum" and rewritten the rule. + socket_proxy_ufw_comment: "DATUM Gateway Stratum public access" - name: Display public Stratum endpoint when: uptime_kuma_enabled | default(false) @@ -845,8 +800,3 @@ - /tmp/setup_datum_stratum_tcp_monitor.py - /tmp/ansible_datum_stratum_config.yml - handlers: - - name: Restart datum-stratum-proxy socket - systemd: - name: datum-stratum-proxy.socket - state: restarted diff --git a/ansible/services/fulcrum/deploy_fulcrum_playbook.yml b/ansible/services/fulcrum/deploy_fulcrum_playbook.yml index a1548a7..6ffe254 100644 --- a/ansible/services/fulcrum/deploy_fulcrum_playbook.yml +++ b/ansible/services/fulcrum/deploy_fulcrum_playbook.yml @@ -552,62 +552,14 @@ uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - - name: Create Fulcrum SSL proxy socket unit - copy: - dest: /etc/systemd/system/fulcrum-ssl-proxy.socket - content: | - [Unit] - Description=Fulcrum SSL Proxy Socket - - [Socket] - ListenStream={{ fulcrum_ssl_port }} - - [Install] - WantedBy=sockets.target - owner: root - group: root - mode: '0644' - notify: Restart fulcrum-ssl-proxy socket - - - name: Create Fulcrum SSL proxy service unit - copy: - dest: /etc/systemd/system/fulcrum-ssl-proxy.service - content: | - [Unit] - Description=Fulcrum SSL Proxy to {{ fulcrum_tailscale_hostname }} - Requires=fulcrum-ssl-proxy.socket - After=network.target - - [Service] - Type=notify - ExecStart=/lib/systemd/systemd-socket-proxyd {{ fulcrum_tailscale_hostname }}:{{ fulcrum_ssl_port }} - owner: root - group: root - mode: '0644' - - - name: Reload systemd daemon - systemd: - daemon_reload: yes - - - name: Enable and start Fulcrum SSL proxy socket - systemd: - name: fulcrum-ssl-proxy.socket - enabled: yes - state: started - - - name: Allow Fulcrum SSL port through UFW - ufw: - rule: allow - port: "{{ fulcrum_ssl_port | string }}" - proto: tcp - comment: "Fulcrum SSL public access" - - - name: Verify connectivity to fulcrum-box via Tailscale - wait_for: - host: "{{ fulcrum_tailscale_hostname }}" - port: "{{ fulcrum_ssl_port }}" - timeout: 10 - ignore_errors: yes + - name: Expose Fulcrum SSL through a socket proxy + ansible.builtin.include_role: + name: socket_proxy + vars: + socket_proxy_name: fulcrum-ssl + socket_proxy_description: "Fulcrum SSL" + socket_proxy_listen_port: "{{ fulcrum_ssl_port }}" + socket_proxy_upstream_host: "{{ fulcrum_tailscale_hostname }}" - name: Display public endpoint when: uptime_kuma_enabled | default(false) @@ -730,9 +682,4 @@ - /tmp/setup_fulcrum_ssl_tcp_monitor.py - /tmp/ansible_fulcrum_ssl_config.yml - handlers: - - name: Restart fulcrum-ssl-proxy socket - systemd: - name: fulcrum-ssl-proxy.socket - state: restarted From cea2523e15c656cc04b960c8eb9b6a324a18115d Mon Sep 17 00:00:00 2001 From: counterweight Date: Fri, 11 Sep 2026 23:53:09 +0200 Subject: [PATCH 09/10] caddy: close out Plan 4 All five close-out greps return nothing: no sites-enabled handling outside roles/, no `systemctl reload caddy`, no caddy_sites_dir self-reference, no inline proxy unit writes. 37 playbooks syntax clean. The 14 Caddy site files and 6 proxy units on the hosts are byte-identical to the Stage 0 baseline. Seven play names still said "on vipy" while the play targeted a group. Renamed to "on the edge host" - the last place a play claimed a hostname after Plan 2. Documented the four vhosts in /etc/caddy/sites-enabled that no playbook writes (uptime-kuma, arbretstaging, bitcoininfra, scriberr) in the caddy_site README. None deleted. uptime-kuma.conf was going to be deleted as dead config. It is not dead: the louislam/uptime-kuma container is STILL RUNNING on watchtower - created 2026-02-07, restart=unless-stopped, healthy - and uptime.contrapeso.xyz returns 302, not the 502 a dead backend would give. The "decommissioning" retired the Ansible code and the vault credentials, not the service. PLAN_3 claimed "the tokens died with the server"; that is corrected there. The Caddyfile.* backups are kept: one per host, Nov-Dec 2025, not churning, and the only record of each Caddyfile before the import line was added. Co-Authored-By: Claude Opus 5 (1M context) --- ansible/roles/caddy_site/README.md | 24 +++++++++++++++++++ .../deploy_bitcoin_knots_playbook.yml | 2 +- .../deploy_datum_gateway_playbook.yml | 4 ++-- .../fulcrum/deploy_fulcrum_playbook.yml | 2 +- .../services/memos/deploy_memos_playbook.yml | 2 +- .../mempool/deploy_mempool_playbook.yml | 2 +- .../phoenixd/deploy_phoenixd_playbook.yml | 2 +- 7 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ansible/roles/caddy_site/README.md b/ansible/roles/caddy_site/README.md index 6891070..c8e32af 100644 --- a/ansible/roles/caddy_site/README.md +++ b/ansible/roles/caddy_site/README.md @@ -92,3 +92,27 @@ upstream hostname` in every case. `datum-gateway` previously said `# Resolve via Tailscale MagicDNS`. Migrating it therefore rewrites one comment line, which Caddy ignores. Every other site renders byte-identical to what its playbook produced. + +## Sites on the hosts that this role does NOT manage + +Four vhosts exist in `/etc/caddy/sites-enabled/` that no playbook writes. They +were made by hand. The role only ever writes the one file it is told to, so it +leaves them alone — but nothing in the repo records them, and that is why they +are listed here. Checked 2026-09-11: + +| File | Host | Serves | State | +|---|---|---|---| +| `uptime-kuma.conf` | watchtower | `localhost:3001` | **HTTP 302 — still live**, see below | +| `arbretstaging.conf` | vipy | `arbret-staging-box:80` via MagicDNS | HTTP 200 | +| `bitcoininfra.conf` | vipy | static `file_server` from `/var/www/bitcoin-services-home` | HTTP 200 | +| `scriberr.conf` | vipy | `scriberr-box:8080` via MagicDNS | HTTP 502 — upstream down | + +**`uptime-kuma.conf` must not be deleted as dead config.** Uptime Kuma was +"decommissioned" in the repo — its playbooks archived and its credentials pulled +from the vault — but the container is **still running** on watchtower +(`louislam/uptime-kuma:latest`, created 2026-02-07, `restart=unless-stopped`) +and is still reachable at its public subdomain. Only the Ansible code was +retired; the service was not. See `archive/uptime_kuma/`. + +`scriberr` returning 502 is the one that looks like genuine rot: it proxies to a +`scriberr-box` that is not answering, and `scriberr-box` is not in the inventory. diff --git a/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml b/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml index 4e9faa3..1ae5823 100644 --- a/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml +++ b/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml @@ -740,7 +740,7 @@ state: restarted -- name: Setup public Bitcoin P2P forwarding on vipy via systemd-socket-proxyd +- name: Setup public Bitcoin P2P forwarding on the edge host hosts: edge become: yes vars_files: diff --git a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml index 1ddc75c..0c53e9d 100644 --- a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml +++ b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml @@ -487,7 +487,7 @@ # =========================================== # Caddy Reverse Proxy for DATUM Dashboard (on vipy) # =========================================== -- name: Configure Caddy reverse proxy for DATUM Gateway dashboard on vipy +- name: Configure Caddy reverse proxy for DATUM Gateway dashboard on the edge host hosts: edge become: yes vars_files: @@ -650,7 +650,7 @@ # Miners connect to vipy:23334; traffic is forwarded to knots-box:23334 # over the Tailscale network, matching the Bitcoin P2P proxy pattern. # =========================================== -- name: Setup public Stratum port forwarding on vipy via systemd-socket-proxyd +- name: Setup public Stratum port forwarding on the edge host hosts: edge become: yes vars_files: diff --git a/ansible/services/fulcrum/deploy_fulcrum_playbook.yml b/ansible/services/fulcrum/deploy_fulcrum_playbook.yml index 6ffe254..01e9f17 100644 --- a/ansible/services/fulcrum/deploy_fulcrum_playbook.yml +++ b/ansible/services/fulcrum/deploy_fulcrum_playbook.yml @@ -540,7 +540,7 @@ state: restarted -- name: Setup public Fulcrum SSL forwarding on vipy via systemd-socket-proxyd +- name: Setup public Fulcrum SSL forwarding on the edge host hosts: edge become: yes vars_files: diff --git a/ansible/services/memos/deploy_memos_playbook.yml b/ansible/services/memos/deploy_memos_playbook.yml index 83c187e..04e99cf 100644 --- a/ansible/services/memos/deploy_memos_playbook.yml +++ b/ansible/services/memos/deploy_memos_playbook.yml @@ -156,7 +156,7 @@ state: restarted -- name: Configure Caddy reverse proxy for Memos on vipy (proxying via Tailscale) +- name: Configure Caddy reverse proxy for Memos on the edge host (via Tailscale) hosts: edge become: yes vars_files: diff --git a/ansible/services/mempool/deploy_mempool_playbook.yml b/ansible/services/mempool/deploy_mempool_playbook.yml index ba3e9e9..e9cab0e 100644 --- a/ansible/services/mempool/deploy_mempool_playbook.yml +++ b/ansible/services/mempool/deploy_mempool_playbook.yml @@ -603,7 +603,7 @@ - /tmp/ansible_mempool_config.yml - /tmp/mempool_push_urls.yml -- name: Configure Caddy reverse proxy for Mempool on vipy +- name: Configure Caddy reverse proxy for Mempool on the edge host hosts: edge become: yes vars_files: diff --git a/ansible/services/phoenixd/deploy_phoenixd_playbook.yml b/ansible/services/phoenixd/deploy_phoenixd_playbook.yml index f20ecdc..8b30a1f 100644 --- a/ansible/services/phoenixd/deploy_phoenixd_playbook.yml +++ b/ansible/services/phoenixd/deploy_phoenixd_playbook.yml @@ -19,7 +19,7 @@ # ⚠️ After the first run, back up {{ phoenixd_data_dir }}/seed.dat. Losing it # means losing the funds. See setup_backup_phoenixd_to_lapy.yml. -- name: Deploy phoenixd on vipy +- name: Deploy phoenixd on the edge host hosts: edge become: yes vars_files: From a8a2815c5f9099cf2d8a37baf5f273920dd315fd Mon Sep 17 00:00:00 2001 From: counterweight Date: Sat, 12 Sep 2026 15:14:47 +0200 Subject: [PATCH 10/10] inventory --- ansible/inventory.ini | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ansible/inventory.ini b/ansible/inventory.ini index 4643dbb..b9b2906 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -2,19 +2,20 @@ vipy ansible_host=167.172.107.33 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua watchtower ansible_host=164.92.239.72 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua spacey ansible_host=64.227.112.128 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua + [nodito_host] nodito ansible_host=192.168.1.139 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua - +# Requires the tailnet to be up on the control node. [nodito_vms] -knots_box_local ansible_host=192.168.1.135 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -fulcrum_box_local ansible_host=192.168.1.142 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -mempool_box_local ansible_host=192.168.1.140 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -memos_box_local ansible_host=192.168.1.130 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -forgejo_runner_local ansible_host=192.168.1.147 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -arbret_staging_local ansible_host=192.168.1.142 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -small_backups_local ansible_host=192.168.1.148 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua -nonkeiwaisi_local ansible_host=192.168.1.151 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +knots_box_local ansible_host=knots-box lan_ip=192.168.1.135 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +fulcrum_box_local ansible_host=fulcrum-box lan_ip=192.168.1.140 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +mempool_box_local ansible_host=mempool-box lan_ip=192.168.1.142 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +memos_box_local ansible_host=memos-box lan_ip=192.168.1.145 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +forgejo_runner_local ansible_host=forgejo-runner-box lan_ip=192.168.1.132 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +arbret_staging_local ansible_host=arbret-staging-box lan_ip=192.168.1.147 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +small_backups_local ansible_host=small-backups-box lan_ip=192.168.1.131 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua +nonkeiwaisi_local ansible_host=nonkeiwaisi-box lan_ip=192.168.1.151 ansible_user=counterweight ansible_port=22 ansible_ssh_private_key_file=~/.ssh/counterganzua # Local connection to laptop: this assumes you're running ansible commands from your personal laptop [lapy] @@ -65,3 +66,6 @@ nodito_vms edge monitoring vpn_control + +[backup_store] +small_backups_local