feat(headscale): ephemeral CI runners join as tag:ci (Forgejo and cache only) #274

Merged
jfraeys merged 1 commit from jfraeys/feat/tag-ci-acl into main 2026-10-17 21:32:26 -04:00
Owner

Follow-up of #251. CI VMs joined with the `tag:web` key, so a job (arbitrary repo code) on a runner inherited the public web host's tailnet reach: SSH to services and web, the LLDAP admin port, Postfix.

  • ACL: new owner `tag:ci` (infra user) and one rule: `tag:ci` to `tag:services:3000` (Forgejo API), `:2222` (git-SSH), `:8089` (cache). Nothing else.
  • `deploy-services.sh` mints `CI_EPHEMERAL_TAILSCALE_AUTHKEY` with `--tags tag:ci`; `playbooks/ci-ephemeral.yml` sets `tailscale_advertise_tags: tag:ci` (documentation-only var, matches the key); provisioner hint text updated.
  • `tag:web`'s own rules are untouched: narrowing web is a separate step (web's real needs not verified).
  • Tests (`test_headscale_acl.py`, in lint): tag:ci has exactly one rule with those three ports, is owned by infra, and is in no admin/SSH/Postfix rule.

Checked

coaching-site's refresh_demo job SSHes to $DEMO_REFRESH_SSH_HOST. The operator confirmed it is a public address, so tag:ci does not affect it. The other workflows in infra, coaching-site and forge-agent use no tailnet SSH.

Rollout (two deploys, in this order)

  1. merge, staging-validate green, `./infra approve`, `git pull`.
  2. `./infra deploy --tags headscale --limit services` (defines the tag; harmless alone). Verify reach from your Mac as in #251 (nc to 100.64.0.3 on 17170, 3000, 2222, 8089).
  3. `scripts/deploy-services.sh --tags ci_ephemeral_provisioner,ci_cache_host --limit services` (mints the tag:ci key; Headscale refuses to mint it if step 2 has not run).
  4. After the next CI job: `./manage vpn status` shows the runner with tag:ci; coaching_site, forge-agent and infra jobs still pass.

Rollback: revert and redeploy both steps (a job fails to reach Forgejo only if the ACL rule is missing; the old key keeps working until its 168h expiry).

The ACL cannot be tested in CI (staging nodes join the live Headscale); the only automated check is the rendered-policy unit test plus `headscale policy check` at deploy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ

Follow-up of #251. CI VMs joined with the \`tag:web\` key, so a job (arbitrary repo code) on a runner inherited the public web host's tailnet reach: SSH to services and web, the LLDAP admin port, Postfix. - ACL: new owner \`tag:ci\` (infra user) and one rule: \`tag:ci\` to \`tag:services:3000\` (Forgejo API), \`:2222\` (git-SSH), \`:8089\` (cache). Nothing else. - \`deploy-services.sh\` mints \`CI_EPHEMERAL_TAILSCALE_AUTHKEY\` with \`--tags tag:ci\`; \`playbooks/ci-ephemeral.yml\` sets \`tailscale_advertise_tags: tag:ci\` (documentation-only var, matches the key); provisioner hint text updated. - \`tag:web\`'s own rules are untouched: narrowing web is a separate step (web's real needs not verified). - Tests (\`test_headscale_acl.py\`, in lint): tag:ci has exactly one rule with those three ports, is owned by infra, and is in no admin/SSH/Postfix rule. ## Checked `coaching-site`'s `refresh_demo` job SSHes to `$DEMO_REFRESH_SSH_HOST`. The operator confirmed it is a public address, so `tag:ci` does not affect it. The other workflows in infra, coaching-site and forge-agent use no tailnet SSH. ## Rollout (two deploys, in this order) 1. merge, staging-validate green, \`./infra approve\`, \`git pull\`. 2. \`./infra deploy --tags headscale --limit services\` (defines the tag; harmless alone). Verify reach from your Mac as in #251 (nc to 100.64.0.3 on 17170, 3000, 2222, 8089). 3. \`scripts/deploy-services.sh --tags ci_ephemeral_provisioner,ci_cache_host --limit services\` (mints the tag:ci key; Headscale refuses to mint it if step 2 has not run). 4. After the next CI job: \`./manage vpn status\` shows the runner with tag:ci; coaching_site, forge-agent and infra jobs still pass. Rollback: revert and redeploy both steps (a job fails to reach Forgejo only if the ACL rule is missing; the old key keeps working until its 168h expiry). The ACL cannot be tested in CI (staging nodes join the live Headscale); the only automated check is the rendered-policy unit test plus \`headscale policy check\` at deploy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
feat(headscale): ephemeral CI runners join as tag:ci (Forgejo and cache only)
All checks were successful
Lint / lint (pull_request) Successful in 3m4s
Secret scan / scan (pull_request) Successful in 16s
7ef67a9416
CI VMs were tag:web and inherited the public web host's reach (SSH to services, LLDAP admin, Postfix).
tag:ci gets one rule: services:3000, :2222, :8089. The provisioner key is minted with tag:ci.
Rollout is two deploys, in order: --tags headscale (defines the tag), then the provisioner (mints the key).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!274
No description provided.