From e4310f52e67c1cf862957ac7c471bc64789f68c2 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Tue, 18 Aug 2026 16:18:13 -0700 Subject: [PATCH] feat(tailscale): manage tailnet policy with gitops Track the tailnet policy file in git and sync it with a GitHub Action: validate on pull requests, apply on merge to master. Authenticates with a Tailscale workload identity federation credential, so no long-lived secret is stored in the repository. Drops tag:dntls, which is no longer used. --- .github/workflows/tailscale-acl.yml | 55 +++++++++++++++++++++++++++++ .gitignore | 3 ++ tailscale/policy.hujson | 55 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 .github/workflows/tailscale-acl.yml create mode 100644 tailscale/policy.hujson diff --git a/.github/workflows/tailscale-acl.yml b/.github/workflows/tailscale-acl.yml new file mode 100644 index 0000000..fcb43a8 --- /dev/null +++ b/.github/workflows/tailscale-acl.yml @@ -0,0 +1,55 @@ +name: Tailscale ACL + +on: + pull_request: + branches: + - master + paths: + - 'tailscale/**' + - '.github/workflows/tailscale-acl.yml' + push: + branches: + - master + paths: + - 'tailscale/**' + - '.github/workflows/tailscale-acl.yml' + workflow_dispatch: + +permissions: {} + +concurrency: + group: tailscale-acl + cancel-in-progress: false + +jobs: + sync: + name: Sync tailnet policy + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Validate policy + if: ${{ github.event_name == 'pull_request' }} + uses: tailscale/gitops-acl-action@5a4a17f5708e9bf96f4ee915a95e9f83c2eebe1a # v1.5.2 + with: + oauth-client-id: ${{ vars.TS_POLICY_CLIENT_ID }} + audience: ${{ vars.TS_POLICY_AUDIENCE }} + tailnet: ${{ vars.TS_TAILNET }} + policy-file: tailscale/policy.hujson + action: test + + - name: Apply policy + if: ${{ github.event_name != 'pull_request' }} + uses: tailscale/gitops-acl-action@5a4a17f5708e9bf96f4ee915a95e9f83c2eebe1a # v1.5.2 + with: + oauth-client-id: ${{ vars.TS_POLICY_CLIENT_ID }} + audience: ${{ vars.TS_POLICY_AUDIENCE }} + tailnet: ${{ vars.TS_TAILNET }} + policy-file: tailscale/policy.hujson + action: apply diff --git a/.gitignore b/.gitignore index 13d7c6d..bb0a7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ __pycache__/ *.py[cod] .venv/ +# Tailscale gitops-pusher etag cache +version-cache.json + # OS and editors .DS_Store .idea/ diff --git a/tailscale/policy.hujson b/tailscale/policy.hujson new file mode 100644 index 0000000..23e57f4 --- /dev/null +++ b/tailscale/policy.hujson @@ -0,0 +1,55 @@ +// GilmanLab tailnet policy file. +// +// This file is the source of truth for the tailnet's access control policy. +// Change it through a pull request against GilmanLab/networking: CI validates +// the policy on pull requests and applies it to the tailnet on merge to master. +// Edits made directly in the Tailscale admin console are overwritten by the +// next apply. +// +// Reference: +// https://github.com/GilmanLab/root/blob/master/docs/docs/reference/networking/tailscale-policy.md +{ + // Who may assign each tag to a device. + "tagOwners": { + "tag:subnet-router": ["autogroup:admin"], + }, + + // Routes a subnet router may advertise without manual approval. These are + // the lab and home ranges; keep them aligned with the address plan. + "autoApprovers": { + "routes": { + "10.10.0.0/16": ["tag:subnet-router"], + "172.16.0.0/16": ["tag:subnet-router"], + "192.168.1.0/24": ["tag:subnet-router"], + "192.168.2.0/24": ["tag:subnet-router"], + }, + }, + + "acls": [ + // Admins reach everything in the tailnet. + {"action": "accept", "src": ["autogroup:admin"], "dst": ["*:*"]}, + + // Subnet routers reach each other so routed traffic can transit + // between them. + { + "action": "accept", + "src": ["tag:subnet-router"], + "dst": ["tag:subnet-router:*"], + }, + + // Routed hosts in the lab and home ranges reach each other in both + // directions. + {"action": "accept", "src": ["10.10.0.0/16"], "dst": ["172.16.0.0/16:*"]}, + {"action": "accept", "src": ["172.16.0.0/16"], "dst": ["10.10.0.0/16:*"]}, + ], + + "ssh": [ + // Tailscale SSH to your own devices, with periodic reauthentication. + { + "action": "check", + "src": ["autogroup:member"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot", "root"], + }, + ], +}