Skip to content

Commit e129d0b

Browse files
committed
action.yml,src: add support for workload identity federation
Add support for workload identity federation based authentication. Updates tailscale/corp#31264 Signed-off-by: Mario Minardi <[email protected]>
1 parent c8de7fa commit e129d0b

File tree

5 files changed

+112
-33
lines changed

5 files changed

+112
-33
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: "Integration Tests"
22

3+
permissions:
4+
id-token: write # This is required for requesting the JWT for workload identity
5+
36
on:
47
pull_request:
58
workflow_dispatch:
@@ -15,6 +18,7 @@ jobs:
1518
strategy:
1619
fail-fast: false
1720
matrix:
21+
credential-type: [oauth, workload-identity]
1822
include:
1923
# Linux tests (AMD64)
2024
- os: ubuntu-latest
@@ -103,8 +107,9 @@ jobs:
103107
id: tailscale-oauth
104108
uses: ./
105109
with:
106-
oauth-client-id: ${{ secrets.TS_AUTH_KEYS_OAUTH_CLIENT_ID }}
107-
oauth-secret: ${{ secrets.TS_AUTH_KEYS_OAUTH_CLIENT_SECRET }}
110+
oauth-client-id: ${{ (matrix.credential-type == 'oauth' && secrets.TS_AUTH_KEYS_OAUTH_CLIENT_ID || secrets.TS_WORKLOAD_IDENTITY_CLIENT_ID) }}
111+
oauth-secret: ${{ matrix.credential-type == 'oauth' && secrets.TS_AUTH_KEYS_OAUTH_CLIENT_SECRET || '' }}
112+
audience: ${{ matrix.credential-type == 'workload-identity' && secrets.TS_AUDIENCE || ''}}
108113
tags: "tag:ci"
109114
version: "${{ matrix.version }}"
110115
use-cache: false

