Skip to content

Commit 33db07e

Browse files
committed
Implement workload identity token exchange
Signed-off-by: Ryan S <267728323+ironcommit@users.noreply.github.com>
1 parent b07430f commit 33db07e

93 files changed

Lines changed: 5047 additions & 371 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contrib/auth/authentik/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.generated/

contrib/auth/authentik/README.md

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ to verify three user-visible flows:
55

66
- log in to NeMo with Authentik
77
- call NeMo APIs through the Authentik gateway
8-
- run a NeMo job whose workload uses a real Authentik workload token
8+
- run a NeMo job whose workload exchanges a real Authentik workload subject token
99

1010
All credentials in this example are for local development only.
1111

@@ -26,6 +26,30 @@ The stack seeds these local-only identities:
2626
- Workload identity: `svc-nemo`
2727
- Workload group: `nemo-editors`
2828

29+
## Token Lifetime Knobs
30+
31+
The demo exposes separate lifetime controls so refresh behavior can be tested
32+
without shortening the human CLI session:
33+
34+
```bash
35+
export AUTHENTIK_NMP_WORKLOAD_IDENTITY_ACCESS_TOKEN_VALIDITY=seconds=90
36+
export AUTHENTIK_NMP_CLI_ACCESS_TOKEN_VALIDITY=hours=1
37+
export NMP_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS=120
38+
```
39+
40+
`AUTHENTIK_NMP_WORKLOAD_IDENTITY_ACCESS_TOKEN_VALIDITY` is rendered into the
41+
Authentik workload OAuth provider. `AUTHENTIK_NMP_CLI_ACCESS_TOKEN_VALIDITY`
42+
is rendered into the CLI OAuth provider. `NMP_WORKLOAD_IDENTITY_TOKEN_TTL_SECONDS`
43+
is used by the Kubernetes demo for projected service account token
44+
`expirationSeconds` and by the Docker controller as a fallback refresh schedule
45+
when the demo issuer omits `expires_in`.
46+
47+
Authentik 2024.12 issues the demo workload subject token, but it does not accept
48+
the RFC 8693 token exchange grant directly. The Compose stack therefore includes
49+
a demo-only `workload-token-exchange` service. The Docker backend refreshes the
50+
Authentik subject token file, the SDK posts that token to the broker, and the
51+
gateway trusts the broker JWKS for exchanged workload access tokens.
52+
2953
## Start The Stack
3054

