{action.yml,.github}: add oidc workload identity federation flow to github action186 from samlinville/main

{action.yml,.github}: add oidc workload identity federation flow to github action
This commit is contained in:
Sam Linville 2025-08-12 09:16:36 -04:00 committed by GitHub
commit 265da64f6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 124 additions and 27 deletions

View file

@ -1,5 +1,8 @@
name: tailscale
permissions:
id-token: write # This is required for requesting the JWT
on:
workflow_dispatch:
push:
@ -15,12 +18,14 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest, windows-11-arm]
cache: ['false', 'true']
auth: ['oauth', 'oauth-legacy', 'oidc']
runs-on: ${{ matrix.os }}
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tailscale Action
- name: Tailscale Action (OAuth)
if: matrix.auth == 'oauth'
uses: ./
with:
oauth-client-id: ${{ secrets.TS_OAUTH_GRANULAR_CLIENT_ID }}
@ -28,29 +33,10 @@ jobs:
tags: tag:ci
use-cache: ${{ matrix.cache }}
- name: check for tailscale connection
shell: bash
run:
tailscale status -json | jq -r .BackendState | grep -q Running
- name: ensure no dirty files from Tailscale Action remain
shell: bash
run: |
extra_files=$(git ls-files . --exclude-standard --others)
if [ ! -z "$extra_files" ]; then
echo "::error::Unexpected extra files: $extra_files"
exit 1
fi
# This job runs as a sanity check to ensure we have not broken the ability for OAuth clients using
# our legacy scopes to successfully connect to tailnets using this action.
legacyScopesCheck:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tailscale Action
# This job runs as a sanity check to ensure we have not broken the ability for OAuth clients using
# our legacy scopes to successfully connect to tailnets using this action.
- name: Tailscale Action (OAuth legacy)
if: matrix.auth == 'oauth-legacy'
uses: ./
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
@ -58,6 +44,16 @@ jobs:
tags: tag:ci
use-cache: ${{ matrix.cache }}
- name: Tailscale Action (OIDC)
if: matrix.auth == 'oidc'
uses: ./
with:
oidc-client-id: ${{ secrets.TS_OIDC_CLIENT_ID }}
oidc-aud-claim: ${{ secrets.TS_OIDC_AUD_CLAIM }}
tags: tag:ci
tailnet: ${{ secrets.TS_TAILNET_NAME }}
use-cache: ${{ matrix.cache }}
- name: check for tailscale connection
shell: bash
run:

View file

