{"slug":"gitops-for-tailscale-with-github-actions","title":"GitOps for Tailscale with GitHub Actions","tags":["tailscale"],"agent_summary":"Last validated: Dec 5, 2025","trigger_phrases":[],"runnable":false,"markdown":"\r\n# GitOps for Tailscale with GitHub Actions\r\n\r\nLast validated: Dec 5, 2025\r\n\r\n[Access control policies](https://tailscale.com/docs/features/access-control) define what users or devices are permitted to access in your Tailscale network (known as a tailnet). An alternative to managing the access control policy changes in the [Access controls](https://login.tailscale.com/admin/acls) page of the admin console is to use [GitOps](https://tailscale.com/docs/gitops) to manage the access control policy changes. This topic provides details on how to use [GitHub Actions](https://github.com/features/actions) to automatically apply and test access control policy changes to your tailnet.\r\n\r\nGitOps for Tailscaleis available for [all plans](https://tailscale.com/pricing).\r\n\r\n## [Prerequisites](https://tailscale.com/docs/integrations/github/gitops\\#prerequisites)\r\n\r\nIn addition to already having your own Tailscale network, you need:\r\n\r\n- A GitHub account.\r\n- Working knowledge of GitHub, including committing changes, creating and merging pull requests.\r\n- A private GitHub repository that will contain your tailnet policy file.\r\n\r\nMake sure this repository is private, as tailnet policy files contain personally identifiable information (PII), such as your users' email addresses.\r\n\r\n- A Tailscale [API access token](https://tailscale.com/docs/reference/tailscale-api) or [OAuth client](https://tailscale.com/docs/features/oauth-clients) for your tailnet. You can create an API access token in the [Keys](https://login.tailscale.com/admin/settings/keys) page of the admin console, or you can create an OAuth client in the [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials) page of the admin console. If you use an OAuth client, the client needs the `policy_file:read` scope to be able to read and validate the tailnet policy file, or the `policy_file` scope to be able to read, validate, and modify the tailnet policy file. For information about other scopes that are required when using the `policy_file:read` or the `policy_file` scopes, refer to the [trust credentials scopes](https://tailscale.com/docs/reference/trust-credentials#scopes) documentation.\r\n\r\n## [GitHub Action to sync Tailscale access control policies](https://tailscale.com/docs/integrations/github/gitops\\#github-action-to-sync-tailscale-access-control-policies)\r\n\r\nTailscale publishes a GitHub Action to [Sync Tailscale access control policies](https://github.com/marketplace/actions/sync-tailscale-acls), available in the GitHub Actions Marketplace.\r\n\r\nYou do not need to use this GitHub Action to set up a GitOps pattern — you can also write your own Action or use another CI/CD tool.\r\n\r\n### [What this action does](https://tailscale.com/docs/integrations/github/gitops\\#what-this-action-does)\r\n\r\nOn **pull requests** that target the `main` branch, the `Test ACL` step (action `test`) will send your tailnet policy file to Tailscale to determine whether the access control policy is valid and whether all [tests](https://tailscale.com/docs/reference/syntax/policy-file#tests) pass. Tailscale will provide your workflow with the result of those checks.\r\n\r\nOn **push** (merge) operations that target the `main` branch, the `Deploy ACL` step (action `apply`) will again check access control policy validity and run tests, with Tailscale reporting back the results to your workflow. If the tests succeed, the merge will complete and the workflow will automatically apply your access control policy changes to your tailnet. If the tests fail, an error will prevent the merge from continuing.\r\n\r\nYou can refer to the status of the workflow in the GitHub user interface's **Actions** tab for your pull request. Look for a workflow with the name you used in `.github/workflows/tailscale.yml`, **Tailscale ACL syncing**.\r\n\r\n![The GitHub Actions workflow interface. Listing a 'Sync Tailscale ACLs' workflow.](https://tailscale.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Factions-workflow.8493ea93.png&w=1080&q=75)\r\n\r\n### [Inputs](https://tailscale.com/docs/integrations/github/gitops\\#inputs)\r\n\r\nThe following inputs apply to the Sync Tailscale ACLs action.\r\n\r\n- `action` (Required): One of `test` or `apply`. If `test`, the action will run [ACL tests](https://tailscale.com/docs/reference/syntax/policy-file#tests) only—it won't update your ACLs in Tailscale. If `apply`, the action will run ACL tests and, if the ACL tests succeed, then update your ACLs in Tailscale. This enables you to use pull requests to make changes, with CI preventing you from pushing a bad change out to production.\r\n\r\n- `api-key` or `oauth-client-id`/`oauth-secret` (Required): A credential to allow the action to interact with your tailnet. You may use an `api-key` or an `oauth-client-id` and `oauth-secret`. You can create an API access token in the [Keys](https://login.tailscale.com/admin/settings/keys)\r\npage of the admin console, or you can create an OAuth client in the [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials) page of the admin console.\r\n\r\n- `policy-file` (Optional): The path to your tailnet policy file in the repository. If not set this defaults to `policy.hujson` in the root of your repository.\r\n\r\n- `tailnet` (Required): This is your organization, which you can find in the [General](https://login.tailscale.com/admin/settings/general) settings page of the admin console.\r\n\r\n\r\n## [Set up your GitOps configuration](https://tailscale.com/docs/integrations/github/gitops\\#set-up-your-gitops-configuration)\r\n\r\n1. In your GitHub repository, create a file named `policy.hujson`. Copy your tailnet policy file contents from the [Access controls](https://login.tailscale.com/admin/acls) page of the admin console into this file.\r\n\r\n\r\n\r\nIf you want to change the tailnet policy file name to something else, you will need to add the `policy-file` argument to the `with` blocks in your GitHub Actions configuration discussed below. Otherwise, the GitHub action will default to `policy.hujson` as the tailnet policy file.\r\n\r\n2. Commit the tailnet policy file and push it to GitHub. For example, if you are using the command line:\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n```shell\r\ngit add .\r\ngit commit -sm \"policy: import from admin console\"\r\ngit push -u origin main\r\n```\r\n\r\n3. Create the following [GitHub Actions secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in your repository's settings:\r\n\r\n\r\n\r\nYou may either supply Tailscale OAuth client credentials or a Tailscale API key. If you supply both, the OAuth credentials will be used.\r\n\r\n\r\n\r\n\r\n   - `TS_TAILNET`: Use your tailnet ID as the value. You can find your tailnet ID in the [General](https://login.tailscale.com/admin/settings/general) page of the admin console.\r\n   - `TS_OAUTH_ID`: Use your Tailscale OAuth client ID as the value.\r\n   - `TS_OAUTH_SECRET`: Use your Tailscale OAuth client secret as the value.\r\n   - `TS_API_KEY`: Use your Tailscale API access token as the value.\r\n\r\nThese secrets provide the configuration for the action.\r\n\r\n4. Make a new GitHub Actions workflow that uses the [Sync Tailscale ACLs](https://github.com/marketplace/actions/sync-tailscale-acls) GitHub Action.\r\n\r\n\r\n\r\n[API key](https://tailscale.com/docs/integrations/github/gitops?tab=api+key) [OAuth client](https://tailscale.com/docs/integrations/github/gitops?tab=oauth+client)\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nCreate a file named `.github/workflows/tailscale.yml`. Paste in the following YAML:\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n```yaml\r\nname: Sync Tailscale ACLs\r\n\r\non:\r\n     push:\r\n       branches: [ \"main\" ]\r\n     pull_request:\r\n       branches: [ \"main\" ]\r\n\r\njobs:\r\n     acls:\r\n       runs-on: ubuntu-latest\r\n\r\n       steps:\r\n      - uses: actions/checkout@v3\r\n\r\n      - name: Deploy ACL\r\n        if: github.event_name == 'push'\r\n        id: deploy-acl\r\n        uses: tailscale/gitops-acl-action@v1\r\n        with:\r\n          api-key: ${{ secrets.TS_API_KEY }}\r\n          tailnet: ${{ secrets.TS_TAILNET }}\r\n          action: apply\r\n\r\n      - name: Test ACL\r\n        if: github.event_name == 'pull_request'\r\n        id: test-acl\r\n        uses: tailscale/gitops-acl-action@v1\r\n        with:\r\n          api-key: ${{ secrets.TS_API_KEY }}\r\n          tailnet: ${{ secrets.TS_TAILNET }}\r\n          action: test\r\n```\r\n\r\nIf you don't use the name `main` for your default branch, update the `branches` fields in the YAML.\r\n\r\nThis workflow appropriately uses the `secrets.TS_API_KEY` and `secrets.TS_TAILNET` environment variables to access your secrets.\r\n\r\nDo not place your actual secret _values_ in `.github/workflows/tailscale.yml`—use the environment values as depicted in the example.\r\n\r\nFor an example repository that has set up this workflow, see [`https://github.com/cetacean/tailscale-acls`](https://github.com/cetacean/tailscale-acls).\r\n\r\n5. Commit and push `.github/workflows/tailscale.yml`.\r\n\r\nThe main steps in the action are installing [gitops-pusher](https://github.com/tailscale/tailscale/tree/main/cmd/gitops-pusher), setting the correct environment variables and flags, and then running the workflow. For more information, refer to [GitHub Action configuration](https://github.com/tailscale/gitops-acl-action/blob/main/action.yml).\r\n\r\nWith this setup, you have created a continuous integration (CI) that automatically tests and pushes your tailnet policy file changes to Tailscale!\r\n\r\n6. Follow the instructions to [prevent others from accidentally modifying your tailnet policy file](https://tailscale.com/docs/integrations/github/gitops#prevent-others-from-accidentally-modifying-your-tailnet-policy-file) in the admin console.\r\n\r\n\r\n## [Push changes to your tailnet policy file](https://tailscale.com/docs/integrations/github/gitops\\#push-changes-to-your-tailnet-policy-file)\r\n\r\nNow that your configuration has been set up, any time your want to update your ACL, modify the tailnet policy file in your repository and use the typical GitHub authoring/review/merge flow. You can check the status of the ACL validity checks and ACL tests in the GitHub UI in the **Actions** tab for your pull request. Look for a workflow with the name you used in `.github/workflows/tailscale.yml`, **Sync Tailscale ACLs**.\r\n\r\n## [Prevent others from accidentally modifying your tailnet policy file](https://tailscale.com/docs/integrations/github/gitops\\#prevent-others-from-accidentally-modifying-your-tailnet-policy-file)\r\n\r\nOn June 3, 2025, we [announced the deprecation](https://tailscale.com/changelog#2025-06-03) of the GitOps code comment technique for locking the policy file editor in the admin console. Tailnets that were using this method were automatically migrated to use the setting in the admin console. The code comment is no longer effective for locking the policy file editor.\r\n\r\nTo prevent other admins in your organization from accidentally changing your tailnet policy file, you can lock the editor in the [Policy file management](https://login.tailscale.com/admin/settings/policy-file-management) page in the Tailscale admin console.\r\n\r\nYou must be an [Owner, Admin, or Network admin](https://tailscale.com/docs/reference/user-roles) of a tailnet to update tailnet policy file settings.\r\n\r\n1. Open the [Policy file management](https://login.tailscale.com/admin/settings/policy-file-management) page in the Tailscale admin console.\r\n2. If it is not already enabled, select **Prevent edits in the admin console** to prevent editing of the file.\r\n3. For **External reference**, enter the URL of your repository. This lets other admins to know where the source of your policies is located.\r\n4. Select **Save**.\r\n\r\nAny [admin with permissions to edit the tailnet policy file](https://tailscale.com/docs/reference/user-roles#permissions-managed-by-user-roles) can still edit it directly by selecting **Edit anyway**. This lets admins to quickly make changes in case of an emergency.\r\n\r\n![The warning given when trying to edit a tailnet policy file which is using GitOps.](https://tailscale.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fgitops-edit-warning.9df07cf9.png&w=1080&q=75)\r\n\r\nThe next time you use the GitOps flow, it overwrites the tailnet policy file changes made in the admin console.\r\n\r\n## [Revert the most recent change to your ACL](https://tailscale.com/docs/integrations/github/gitops\\#revert-the-most-recent-change-to-your-acl)\r\n\r\nIf you need to revert the most recent change, use the GitHub UI to [revert the merged pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/reverting-a-pull-request).\r\n\r\n## [Remove your GitOps configuration](https://tailscale.com/docs/integrations/github/gitops\\#remove-your-gitops-configuration)\r\n\r\n1. Open the [Policy file management](https://login.tailscale.com/admin/settings/policy-file-management) page in the Tailscale admin console and disable the **Prevent edits in the admin console** toggle.\r\n\r\n2. Optionally, delete the **External reference link** and select **Save**.\r\n\r\n3. [Revoke](https://tailscale.com/docs/features/access-control/auth-keys#revoke-an-auth-key) any associated Tailscale API access tokens for this configuration.\r\n\r\n4. Archive or delete the associated GitHub repository.\r\n\r\n\r\n## [Additional information](https://tailscale.com/docs/integrations/github/gitops\\#additional-information)\r\n\r\n- Any manual tailnet policy file changes in the admin console won't be reflected in your GitHub version of the tailnet policy file. The next time you use the Sync Tailscale ACLs GitHub Action, any changes made in the Tailscale admin console will be overwritten.\r\n\r\n- Tailscale [API access tokens expire](https://tailscale.com/docs/reference/tailscale-api#authentication) and currently there is no mechanism to have them automatically renewed. To handle the expiration, create a new API access token and update the GitHub `TS_API_KEY` secret to use the new value.\r\n\r\n\r\n\r\nTailscale API access tokens expire after 90 days (or less if the key has a shorter expiry) but updating the GitHub secret monthly is a good practice.\r\n\r\n\r\n\r\nWhen you no longer need to use a Tailscale API access token, make sure you revoke it in the [Keys](https://login.tailscale.com/admin/settings/keys) page\r\nof the admin console.\r\n\r\n- Tailscale tailnet policy files are in [HuJSON](https://nigeltao.github.io/blog/2021/json-with-commas-comments.html), a JSON format with trailing commas and comments. If you don't want to write your tailnet policy files in HuJSON directly, you can use a tool that lets you generate JSON in the same schema as the HuJSON format. Make sure your tool puts the file in the same place as the `policy-file` setting in the GitHub Action.\r\n\r\n\r\n![Project Logo](https://cdn.brandfetch.io/tailscale.com/fallback/lettermark/theme/dark/h/256/w/256/icon?c=1bfwsmEH20zzEfSNTed)\r\n\r\nAsk AI\r\n\r\nreCAPTCHA\r\n\r\nRecaptcha requires verification.\r\n\r\nprotected by **reCAPTCHA**\r\n","html":"<h1>GitOps for Tailscale with GitHub Actions</h1>\n<p>Last validated: Dec 5, 2025</p>\n<p><a href=\"https://tailscale.com/docs/features/access-control\">Access control policies</a> define what users or devices are permitted to access in your Tailscale network (known as a tailnet). An alternative to managing the access control policy changes in the <a href=\"https://login.tailscale.com/admin/acls\">Access controls</a> page of the admin console is to use <a href=\"https://tailscale.com/docs/gitops\">GitOps</a> to manage the access control policy changes. This topic provides details on how to use <a href=\"https://github.com/features/actions\">GitHub Actions</a> to automatically apply and test access control policy changes to your tailnet.</p>\n<p>GitOps for Tailscaleis available for <a href=\"https://tailscale.com/pricing\">all plans</a>.</p>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#prerequisites\">Prerequisites</a></h2>\n<p>In addition to already having your own Tailscale network, you need:</p>\n<ul>\n<li>A GitHub account.</li>\n<li>Working knowledge of GitHub, including committing changes, creating and merging pull requests.</li>\n<li>A private GitHub repository that will contain your tailnet policy file.</li>\n</ul>\n<p>Make sure this repository is private, as tailnet policy files contain personally identifiable information (PII), such as your users' email addresses.</p>\n<ul>\n<li>A Tailscale <a href=\"https://tailscale.com/docs/reference/tailscale-api\">API access token</a> or <a href=\"https://tailscale.com/docs/features/oauth-clients\">OAuth client</a> for your tailnet. You can create an API access token in the <a href=\"https://login.tailscale.com/admin/settings/keys\">Keys</a> page of the admin console, or you can create an OAuth client in the <a href=\"https://login.tailscale.com/admin/settings/trust-credentials\">Trust credentials</a> page of the admin console. If you use an OAuth client, the client needs the <code>policy_file:read</code> scope to be able to read and validate the tailnet policy file, or the <code>policy_file</code> scope to be able to read, validate, and modify the tailnet policy file. For information about other scopes that are required when using the <code>policy_file:read</code> or the <code>policy_file</code> scopes, refer to the <a href=\"https://tailscale.com/docs/reference/trust-credentials#scopes\">trust credentials scopes</a> documentation.</li>\n</ul>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#github-action-to-sync-tailscale-access-control-policies\">GitHub Action to sync Tailscale access control policies</a></h2>\n<p>Tailscale publishes a GitHub Action to <a href=\"https://github.com/marketplace/actions/sync-tailscale-acls\">Sync Tailscale access control policies</a>, available in the GitHub Actions Marketplace.</p>\n<p>You do not need to use this GitHub Action to set up a GitOps pattern — you can also write your own Action or use another CI/CD tool.</p>\n<h3><a href=\"https://tailscale.com/docs/integrations/github/gitops#what-this-action-does\">What this action does</a></h3>\n<p>On <strong>pull requests</strong> that target the <code>main</code> branch, the <code>Test ACL</code> step (action <code>test</code>) will send your tailnet policy file to Tailscale to determine whether the access control policy is valid and whether all <a href=\"https://tailscale.com/docs/reference/syntax/policy-file#tests\">tests</a> pass. Tailscale will provide your workflow with the result of those checks.</p>\n<p>On <strong>push</strong> (merge) operations that target the <code>main</code> branch, the <code>Deploy ACL</code> step (action <code>apply</code>) will again check access control policy validity and run tests, with Tailscale reporting back the results to your workflow. If the tests succeed, the merge will complete and the workflow will automatically apply your access control policy changes to your tailnet. If the tests fail, an error will prevent the merge from continuing.</p>\n<p>You can refer to the status of the workflow in the GitHub user interface's <strong>Actions</strong> tab for your pull request. Look for a workflow with the name you used in <code>.github/workflows/tailscale.yml</code>, <strong>Tailscale ACL syncing</strong>.</p>\n<p><img src=\"https://tailscale.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Factions-workflow.8493ea93.png&#x26;w=1080&#x26;q=75\" alt=\"The GitHub Actions workflow interface. Listing a &#x27;Sync Tailscale ACLs&#x27; workflow.\"></p>\n<h3><a href=\"https://tailscale.com/docs/integrations/github/gitops#inputs\">Inputs</a></h3>\n<p>The following inputs apply to the Sync Tailscale ACLs action.</p>\n<ul>\n<li>\n<p><code>action</code> (Required): One of <code>test</code> or <code>apply</code>. If <code>test</code>, the action will run <a href=\"https://tailscale.com/docs/reference/syntax/policy-file#tests\">ACL tests</a> only—it won't update your ACLs in Tailscale. If <code>apply</code>, the action will run ACL tests and, if the ACL tests succeed, then update your ACLs in Tailscale. This enables you to use pull requests to make changes, with CI preventing you from pushing a bad change out to production.</p>\n</li>\n<li>\n<p><code>api-key</code> or <code>oauth-client-id</code>/<code>oauth-secret</code> (Required): A credential to allow the action to interact with your tailnet. You may use an <code>api-key</code> or an <code>oauth-client-id</code> and <code>oauth-secret</code>. You can create an API access token in the <a href=\"https://login.tailscale.com/admin/settings/keys\">Keys</a>\r\npage of the admin console, or you can create an OAuth client in the <a href=\"https://login.tailscale.com/admin/settings/trust-credentials\">Trust credentials</a> page of the admin console.</p>\n</li>\n<li>\n<p><code>policy-file</code> (Optional): The path to your tailnet policy file in the repository. If not set this defaults to <code>policy.hujson</code> in the root of your repository.</p>\n</li>\n<li>\n<p><code>tailnet</code> (Required): This is your organization, which you can find in the <a href=\"https://login.tailscale.com/admin/settings/general\">General</a> settings page of the admin console.</p>\n</li>\n</ul>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#set-up-your-gitops-configuration\">Set up your GitOps configuration</a></h2>\n<ol>\n<li>In your GitHub repository, create a file named <code>policy.hujson</code>. Copy your tailnet policy file contents from the <a href=\"https://login.tailscale.com/admin/acls\">Access controls</a> page of the admin console into this file.</li>\n</ol>\n<p>If you want to change the tailnet policy file name to something else, you will need to add the <code>policy-file</code> argument to the <code>with</code> blocks in your GitHub Actions configuration discussed below. Otherwise, the GitHub action will default to <code>policy.hujson</code> as the tailnet policy file.</p>\n<ol start=\"2\">\n<li>Commit the tailnet policy file and push it to GitHub. For example, if you are using the command line:</li>\n</ol>\n<pre><code class=\"language-shell\">git add .\r\ngit commit -sm \"policy: import from admin console\"\r\ngit push -u origin main\n</code></pre>\n<ol start=\"3\">\n<li>Create the following <a href=\"https://docs.github.com/en/actions/security-guides/encrypted-secrets\">GitHub Actions secrets</a> in your repository's settings:</li>\n</ol>\n<p>You may either supply Tailscale OAuth client credentials or a Tailscale API key. If you supply both, the OAuth credentials will be used.</p>\n<ul>\n<li><code>TS_TAILNET</code>: Use your tailnet ID as the value. You can find your tailnet ID in the <a href=\"https://login.tailscale.com/admin/settings/general\">General</a> page of the admin console.</li>\n<li><code>TS_OAUTH_ID</code>: Use your Tailscale OAuth client ID as the value.</li>\n<li><code>TS_OAUTH_SECRET</code>: Use your Tailscale OAuth client secret as the value.</li>\n<li><code>TS_API_KEY</code>: Use your Tailscale API access token as the value.</li>\n</ul>\n<p>These secrets provide the configuration for the action.</p>\n<ol start=\"4\">\n<li>Make a new GitHub Actions workflow that uses the <a href=\"https://github.com/marketplace/actions/sync-tailscale-acls\">Sync Tailscale ACLs</a> GitHub Action.</li>\n</ol>\n<p><a href=\"https://tailscale.com/docs/integrations/github/gitops?tab=api+key\">API key</a> <a href=\"https://tailscale.com/docs/integrations/github/gitops?tab=oauth+client\">OAuth client</a></p>\n<p>Create a file named <code>.github/workflows/tailscale.yml</code>. Paste in the following YAML:</p>\n<pre><code class=\"language-yaml\">name: Sync Tailscale ACLs\r\n\r\non:\r\n     push:\r\n       branches: [ \"main\" ]\r\n     pull_request:\r\n       branches: [ \"main\" ]\r\n\r\njobs:\r\n     acls:\r\n       runs-on: ubuntu-latest\r\n\r\n       steps:\r\n      - uses: actions/checkout@v3\r\n\r\n      - name: Deploy ACL\r\n        if: github.event_name == 'push'\r\n        id: deploy-acl\r\n        uses: tailscale/gitops-acl-action@v1\r\n        with:\r\n          api-key: ${{ secrets.TS_API_KEY }}\r\n          tailnet: ${{ secrets.TS_TAILNET }}\r\n          action: apply\r\n\r\n      - name: Test ACL\r\n        if: github.event_name == 'pull_request'\r\n        id: test-acl\r\n        uses: tailscale/gitops-acl-action@v1\r\n        with:\r\n          api-key: ${{ secrets.TS_API_KEY }}\r\n          tailnet: ${{ secrets.TS_TAILNET }}\r\n          action: test\n</code></pre>\n<p>If you don't use the name <code>main</code> for your default branch, update the <code>branches</code> fields in the YAML.</p>\n<p>This workflow appropriately uses the <code>secrets.TS_API_KEY</code> and <code>secrets.TS_TAILNET</code> environment variables to access your secrets.</p>\n<p>Do not place your actual secret <em>values</em> in <code>.github/workflows/tailscale.yml</code>—use the environment values as depicted in the example.</p>\n<p>For an example repository that has set up this workflow, see <a href=\"https://github.com/cetacean/tailscale-acls\"><code>https://github.com/cetacean/tailscale-acls</code></a>.</p>\n<ol start=\"5\">\n<li>Commit and push <code>.github/workflows/tailscale.yml</code>.</li>\n</ol>\n<p>The main steps in the action are installing <a href=\"https://github.com/tailscale/tailscale/tree/main/cmd/gitops-pusher\">gitops-pusher</a>, setting the correct environment variables and flags, and then running the workflow. For more information, refer to <a href=\"https://github.com/tailscale/gitops-acl-action/blob/main/action.yml\">GitHub Action configuration</a>.</p>\n<p>With this setup, you have created a continuous integration (CI) that automatically tests and pushes your tailnet policy file changes to Tailscale!</p>\n<ol start=\"6\">\n<li>Follow the instructions to <a href=\"https://tailscale.com/docs/integrations/github/gitops#prevent-others-from-accidentally-modifying-your-tailnet-policy-file\">prevent others from accidentally modifying your tailnet policy file</a> in the admin console.</li>\n</ol>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#push-changes-to-your-tailnet-policy-file\">Push changes to your tailnet policy file</a></h2>\n<p>Now that your configuration has been set up, any time your want to update your ACL, modify the tailnet policy file in your repository and use the typical GitHub authoring/review/merge flow. You can check the status of the ACL validity checks and ACL tests in the GitHub UI in the <strong>Actions</strong> tab for your pull request. Look for a workflow with the name you used in <code>.github/workflows/tailscale.yml</code>, <strong>Sync Tailscale ACLs</strong>.</p>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#prevent-others-from-accidentally-modifying-your-tailnet-policy-file\">Prevent others from accidentally modifying your tailnet policy file</a></h2>\n<p>On June 3, 2025, we <a href=\"https://tailscale.com/changelog#2025-06-03\">announced the deprecation</a> of the GitOps code comment technique for locking the policy file editor in the admin console. Tailnets that were using this method were automatically migrated to use the setting in the admin console. The code comment is no longer effective for locking the policy file editor.</p>\n<p>To prevent other admins in your organization from accidentally changing your tailnet policy file, you can lock the editor in the <a href=\"https://login.tailscale.com/admin/settings/policy-file-management\">Policy file management</a> page in the Tailscale admin console.</p>\n<p>You must be an <a href=\"https://tailscale.com/docs/reference/user-roles\">Owner, Admin, or Network admin</a> of a tailnet to update tailnet policy file settings.</p>\n<ol>\n<li>Open the <a href=\"https://login.tailscale.com/admin/settings/policy-file-management\">Policy file management</a> page in the Tailscale admin console.</li>\n<li>If it is not already enabled, select <strong>Prevent edits in the admin console</strong> to prevent editing of the file.</li>\n<li>For <strong>External reference</strong>, enter the URL of your repository. This lets other admins to know where the source of your policies is located.</li>\n<li>Select <strong>Save</strong>.</li>\n</ol>\n<p>Any <a href=\"https://tailscale.com/docs/reference/user-roles#permissions-managed-by-user-roles\">admin with permissions to edit the tailnet policy file</a> can still edit it directly by selecting <strong>Edit anyway</strong>. This lets admins to quickly make changes in case of an emergency.</p>\n<p><img src=\"https://tailscale.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fgitops-edit-warning.9df07cf9.png&#x26;w=1080&#x26;q=75\" alt=\"The warning given when trying to edit a tailnet policy file which is using GitOps.\"></p>\n<p>The next time you use the GitOps flow, it overwrites the tailnet policy file changes made in the admin console.</p>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#revert-the-most-recent-change-to-your-acl\">Revert the most recent change to your ACL</a></h2>\n<p>If you need to revert the most recent change, use the GitHub UI to <a href=\"https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/reverting-a-pull-request\">revert the merged pull request</a>.</p>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#remove-your-gitops-configuration\">Remove your GitOps configuration</a></h2>\n<ol>\n<li>\n<p>Open the <a href=\"https://login.tailscale.com/admin/settings/policy-file-management\">Policy file management</a> page in the Tailscale admin console and disable the <strong>Prevent edits in the admin console</strong> toggle.</p>\n</li>\n<li>\n<p>Optionally, delete the <strong>External reference link</strong> and select <strong>Save</strong>.</p>\n</li>\n<li>\n<p><a href=\"https://tailscale.com/docs/features/access-control/auth-keys#revoke-an-auth-key\">Revoke</a> any associated Tailscale API access tokens for this configuration.</p>\n</li>\n<li>\n<p>Archive or delete the associated GitHub repository.</p>\n</li>\n</ol>\n<h2><a href=\"https://tailscale.com/docs/integrations/github/gitops#additional-information\">Additional information</a></h2>\n<ul>\n<li>\n<p>Any manual tailnet policy file changes in the admin console won't be reflected in your GitHub version of the tailnet policy file. The next time you use the Sync Tailscale ACLs GitHub Action, any changes made in the Tailscale admin console will be overwritten.</p>\n</li>\n<li>\n<p>Tailscale <a href=\"https://tailscale.com/docs/reference/tailscale-api#authentication\">API access tokens expire</a> and currently there is no mechanism to have them automatically renewed. To handle the expiration, create a new API access token and update the GitHub <code>TS_API_KEY</code> secret to use the new value.</p>\n</li>\n</ul>\n<p>Tailscale API access tokens expire after 90 days (or less if the key has a shorter expiry) but updating the GitHub secret monthly is a good practice.</p>\n<p>When you no longer need to use a Tailscale API access token, make sure you revoke it in the <a href=\"https://login.tailscale.com/admin/settings/keys\">Keys</a> page\r\nof the admin console.</p>\n<ul>\n<li>Tailscale tailnet policy files are in <a href=\"https://nigeltao.github.io/blog/2021/json-with-commas-comments.html\">HuJSON</a>, a JSON format with trailing commas and comments. If you don't want to write your tailnet policy files in HuJSON directly, you can use a tool that lets you generate JSON in the same schema as the HuJSON format. Make sure your tool puts the file in the same place as the <code>policy-file</code> setting in the GitHub Action.</li>\n</ul>\n<p><img src=\"https://cdn.brandfetch.io/tailscale.com/fallback/lettermark/theme/dark/h/256/w/256/icon?c=1bfwsmEH20zzEfSNTed\" alt=\"Project Logo\"></p>\n<p>Ask AI</p>\n<p>reCAPTCHA</p>\n<p>Recaptcha requires verification.</p>\n<p>protected by <strong>reCAPTCHA</strong></p>\n"}