diff --git a/ansible/services/headscale/deploy_headscale_playbook.yml b/ansible/services/headscale/deploy_headscale_playbook.yml index e181f24..fe3a225 100644 --- a/ansible/services/headscale/deploy_headscale_playbook.yml +++ b/ansible/services/headscale/deploy_headscale_playbook.yml @@ -79,15 +79,24 @@ groups: headscale append: yes - - name: Create ACL policies file + # This used to write `{}` - an empty policy - over the real ACL, which was + # maintained by hand on the box. It had already flattened it once + # (/etc/headscale/acl.json.937464.2026-09-12@14:03:08~ is Ansible's own + # backup of the 1596-byte policy it replaced), and every subsequent + # site.yml run would have taken the whole tailnet's access rules with it. + # + # The policy now lives in templates/acl.json.j2, captured from the live + # file on 2026-09-19. validate first: a malformed policy is one headscale + # refuses to load, which is the same outage as an empty one. + - name: Deploy the Headscale ACL policy become: yes - copy: + ansible.builtin.template: + src: acl.json.j2 dest: /etc/headscale/acl.json - content: | - {} owner: headscale group: headscale mode: '0640' + validate: "python3 -m json.tool %s" notify: Restart headscale - name: Deploy headscale configuration file diff --git a/ansible/services/headscale/templates/acl.json.j2 b/ansible/services/headscale/templates/acl.json.j2 new file mode 100644 index 0000000..98ada80 --- /dev/null +++ b/ansible/services/headscale/templates/acl.json.j2 @@ -0,0 +1,130 @@ +{# + Headscale ACL policy. + + CAPTURED FROM THE LIVE FILE on spacey, 2026-09-19. Until then this playbook + wrote `{}` here - an empty policy - while the real rules were maintained by + hand on the box. Ansible had already flattened them once: + /etc/headscale/acl.json.937464.2026-09-12@14:03:08~ is its own backup of a + 1596-byte policy it replaced with {}. The next full site.yml run would have + done it again, taking the whole tailnet's access rules with it. + + So this is the source of truth now. It is a template and not inline `content:` + (the style used elsewhere in this playbook) precisely because it needs to be + diffable in review - an empty policy is the kind of change that must never + again slip through as three invisible characters. + + ADDING A HOST: it needs a tagOwners entry, and an acls rule naming the exact + ports its peers may reach. A node with no rule is INVISIBLE to the others - + it will not appear in `tailscale status` and MagicDNS will not resolve it, + which surfaces as a Caddy 502 with "no such host" rather than as a refused + connection. +#} +{ + "acls": [ + { + "action": "accept", + "dst": [ + "*:22", + "tag:arbret-staging-box:4117", + "tag:prd-arbret:4117", + "tag:nodito:8006" + ], + "src": [ + "tag:framie" + ] + }, + { + "action": "accept", + "dst": [ + "tag:knots-box:8333", + "tag:knots-box:7152", + "tag:knots-box:23334", + "tag:fulcrum-box:50002", + "tag:mempool-box:8080", + "tag:memos-box:5230", + "tag:arbret-staging-box:80", + "tag:stalwart-box:25", + "tag:stalwart-box:465", + "tag:stalwart-box:993", + "tag:stalwart-box:4190", + "tag:stalwart-box:8080" + ], + "src": [ + "tag:prd-vipy" + ] + }, + { + "action": "accept", + "dst": [ + "tag:knots-box:8332", + "tag:knots-box:28332-28335" + ], + "src": [ + "tag:fulcrum-box" + ] + }, + { + "action": "accept", + "dst": [ + "tag:knots-box:8332", + "tag:fulcrum-box:50001" + ], + "src": [ + "tag:mempool-box" + ] + }, + { + "action": "accept", + "dst": [ + "tag:prd-arbret:22", + "tag:prd-vipy:22", + "tag:memos-box:22", + "tag:stalwart-box:22" + ], + "src": [ + "tag:small-backups-box" + ] + } + ], + "tagOwners": { + "tag:arbret-staging-box": [ + "{{ headscale_namespace }}@" + ], + "tag:forgejo-runner-box": [ + "{{ headscale_namespace }}@" + ], + "tag:framie": [ + "{{ headscale_namespace }}@" + ], + "tag:fulcrum-box": [ + "{{ headscale_namespace }}@" + ], + "tag:knots-box": [ + "{{ headscale_namespace }}@" + ], + "tag:memos-box": [ + "{{ headscale_namespace }}@" + ], + "tag:mempool-box": [ + "{{ headscale_namespace }}@" + ], + "tag:nodito": [ + "{{ headscale_namespace }}@" + ], + "tag:prd-arbret": [ + "{{ headscale_namespace }}@" + ], + "tag:prd-monitoring": [ + "{{ headscale_namespace }}@" + ], + "tag:prd-vipy": [ + "{{ headscale_namespace }}@" + ], + "tag:small-backups-box": [ + "{{ headscale_namespace }}@" + ], + "tag:stalwart-box": [ + "{{ headscale_namespace }}@" + ] + } +}