@ -29,6 +29,19 @@ be automatically removed by the coordination server a short time after they
finish their run. The nodes are also [marked Preapproved](https://tailscale.com/kb/1085/auth-keys/)
on tailnets which use [Device Approval](https://tailscale.com/kb/1099/device-approval/)
## Authenticate with GitHub's OIDC provider (alpha)
The Tailscale GitHub action can use an OIDC token provided by GitHub to authenticate the workflow to your tailnet. This functionality is currently available as a private alpha. To join the alpha program, contact your account rep or email `sam@tailscale.com`.
```yaml
- name: Tailscale
uses: tailscale/github-action@v3
with:
oidc-client-id: ${{ secrets.TS_OIDC_CLIENT_ID }}
oidc-aud-claim: ${{ secrets.TS_OIDC_AUD_CLAIM }}
tags: tag:ci
```
## Tailnet Lock
If you are using this Action in a [Tailnet
@ -92,4 +105,4 @@ You can opt in to caching Tailscale binaries by passing `'true'` to the `use-cac
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
use-cache: 'true'
```
```

View file

@ -17,6 +17,15 @@ inputs:
oauth-secret:
description: 'Your Tailscale OAuth Client Secret.'
required: false
oidc-client-id:
description: 'Your Tailscale OIDC Client ID.'
required: false
oidc-aud-claim:
description: 'Your Tailscale OIDC audience claim'
required: false
tailnet:
description: 'Your Tailscale tailnet name (e.g., example.com)'
required: false
tags:
description: 'Comma separated list of Tags to be applied to nodes. The OAuth client must have permission to apply these tags.'
required: false
@ -66,12 +75,82 @@ runs:
echo "::error title=⛔ error hint::Support Linux, Windows, and macOS Only"
exit 1
- name: Check Auth Info Empty
if: ${{ inputs.authkey == '' && (inputs['oauth-secret'] == '' || inputs.tags == '') }}
if: ${{ inputs.authkey == '' && (inputs.tags == '' || ((inputs['oauth-secret'] == '') && (inputs['oidc-client-id'] == '' || inputs['oidc-aud-claim'] == '')) }}
shell: bash
run: |
echo "::error title=⛔ error hint::OAuth identity empty, 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"
echo "::error title=⛔ error hint::OAuth and OIDC identity are empty, 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: get OIDC token from GitHub Actions
if: ${{ inputs['oidc-client-id'] != '' && inputs['oidc-aud-claim'] != '' && inputs['oauth-secret'] == '' }}
shell: bash
run: |
JWT=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${{ inputs['oidc-aud-claim'] }}" | jq -r '.value')
if [ -z "$JWT" ]; then
echo "::error title=⛔ error hint::Failed to get JWT from GitHub Actions OIDC provider"
exit 1
fi
echo "::add-mask::$JWT" # Mask the JWT in the logs
echo "JWT=$JWT" >> $GITHUB_ENV
- name: Exchange JWT OIDC token and get authkey
if: ${{ inputs['oidc-client-id'] != '' && inputs['oidc-aud-claim'] != '' }}
shell: bash
run: |
echo "Exchanging JWT OIDC token for short-lived API token..."
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=$JWT")
export ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.access_token')
echo "::add-mask::$ACCESS_TOKEN" # Mask the access token in the logs
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
echo "::error title=⛔ error hint::Failed to get ACCESS_TOKEN from OIDC token exchange, response: $RESPONSE"
exit 1
fi
echo "Retrieving authkey from Tailscale API"
# Convert comma-separated tags to JSON array format
TAGS_JSON=$(echo "${{ inputs.tags }}" | sed 's/,/","/g' | sed 's/^/["/' | sed 's/$/"]/')
# Make the API call and capture both the response and curl info
CURL_OUTPUT=$(curl -s -w "\n%{http_code}" -X POST https://api.tailscale.com/api/v2/tailnet/${{ inputs.tailnet }}/keys \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer ${ACCESS_TOKEN}" \
--data "{
\"keyType\": \"auth\",
\"capabilities\": {
\"devices\": {
\"create\": {
\"reusable\": false,
\"ephemeral\": true,
\"preauthorized\": true,
\"tags\": $TAGS_JSON
}
}
},
\"expirySeconds\": 3600
}")
# Extract HTTP status code (last line) and JSON response (everything else)
HTTP_STATUS=$(echo "$CURL_OUTPUT" | tail -n1)
JSON_RESPONSE=$(echo "$CURL_OUTPUT" | sed '$d')
echo "::add-mask::$JSON_RESPONSE" # Mask the JSON response in the logs
if [ "$HTTP_STATUS" != "200" ]; then
echo "::error title=⛔ error hint::Tailscale API returned HTTP $HTTP_STATUS"
exit 1
fi
export AUTHKEY=$(echo "$JSON_RESPONSE" | jq -r '.key')
echo "::add-mask::$AUTHKEY" # Mask the auth key in the logs
if [ -z "$AUTHKEY" ] || [ "$AUTHKEY" = "null" ]; then
echo "::error title=⛔ error hint::Failed to get AUTHKEY from Tailscale API"
exit 1
fi
echo "AUTHKEY=$AUTHKEY" >> $GITHUB_ENV
- name: Set Resolved Version
shell: bash
run: |
@ -310,6 +389,15 @@ runs:
TAILSCALE_AUTHKEY="${{ inputs['oauth-secret'] }}?preauthorized=true&ephemeral=true"
TAGS_ARG="--advertise-tags=${{ inputs.tags }}"
fi
if [ -n "${{ inputs['oidc-aud-claim'] }}" ] && [ -n "${{ inputs['oidc-client-id'] }}" ]; then
if [ -z "$AUTHKEY" ]; then
echo "::error title=⛔ error hint::Failed to get AUTHKEY from OIDC token exchange"
exit 1
fi
TAILSCALE_AUTHKEY="$AUTHKEY"
TAGS_ARG="--advertise-tags=${{ inputs.tags }}"
fi
if [ "${{ runner.os }}" != "Windows" ]; then
MAYBE_SUDO="sudo -E"
fi