From 046dc7260e96b71440a395ffa054f0228c3eb5b2 Mon Sep 17 00:00:00 2001 From: Kobi Meirson Date: Sun, 2 Nov 2025 15:37:30 +0200 Subject: [PATCH] feat: use Workload Identity Federation for authentication With the latest introduction of [Workload Identity Federation](https://tailscale.com/kb/1581/workload-identity-federation), there's an idiomatic mechanism for authenticating to Tailscale from Github Actions jobs without having to store any credentials (API Key / OAuth client secret) in Github Actions. The latest version of the action contained two different authentication options - API Key and OAuth Client. API Keys have time limit (maximum 90 days) and require rotation. With the introduction of OAuth clients and now Workload Identity Federation, we should discourage the usage of an expiring secret that can be used outside of Github Actions. OAuth clients introduced the need to store an additional credentials (OAuth Client ID & Secret) in Github Actions. As these credentials can be used everywhere, there's no guarentee they will only be used in the context of a Github Actions job. OpenID Connect and the implementation of Workload Identity Federation allows to **encourage** best practices by *forcing* the user to use these instead of any of the above. Leaving a single authentication method would also simplify the implementation of this Action - there's no need to validate which set of `inputs` was set, and later on we could also remove the [OAuth-related functionality from `gitops-pusher` itself](https://github.com/tailscale/tailscale/blob/db7dcd516f7da6792cd4fa44b97bc510102941c5/cmd/gitops-pusher/gitops-pusher.go#L179-L187). --- README.md | 45 ++++++++++++++++++++------------------------- action.yml | 47 ++++++++++++++++++++++++----------------------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index bbab563..5d93ed1 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,10 @@ as your source of truth. panel](https://login.tailscale.com/admin) and copying down the name next to the Tailscale logo in the upper left hand corner of the page. -### `api-key` +### `oidc-client-id` and `oidc-audience` -**Optional** An API key authorized for your tailnet. You can get one [in the -admin panel](https://login.tailscale.com/admin/settings/keys). -Either `api-key` or `oauth-client-id` and `oauth-secret` are required. - -Please note that API keys will expire in 90 days. Set up a monthly event to -rotate your Tailscale API key, or use an OAuth client. - -### `oauth-client-id` and `oauth-secret` - -**Optional** The ID and secret for an [OAuth client](https://tailscale.com/kb/1215/oauth-clients) -for your tailnet. The client must have the `acl` scope. +**Required** The OIDC client ID and audience for an [OIDC Credential](https://tailscale.com/kb/1581/workload-identity-federation#configure-federated-identities-in-the-admin-console) +for your tailnet. The credential must have the `policy_file` scope. Either `api-key` or `oauth-client-id` and `oauth-secret` are required. @@ -44,7 +35,7 @@ out to production. ## Getting Started -Set up a new GitHub repository that will contain your tailnet policy file. Open the [Access Controls page of the admin console](https://login.tailscale.com/admin/acls) and copy your policy file to +Set up a new GitHub repository that will contain your tailnet policy file. Open the [Access Controls page of the admin console](https://login.tailscale.com/admin/acls/file) and copy your policy file to a file in that repo called `policy.hujson`. If you want to change this name to something else, you will need to add the @@ -63,10 +54,13 @@ on: jobs: acls: + permissions: + contents: read + id-token: write # required for generating an OIDC token runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Fetch version-cache.json uses: actions/cache@v4 @@ -81,7 +75,8 @@ jobs: id: deploy-acl uses: tailscale/gitops-acl-action@v1 with: - api-key: ${{ secrets.TS_API_KEY }} + oidc-client-id: ${{ secrets.TS_OIDC_CLIENT_ID }} + oidc-audience: ${{ secrets.TS_OIDC_AUDIENCE }} tailnet: ${{ secrets.TS_TAILNET }} action: apply @@ -90,23 +85,23 @@ jobs: id: test-acl uses: tailscale/gitops-acl-action@v1 with: - api-key: ${{ secrets.TS_API_KEY }} + oidc-client-id: ${{ secrets.TS_OIDC_CLIENT_ID }} + oidc-audience: ${{ secrets.TS_OIDC_AUDIENCE }} tailnet: ${{ secrets.TS_TAILNET }} action: test ``` -Generate a new API key [here](https://login.tailscale.com/admin/settings/keys). +Generate a new credential via [Trust Credentials](https://login.tailscale.com/admin/settings/trust-credentials): + +Choose an *OpenID Connect* credential, and set the *Issuer* as `Github`. The Subject should be `repo:/::*`. ([refer to Github's documentation on the `sub` field](https://docs.github.com/en/actions/reference/security/oidc#example-subject-claims).) -Set a monthly calendar reminder to renew this key because Tailscale does not -currently support API key renewal (this will be updated to support that when -that feature is implemented). +In the *Scopes* step, choose **Policy File: Write** (or *Read* if not using the `apply` action.) -Then open the secrets settings for your repo and add two secrets: +Then open the secrets settings for your repo and add three secrets: -* `TS_API_KEY`: Your Tailscale API key from the earlier step -* `TS_TAILNET`: Your tailnet's name (it's next to the logo on the upper - left-hand corner of the [admin - panel](https://login.tailscale.com/admin/machines)) +* `TS_OIDC_CLIENT_ID`: Your Tailscale OIDC Client ID from the earlier step +* `TS_OIDC_AUDIENCE`: Your Tailscale OIDC Audience from the earlier step +* `TS_TAILNET`: Your tailnet's ID/name (it's available on the [admin panel Settings](https://login.tailscale.com/admin/settings/general)) Once you do that, commit the changes and push them to GitHub. You will have CI automatically test and push changes to your tailnet policy file to Tailscale. diff --git a/action.yml b/action.yml index 5a688ef..844e6af 100644 --- a/action.yml +++ b/action.yml @@ -2,17 +2,14 @@ name: "Sync Tailscale ACLs" description: "Push changes to Tailscale and run ACL tests in CI" inputs: tailnet: - description: "Tailnet name (eg. example.com, xe.github, tailscale.org.github)" + description: "Tailnet ID or Name (eg. TCZrp4oabE12CNTRL, example.com, xe.github, tailscale.org.github)" + required: true + oidc-client-id: + description: "Tailscale OIDC Client ID" + required: true + oidc-audience: + description: "Tailscale OIDC Audience" required: true - api-key: - description: "Tailscale API key" - required: false - oauth-client-id: - description: "Tailscale OAuth ID" - required: false - oauth-secret: - description: "Tailscale OAuth Secret" - required: false policy-file: description: "Path to policy file" required: true @@ -23,18 +20,26 @@ inputs: runs: using: "composite" steps: - - name: Check Auth Info Empty - if: ${{ inputs['api-key'] == '' && inputs['oauth-secret'] == '' }} + - name: Generate OIDC JWT from Github Actions + id: get_gh_oidc_token shell: bash run: | - echo "::error title=⛔ error hint::API Key or OAuth secret must be specified. Maybe you need to populate it in the Secrets for your workflow, see more in https://docs.github.com/en/actions/security-guides/encrypted-secrets and https://tailscale.com/s/oauth-clients" - exit 1 - - name: Check Conflicting Auth Info - if: ${{ inputs['api-key'] != '' && inputs['oauth-secret'] != '' }} + OIDC_JWT=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${{ inputs.oidc-audience }}" | jq -r '.value') + echo "::add-mask::$OIDC_JWT" + echo "jwt=$OIDC_JWT" >> $GITHUB_OUTPUT + + - name: Exchange OIDC JWT with a short-lived Tailscale API Key + id: tailscale_api_key shell: bash run: | - echo "::error title=⛔ error hint::only one of API Key or OAuth secret should be specified. - exit 1 + RESPONSE=$(curl -X POST https://api.tailscale.com/api/v2/oauth/token-exchange \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=${{ inputs.oidc-client-id }}" \ + -d "jwt=${{ steps.get_gh_oidc_token.outputs.jwt }}") + export ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token') + echo "::add-mask::$ACCESS_TOKEN" + echo "access_token=$ACCESS_TOKEN" >> $GITHUB_OUTPUT + - uses: actions/setup-go@v5 with: go-version: 1.22.4 @@ -43,10 +48,6 @@ runs: - name: Gitops pusher shell: bash env: - # gitops-pusher will use OAUTH_ID and OAUTH_SECRET if non-empty, - # otherwise it will use API_KEY. - TS_OAUTH_ID: "${{ inputs.oauth-client-id }}" - TS_OAUTH_SECRET: "${{ inputs.oauth-secret }}" - TS_API_KEY: "${{ inputs.api-key }}" + TS_API_KEY: "${{ steps.tailscale_api_key.outputs.access_token }}" TS_TAILNET: "${{ inputs.tailnet }}" run: go run tailscale.com/cmd/gitops-pusher@66aa77416744037baec93206ae212012a2314f83 "--policy-file=${{ inputs.policy-file }}" "${{ inputs.action }}"