personal_infra/ansible/services/headscale/templates/acl.json.j2
counterweight 5447e03d1f
arbret: our side of running it on vipy
prd-arbret was folded into vipy. arbret itself (app, worker, Postgres, timers,
its backup dump) is deployed by its owner's own repo; this repo owns the
shared host's side of it. arbret_handoff.md documents the split, the Gatus
push API and the backup contract.

- services/arbret: arbret.com -> localhost:3100, www redirects to the apex,
  port 4117 allowed on tailscale0 only, /opt/backups/arbret pre-created to
  the contract so the pull job is green before the first dump.
- headscale ACL: framie may reach prd-vipy:4117.
- vipy: arbret-server, arbret-worker and postgresql in the unit checks.
- 402: DNS and HTTP checks for arbret.com, www, and /api/health with
  [BODY].status == ok. Entries may now name an fqdn outside root_domain and
  add conditions.
- 403: probes arbret-analytics (hourly, 130m window) and arbret-beancount
  (daily, 26h), pushing with the dedicated arbret token.
- backups: pull from backup-pull@prd-vipy:/opt/backups/arbret/, and a
  backup-dump_arbret endpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 22:10:26 +02:00

128 lines
3.2 KiB
Django/Jinja

{#
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-vipy: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:music-box:4533",
"tag:jar-lid-archive-staging-box:8000",
"tag:monerod-box:18089",
"tag:monerod-box:8081",
"tag:arbret-staging-box:80"
],
"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-vipy:22",
"tag:memos-box:22",
"tag:jar-lid-archive-staging-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:jar-lid-archive-staging-box": [
"{{ headscale_namespace }}@"
],
"tag:knots-box": [
"{{ headscale_namespace }}@"
],
"tag:memos-box": [
"{{ headscale_namespace }}@"
],
"tag:mempool-box": [
"{{ headscale_namespace }}@"
],
"tag:monerod-box": [
"{{ headscale_namespace }}@"
],
"tag:music-box": [
"{{ headscale_namespace }}@"
],
"tag:nodito": [
"{{ headscale_namespace }}@"
],
"tag:prd-vipy": [
"{{ headscale_namespace }}@"
],
"tag:small-backups-box": [
"{{ headscale_namespace }}@"
]
}
}