3155
From the repo root:
@@ -67,6 +91,11 @@ contrib/auth/authentik/run.sh test --lifecycle reuse
6791
contrib/auth/authentik/run.sh test --image registry.example.com/nemo/nmp-api:<tag>
6892
```
6993

94+
For a Kubernetes-hosted Authentik reference, see
95+
`contrib/auth/authentik/kubernetes/README.md`. That demo uses Kubernetes
96+
projected service account tokens as the workload subject token source instead
97+
of Docker's controller-managed secrets volume.
98+
7099
Wait until the platform is ready through the gateway:
71100

72101
```bash
@@ -142,26 +171,6 @@ group can read the workspace from a job.
142171
export JOB_NAME=authentik-workload-demo
143172
```
144173

145-
Fetch a local demo token for the seeded workload identity:
146-
147-
```bash
148-
export WORKLOAD_ACCESS_TOKEN="$(
149-
curl -fsS http://127.0.0.1:18080/application/o/token/ \
150-
-d grant_type=password \
151-
-d client_id=nemo-platform \
152-
-d client_secret=nemo-platform-secret-dev \
153-
-d username=svc-nemo \
154-
-d password=svc-nemo-token-secret-dev \
155-
-d scope="openid email groups" \
156-
| python -c 'import json, sys; print(json.load(sys.stdin)["access_token"])'
157-
)"
158-
```
159-
160-
Keep this token in a non-reserved shell variable. Do not export it as
161-
`NEMO_WORKLOAD_TOKEN` in your shell. The NeMo CLI uses that variable as a
162-
runtime credential override, which would make later CLI commands run as the
163-
workload identity instead of `authentik-human`.
164-
165174
Submit a job that runs the built-in hello-world workload auth task:
166175

167176
```bash
@@ -191,12 +200,6 @@ cat <<EOF | nemo --context authentik-human jobs create "$JOB_NAME" \
191200
]
192201
}
193202
},
194-
"environment": [
195-
{
196-
"name": "NEMO_WORKLOAD_TOKEN",
197-
"value": "${WORKLOAD_ACCESS_TOKEN}"
198-
}
199-
],
200203
"config": {"workspace": "${WORKSPACE}"}
201204
}
202205
]
@@ -211,9 +214,17 @@ Watch it complete:
211214
nemo --context authentik-human jobs get-status "$JOB_NAME" --workspace "$WORKSPACE"
212215
```
213216

214-
Expected result: the job reaches `completed`. The workload exits successfully
215-
only after it uses the Authentik workload token to call NeMo through the gateway
216-
and retrieve the workspace.
217+
Expected result: the job reaches `completed`. The Docker jobs backend creates a
218+
dedicated workload identity volume, writes the current subject token to
219+
`/var/run/secrets/nemo-platform/workload/token`, injects
220+
`NMP_WORKLOAD_IDENTITY_TOKEN_FILE` with that path, and refreshes the file while
221+
the workload is running. The SDK reads that file and performs RFC 8693 token
222+
exchange before calling NeMo through the gateway.
223+
224+
Do not include `NMP_WORKLOAD_IDENTITY_TOKEN_FILE`, `NEMO_WORKLOAD_TOKEN`, or
225+
`NEMO_WORKLOAD_TOKEN_FILE` in the job request. Managed job backends own those
226+
auth variables. The existing `/var/run/scratch/config` mount remains only for
227+
task configuration.
217228

218229
Read the job logs:
219230

contrib/auth/authentik/blueprints/nemo.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ entries:
5353
client_id: nemo-platform-cli
5454
sub_mode: user_username
5555
include_claims_in_id_token: true
56+
access_token_validity: "hours=1"
5657
redirect_uris: []
5758
authorization_flow:
5859
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
@@ -66,6 +67,29 @@ entries:
6667
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]]
6768
- !KeyOf nemo-groups-scope
6869

70+
- model: authentik_providers_oauth2.oauth2provider
71+
id: nemo-workload-provider
72+
identifiers:
73+
name: nemo-platform-workload
74+
attrs:
75+
name: nemo-platform-workload
76+
client_type: public
77+
client_id: nemo-platform-workload
78+
sub_mode: user_username
79+
include_claims_in_id_token: true
80+
access_token_validity: "minutes=5"
81+
redirect_uris: []
82+
authorization_flow:
83+
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
84+
invalidation_flow:
85+
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
86+
signing_key:
87+
!Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
88+
property_mappings:
89+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
90+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
91+
- !KeyOf nemo-groups-scope
92+
6993
- model: authentik_core.application
7094
identifiers:
7195
slug: nemo
@@ -82,6 +106,14 @@ entries:
82106
slug: nemo-cli
83107
provider: !KeyOf nemo-cli-provider
84108

109+
- model: authentik_core.application
110+
identifiers:
111+
slug: nemo-workload
112+
attrs:
113+
name: NeMo Platform Workload Identity
114+
slug: nemo-workload
115+
provider: !KeyOf nemo-workload-provider
116+
85117
- model: authentik_core.group
86118
id: group-nemo-admins
87119
identifiers:
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# authentik blueprint: provision everything NeMo needs to use authentik as its
2+
# OIDC IdP, including a demo workload identity for gateway-verified auth.
3+
4+
version: 1
5+
metadata:
6+
name: nemo-platform-oidc
7+
labels:
8+
blueprints.goauthentik.io/description: "NeMo Platform OIDC provider + demo workload identity"
9+
10+
entries:
11+
- model: authentik_providers_oauth2.scopemapping
12+
id: nemo-groups-scope
13+
identifiers:
14+
name: "NeMo groups"
15+
attrs:
16+
name: "NeMo groups"
17+
scope_name: groups
18+
description: "Comma-separated group membership used by the gateway workload header contract"
19+
expression: |
20+
return {"groups": ",".join(group.name for group in user.ak_groups.all())}
21+
22+
- model: authentik_providers_oauth2.oauth2provider
23+
id: nemo-provider
24+
identifiers:
25+
name: nemo-platform
26+
attrs:
27+
name: nemo-platform
28+
client_type: confidential
29+
client_id: nemo-platform
30+
client_secret: nemo-platform-secret-dev
31+
sub_mode: user_username
32+
include_claims_in_id_token: true
33+
redirect_uris: []
34+
authorization_flow:
35+
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
36+
invalidation_flow:
37+
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
38+
signing_key:
39+
!Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
40+
property_mappings:
41+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
42+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
43+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]]
44+
- !KeyOf nemo-groups-scope
45+
46+
- model: authentik_providers_oauth2.oauth2provider
47+
id: nemo-cli-provider
48+
identifiers:
49+
name: nemo-platform-cli
50+
attrs:
51+
name: nemo-platform-cli
52+
client_type: public
53+
client_id: nemo-platform-cli
54+
sub_mode: user_username
55+
include_claims_in_id_token: true
56+
access_token_validity: "__AUTHENTIK_NMP_CLI_ACCESS_TOKEN_VALIDITY__"
57+
redirect_uris: []
58+
authorization_flow:
59+
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
60+
invalidation_flow:
61+
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
62+
signing_key:
63+
!Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
64+
property_mappings:
65+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
66+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
67+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]]
68+
- !KeyOf nemo-groups-scope
69+
70+
- model: authentik_providers_oauth2.oauth2provider
71+
id: nemo-workload-provider
72+
identifiers:
73+
name: nemo-platform-workload
74+
attrs:
75+
name: nemo-platform-workload
76+
client_type: public
77+
client_id: nemo-platform-workload
78+
sub_mode: user_username
79+
include_claims_in_id_token: true
80+
access_token_validity: "__AUTHENTIK_NMP_WORKLOAD_IDENTITY_ACCESS_TOKEN_VALIDITY__"
81+
redirect_uris: []
82+
authorization_flow:
83+
!Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
84+
invalidation_flow:
85+
!Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
86+
signing_key:
87+
!Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
88+
property_mappings:
89+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
90+
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
91+
- !KeyOf nemo-groups-scope
92+
93+
- model: authentik_core.application
94+
identifiers:
95+
slug: nemo
96+
attrs:
97+
name: NeMo Platform
98+
slug: nemo
99+
provider: !KeyOf nemo-provider
100+
101+
- model: authentik_core.application
102+
identifiers:
103+
slug: nemo-cli
104+
attrs:
105+
name: NeMo Platform CLI
106+
slug: nemo-cli
107+
provider: !KeyOf nemo-cli-provider
108+
109+
- model: authentik_core.application
110+
identifiers:
111+
slug: nemo-workload
112+
attrs:
113+
name: NeMo Platform Workload Identity
114+
slug: nemo-workload
115+
provider: !KeyOf nemo-workload-provider
116+
117+
- model: authentik_core.group
118+
id: group-nemo-admins
119+
identifiers:
120+
name: nemo-admins
121+
attrs:
122+
name: nemo-admins
123+
124+
- model: authentik_core.group
125+
id: group-nemo-editors
126+
identifiers:
127+
name: nemo-editors
128+
attrs:
129+
name: nemo-editors
130+
131+
- model: authentik_core.user
132+
id: nemo-user
133+
identifiers:
134+
username: nemo-user
135+
attrs:
136+
username: nemo-user
137+
email: nemo-user@example.com
138+
name: "NeMo CLI demo user"
139+
password: nemo-user-password-dev
140+
groups:
141+
- !KeyOf group-nemo-editors
142+
143+
- model: authentik_core.token
144+
identifiers:
145+
identifier: nemo-user-token
146+
attrs:
147+
identifier: nemo-user-token
148+
intent: app_password
149+
user: !KeyOf nemo-user
150+
key: nemo-user-token-secret-dev
151+
expiring: false
152+
153+
- model: authentik_core.user
154+
id: svc-nemo
155+
identifiers:
156+
username: svc-nemo
157+
attrs:
158+
username: svc-nemo
159+
name: "NeMo demo workload identity"
160+
type: service_account
161+
path: service-accounts
162+
groups:
163+
- !KeyOf group-nemo-editors
164+
165+
- model: authentik_core.token
166+
identifiers:
167+
identifier: svc-nemo-token
168+
attrs:
169+
identifier: svc-nemo-token
170+
intent: app_password
171+
user: !KeyOf svc-nemo
172+
key: svc-nemo-token-secret-dev
173+
expiring: false
174+
175+
- model: authentik_brands.brand
176+
identifiers:
177+
domain: authentik-default
178+
attrs:
179+
default: true
180+
flow_device_code:
181+
!Find [authentik_flows.flow, [slug, default-authentication-flow]]

contrib/auth/authentik/config/platform-compose-authentik.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ platform:
22
runtime: "none"
33
base_url: "http://0.0.0.0:8080"
44
loopback_address: "nemo-gateway"
5+
service_discovery:
6+
files: "http://nemo-direct:8080"
57

68
service: {}
79

@@ -23,6 +25,13 @@ auth:
2325
client_id: "nemo-platform-cli"
2426
token_endpoint: "http://127.0.0.1:18080/application/o/token/"
2527
device_authorization_endpoint: "http://127.0.0.1:18080/application/o/device/"
28+
workload_token_exchange_enabled: true
29+
workload_client_id: "nemo-platform-workload"
30+
workload_token_endpoint: "http://workload-token-exchange:8080/application/o/token/"
31+
workload_subject_token_type: "urn:ietf:params:oauth:token-type:jwt"
32+
workload_requested_token_type: "urn:ietf:params:oauth:token-type:access_token"
33+
workload_audience: "nemo-platform"
34+
workload_scope: "openid email groups"
2635
subject_claim: "sub"
2736
email_claim: "email"
2837
groups_claim: "groups"
@@ -39,6 +48,13 @@ jobs:
3948
config:
4049
cleanup_completed_jobs_immediately: false
4150
launcher_tool_path: /tools/jobs-launcher
51+
workload_identity:
52+
enabled: true
53+
token_endpoint: "http://authentik-server:9000/application/o/token/"
54+
client_id: "nemo-platform-workload"
55+
username: "svc-nemo"
56+
password: "svc-nemo-token-secret-dev"
57+
scope: "openid email groups"
4258
executor_defaults:
4359
docker:
4460
cleanup_completed_jobs_immediately: false

0 commit comments

Comments
 (0)