README.md

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,58 @@ by adding a step to your workflow.
1414
1515
Subsequent steps in the Action can then access nodes in your Tailnet.
1616
17-
oauth-client-id and oauth-secret are an [OAuth client](https://tailscale.com/s/oauth-clients/)
17+
oauth-client-id and oauth-secret are an [OAuth client][kb-oauth-clients]
1818
for the tailnet to be accessed. We recommend storing these as
1919
[GitHub Encrypted Secrets.](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
2020
OAuth clients used for this purpose must have the
21-
[`auth_keys` scope.](https://tailscale.com/kb/1215/oauth-clients#scopes)
21+
[`auth_keys` scope.][kb-trust-credentials-scopes]
2222

23-
tags is a comma-separated list of one or more [ACL Tags](https://tailscale.com/kb/1068/acl-tags/)
23+
tags is a comma-separated list of one or more [Tags][kb-tags]
2424
for the node. At least one tag is required: an OAuth client is not associated
2525
with any of the Users on the tailnet, it has to Tag its nodes.
2626

27-
Nodes created by this Action are [marked as Ephemeral](https://tailscale.com/s/ephemeral-nodes) to
27+
Nodes created by this Action are [marked as Ephemeral][kb-ephemeral-nodes] to
2828
and log out immediately after finishing their CI run, at which point they are automatically removed
29-
by the coordination server. The nodes are also [marked Preapproved](https://tailscale.com/kb/1085/auth-keys/)
30-
on tailnets which use [Device Approval](https://tailscale.com/kb/1099/device-approval/)
29+
by the coordination server. The nodes are also [marked Preapproved][kb-auth-keys]
30+
on tailnets which use [Device Approval][kb-device-approval]
31+
32+
33+
### Workload identity federation
34+
[Workload identity federation][kb-workload-identity-federation] can also be used for authenticating nodes with your tailnet:
35+
36+
```yaml
37+
- name: Tailscale
38+
uses: tailscale/github-action@v4
39+
with:
40+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
41+
audience: ${{ secrets.TS_AUDIENCE }}
42+
tags: tag:ci
43+
```
44+
45+
Workload identity federation requires the `id-token: write` [permission setting](https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-cloud-providers#adding-permissions-settings) for the workflow:
46+
47+
```yaml
48+
permissions:
49+
id-token: write # This is required for the tailscale action to request a JWT from GitHub
50+
```
51+
52+
Federated identity credentials used for this purpose must have the [`auth_keys` scope.][kb-trust-credentials-scopes]
53+
54+
tags is a comma-separated list of one or more [Tags][kb-tags]
55+
for the node. At least one tag is required: a federated identity is not associated
56+
with any of the Users on the tailnet, it has to Tag its nodes.
57+
58+
> [!IMPORTANT]
59+
> Tailscale version `1.90.1` or later is required for workload identity federation.
3160

3261
## Prerequisites
3362

3463
Before using the Tailscale GitHub Action, ensure you have the following:
3564

36-
1. A Tailscale account with <Role>Owner, Admin, or Network admin</Role> permissions.
65+
1. A Tailscale account with Owner, Admin, or Network admin permissions.
3766
1. A GitHub repository that you have admin access to (required to set up the GitHub Action).
38-
1. At least one configured [tag][kb-tags].
39-
1. An [OAuth client][kb-oauth-clients] ID and secret OR an [auth key][kb-auth-keys].
67+
1. At least one configured [tag][kb-tags] if using OAuth or workload identity federation.
68+
1. An [OAuth client][kb-oauth-clients] ID and secret, [federated identity][kb-workload-identity-federation] client ID and audience, OR an [auth key][kb-auth-keys].
4069
1. A runner image version >= 2.237.1 (required to support running Node.js 24).
4170

4271
## Eventual consistency
@@ -55,22 +84,21 @@ You can do this by adding a list of hosts to ping to the action configuration:
5584
ping: 100.x.y.z,my-machine.my-tailnet.ts.net
5685
```
5786

58-
or with the [tailscale ping](https://tailscale.com/kb/1080/cli#ping) command if you do not know the peers at the time of installing Tailscale in the workflow:
87+
or with the [tailscale ping][kb-cli-ping] command if you do not know the peers at the time of installing Tailscale in the workflow:
5988

6089
```bash
6190
tailscale ping my-target.my-tailnet.ts.net
6291
```
6392

64-
The `ping` option will wait up to to 3 minutes for a connection (direct or relayed).
93+
The `ping` option will wait up to 3 minutes for a connection (direct or relayed).
6594

6695
## Tailnet Lock
6796

68-
If you are using this Action in a [Tailnet
69-
Lock](https://tailscale.com/kb/1226/tailnet-lock) enabled network, you need to:
97+
If you are using this Action in a [Tailnet Lock][kb-tailnet-lock] enabled network, you need to:
7098

71-
- Authenticate using an ephemeral reusable [pre-signed auth key](https://tailscale.com/kb/1226/tailnet-lock#add-a-node-using-a-pre-signed-auth-key)
99+
- Authenticate using an ephemeral reusable [pre-signed auth key][kb-tailnet-lock-pre-signed]
72100
rather than an OAuth client.
73-
- Specify a [state directory](https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled) for the
101+
- Specify a [state directory][kb-tailscaled-flags] for the
74102
client to store the Tailnet Key Authority data in.
75103

76104
```yaml
@@ -139,4 +167,16 @@ the GitHub Action leaves tailscale binaries installed but stops the tailscale ba
139167

140168
### requested tags [tag:mytag] are invalid or not permitted
141169

142-
You may encounter this error when using an OAuth client. OAuth clients must have the [`auth_keys` scope](https://tailscale.com/kb/1215/oauth-clients#scopes) with one or more [tags](https://tailscale.com/kb/1068/acl-tags/), and the tags specified with `tags` must match all tags on the OAuth client.
170+
You may encounter this error when using an OAuth client. OAuth clients must have the [`auth_keys` scope][kb-trust-credentials-scopes] with one or more [tags][kb-tags], and the tags specified with `tags` must match all tags on the OAuth client.
171+
172+
[kb-auth-keys]: https://tailscale.com/kb/1085/auth-keys
173+
[kb-cli-ping]: https://tailscale.com/kb/1080/cli#ping
174+
[kb-device-approval]: https://tailscale.com/kb/1099/device-approval
175+
[kb-ephemeral-nodes]: https://tailscale.com/kb/1111/ephemeral-nodes
176+
[kb-oauth-clients]: https://tailscale.com/kb/1215/oauth-clients
177+
[kb-tags]: https://tailscale.com/kb/1068/tags
178+
[kb-tailnet-lock]: https://tailscale.com/kb/1226/tailnet-lock
179+
[kb-tailnet-lock-pre-signed]: https://tailscale.com/kb/1226/tailnet-lock#add-a-node-using-a-pre-signed-auth-key
180+
[kb-tailscaled-flags]: https://tailscale.com/kb/1278/tailscaled#flags-to-tailscaled
181+
[kb-trust-credentials-scopes]: https://tailscale.com/kb/1623/trust-credentials#scopes
182+
[kb-workload-identity-federation]: https://tailscale.com/kb/1581/workload-identity-federation

action.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ inputs:
1313
required: false
1414
deprecationMessage: 'An OAuth API client https://tailscale.com/s/oauth-clients is recommended instead of an authkey'
1515
oauth-client-id:
16-
description: 'Your Tailscale OAuth Client ID.'
16+
description: 'Your Tailscale OAuth or Workload Identity Federation Client ID.'
17+
required: false
18+
audience:
19+
description: 'Your Tailscale Workload Identity Federation Audience'
1720
required: false
1821
oauth-secret:
1922
description: 'Your Tailscale OAuth Client Secret.'
@@ -24,7 +27,7 @@ inputs:
2427
version:
2528
description: 'Tailscale version to use. Specify `latest` to use the latest stable version, and `unstable` to use the latest development version.'
2629
required: true
27-
default: '1.88.3'
30+
default: '1.90.2' # TODO: adjust this
2831
args:
2932
description: 'Optional additional arguments to `tailscale up`.'
3033
required: false

dist/index.js

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface TailscaleConfig {
3030
arch: string;
3131
authKey: string;
3232
oauthClientId: string;
33+
audience: string;
3334
oauthSecret: string;
3435
tags: string;
3536
hostname: string;
@@ -214,6 +215,7 @@ async function getInputs(): Promise<TailscaleConfig> {
214215
arch: "",
215216
authKey: authKey,
216217
oauthClientId: core.getInput("oauth-client-id") || "",
218+
audience: core.getInput("audience") || "",
217219
oauthSecret: oauthSecret,
218220
tags: core.getInput("tags") || "",
219221
hostname: core.getInput("hostname") || "",
@@ -237,9 +239,13 @@ async function getInputs(): Promise<TailscaleConfig> {
237239
}
238240

239241
function validateAuth(config: TailscaleConfig): void {
240-
if (!config.authKey && (!config.oauthSecret || !config.tags)) {
242+
if (
243+
!config.authKey &&
244+
(!config.oauthSecret || !config.tags) &&
245+
(!config.audience || !config.oauthClientId || !config.tags)
246+
) {
241247
throw new Error(
242-
"OAuth identity empty, please provide either an auth key or OAuth secret and tags."
248+
"Please provide either an auth key, OAuth secret and tags, or federated identity client ID and audience with tags."
243249
);
244250
}
245251
}
@@ -678,14 +684,26 @@ async function connectToTailscale(
678684
hostname = hostname.substring(0, 63);
679685

680686
// Prepare auth and tags
681-
let finalAuthKey = config.authKey;
687+
const authArgs: string[] = [];
682688
const tagsArg: string[] = [];
683689

684-
if (config.oauthSecret) {
685-
finalAuthKey = `${config.oauthSecret}?preauthorized=true&ephemeral=true`;
690+
if (config.authKey) {
691+
authArgs.push(`--authkey=${config.authKey}`);
692+
} else {
686693
tagsArg.push(`--advertise-tags=${config.tags}`);
687-
}
688694

695+
if (config.oauthSecret) {
696+
authArgs.push(
697+
`--authkey=${config.oauthSecret}?preauthorized=true&ephemeral=true`
698+
);
699+
} else if (config.audience) {
700+
const token = await core.getIDToken(config.audience);
701+
authArgs.push(
702+
`--client-id=${config.oauthClientId}?preauthorized=true&ephemeral=true`
703+
);
704+
authArgs.push(`--id-token=${token}`);
705+
}
706+
}
689707
// Platform-specific args
690708
const platformArgs: string[] = [];
691709
if (runnerOS === runnerWindows) {
@@ -696,11 +714,11 @@ async function connectToTailscale(
696714
const upArgs = [
697715
"up",
698716
...tagsArg,
699-
`--authkey=${finalAuthKey}`,
700717
`--hostname=${hostname}`,
701718
"--accept-routes",
702719
...platformArgs,
703720
...config.args.split(" ").filter(Boolean),
721+
...authArgs,
704722
];
705723

706724
// Retry logic

0 commit comments

Comments
 (0)