Manage Tailscale Policy with GitHub Actions
A Tailscale policy is security-sensitive configuration. Once the tailnet matters, editing that policy only in a browser leaves too much intent in memory and too little in reviewable history.
GitOps gives the policy:
- a durable source file,
- an exact diff,
- pull-request review,
- automated policy validation and tests,
- a rollback commit,
- an audit trail.
Tailscale supports a GitHub Actions workflow in which pull requests run the
policy action in test mode, while pushes to the default branch run it in
apply mode.[9]
That boundary is the most important fact in this guide:
Pull request → validate and test onlyMerge to main → validate, test, and apply to the live tailnetRead Design a Tailscale Access Policy before building automation around a policy you do not yet understand.
Safety boundary
Section titled “Safety boundary”Merging this repository is a live network-security change.
A draft branch or pull request should not mutate the tailnet. The merge to the apply branch should happen only after the exact diff, test results, blast radius, and rollback are understood.
Use a private repository. Tailscale warns that policy files can contain personally identifiable information such as users’ email addresses.[9]
What you will build
Section titled “What you will build”private GitHub repository├── policy.hujson└── .github/ └── workflows/ └── tailscale.ymlThe workflow will:
- check out the repository,
- authenticate to Tailscale without placing credentials in Git,
- test pull requests,
- apply only pushes to
mainafter tests pass.[9]
Prerequisites
Section titled “Prerequisites”You need:
- an existing tailnet,
- owner or appropriate network administration authority,
- a reviewed current policy,
- a private GitHub repository,
- permission to configure GitHub Actions secrets or trust credentials,
- a recovery path if the applied policy blocks important access.
Before importing the policy, decide which source is authoritative. Once GitHub is the source of truth, emergency console edits can be overwritten by the next successful apply.[9]
1. Create the private repository
Section titled “1. Create the private repository”Create a private repository dedicated to the Tailscale policy. Keep the repository small. It needs policy, workflow, and concise operating documentation —not a framework in search of employment.
A minimal .gitignore is enough:
.env*.localNever store OAuth secrets, API keys, auth keys, or downloaded credentials in the repository.
2. Import the current live policy
Section titled “2. Import the current live policy”In the Tailscale admin console, open Access controls and copy the current policy into:
policy.hujsonTailscale policy files use HuJSON, which permits comments and trailing commas.[9]
Do not “clean up” the policy during the import. The first commit should preserve the known live behavior so later diffs have a trustworthy baseline.
Example skeleton:
{ "groups": {}, "tagOwners": {}, "grants": [], "tests": []}Your actual file should come from the live policy, not from this empty example.
3. Add policy tests before automation
Section titled “3. Add policy tests before automation”Tests should cover critical access and critical denials:
{ "tests": [ { "accept": ["tag:server:22"], "deny": ["tag:database:5432"] } ]}Use the current policy syntax reference for the exact forms your policy requires.[8]
At minimum, protect:
- the administration path you need for recovery,
- an ordinary user path,
- an automation or agent path,
- a sensitive path that must remain denied.
4. Create a Tailscale trust credential
Section titled “4. Create a Tailscale trust credential”Prefer a Tailscale trust credential or OAuth-based workflow over a manually rotated user API token. Give it only the policy permissions required by the GitOps action.
Record only the non-secret configuration names in your repository. Put secret values in GitHub’s encrypted Actions secrets or the supported trust-credential configuration.[9]
Common secret names in Tailscale’s GitHub Actions documentation include:
TS_TAILNETTS_OAUTH_IDTS_AUDIENCEDepending on the authentication method you choose, the current action may use an OAuth audience, OAuth secret, or API key. Follow the current Tailscale action documentation rather than copying an old credential shape from a blog post.[9]
5. Add the GitHub Actions workflow
Section titled “5. Add the GitHub Actions workflow”Create .github/workflows/tailscale.yml:
name: Sync Tailscale policy
on: pull_request: branches: [main] push: branches: [main]
permissions: contents: read
jobs: policy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6
- name: Test policy if: github.event_name == 'pull_request' uses: tailscale/gitops-acl-action@v1 with: oauth-client-id: ${{ secrets.TS_OAUTH_ID }} audience: ${{ secrets.TS_AUDIENCE }} tailnet: ${{ secrets.TS_TAILNET }} policy-file: policy.hujson action: test
- name: Apply policy if: github.event_name == 'push' uses: tailscale/gitops-acl-action@v1 with: oauth-client-id: ${{ secrets.TS_OAUTH_ID }} audience: ${{ secrets.TS_AUDIENCE }} tailnet: ${{ secrets.TS_TAILNET }} policy-file: policy.hujson action: applyTailscale’s documented action behavior is:
action: testvalidates the policy and runs its tests without updating the tailnet;action: applyvalidates, tests, and then updates the live policy when the checks succeed.[9]
Review the current upstream action before first use. Pinning a reviewed full commit SHA provides stronger supply-chain control than a floating major-version tag. If you pin it, use dependency automation to propose future updates.
6. Protect the main branch
Section titled “6. Protect the main branch”Configure branch protection so main requires:
- a pull request,
- the Tailscale policy test job,
- at least one appropriate reviewer,
- no unresolved conversations,
- a current branch before merge.
For a one-person homelab, “reviewer” may still be you after a pause and a fresh read. The useful control is separating authoring from applying, not collecting ceremonial approvals.
If agents can open pull requests, do not let their identity bypass branch protection or merge policy changes automatically.
7. Make the first pull request
Section titled “7. Make the first pull request”Use a no-behavior-change edit first, such as adding a harmless comment, only if the action requires a test event after the baseline workflow exists.
Verify:
- the pull request job uses
action: test, - the apply step is skipped,
- policy validation succeeds,
- all built-in tests pass,
- no live policy change occurred.
Then close or merge according to the change’s intent. Remember that merging invokes the live apply path.
8. Change policy safely
Section titled “8. Change policy safely”For each real policy change:
- Pull the current
mainbranch. - Confirm Git and the live policy have not drifted.
- Create a focused branch.
- Change the smallest useful policy section.
- Add or update positive and negative tests.
- Open a pull request.
- Review the exact HuJSON diff.
- Require the test workflow to pass.
- Record expected impact and rollback.
- Merge only with live-apply authority.
- Verify the apply job for the merge commit.
- Read back the live policy and test the real connection.
A green pull-request test proves the candidate is valid according to its tests. It does not prove the intended user can log in to the application, that DNS resolves, or that the backend is healthy.
Agent-assisted policy changes
Section titled “Agent-assisted policy changes”An agent can help:
- explain a policy block,
- draft a narrow grant,
- add tests,
- compare a source policy with a retrieved live policy,
- open a pull request for review,
- summarize CI failures.
An agent should not receive automatic merge authority merely because it can produce valid HuJSON. Tailscale’s official coding-agent skill is alpha and says to verify generated commands, syntax, and configuration before production use.[4]
A good division of labor is:
Agent: inspect, draft, test, explainCI: validate exact candidateHuman or guarded control plane: approve merge/applyRuntime check: prove intended allow and deny behaviorSee SEL and Cynefin Framework for Agent Autonomy for a general way to place approval boundaries around agent actions.
DockTail policy changes
Section titled “DockTail policy changes”DockTail may require:
tagOwnersentries for the host and service tags,autoApprovers.servicesfor approved service hosts,- grants that let selected users or workloads reach the Tailscale Service.[10][11]
Keep the runtime and policy changes reviewable as separate concerns:
- prepare and validate the policy change,
- apply and verify it,
- then activate or reconcile DockTail labels,
- verify the service from an allowed client,
- verify denial from a disallowed identity where practical.
Read DockTail for the Compose side.
Prevent accidental console drift
Section titled “Prevent accidental console drift”Tailscale provides a policy file management setting that warns or prevents normal editing through the admin console when GitOps owns the policy.[9]
Do not enable that setting until:
- the GitHub workflow has been tested,
- the repository is recoverable,
- the workflow credential works,
- the apply path is understood,
- another administrator knows the break-glass process.
Authorized administrators may still use Edit anyway for emergencies. The next GitOps apply can overwrite that emergency edit, so reconcile the change back into Git immediately.[9]
Rollback
Section titled “Rollback”Before merge
Section titled “Before merge”Close the pull request or delete the branch. The test workflow should not have changed the live tailnet.
After merge
Section titled “After merge”Revert the merge commit through a reviewed pull request. The revert merge will run the apply workflow and restore the previous policy if validation and tests pass.[9]
If the bad policy blocks the normal workflow or recovery path:
- use an authorized break-glass administrator,
- restore a known-good policy through the Tailscale admin control plane,
- repair or revert Git immediately,
- rerun the GitOps workflow,
- verify source and live policy match again.
Do not leave an emergency console edit as undocumented permanent drift.
Verification checklist
Section titled “Verification checklist”After every approved merge:
- The merge commit is the reviewed commit.
- The pull-request test job passed.
- The push apply job passed for the merge commit.
- The live policy matches repository
mainsemantically. - The intended connection succeeds from the intended identity.
- A protected connection remains denied.
- DockTail or another dependent service advertises correctly, if applicable.
- No secret values appeared in logs, commits, or screenshots.
- Rollback instructions still work.
Continue the series
Section titled “Continue the series”- Tailscale for People and AI Agents
- Design a Tailscale Access Policy
- DockTail: Private Docker Services over Tailscale
- APIs, MCP, and CLIs
- Architectural Decisions: Writing Down the Why