Skip to content

Commit d8e2ae4

Browse files
authored
feature(auth): add optional scoped access key authentication (#970)
Signed-off-by: Ryan S <267728323+ironcommit@users.noreply.github.com>
1 parent 74225c4 commit d8e2ae4

100 files changed

Lines changed: 9251 additions & 591 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/compose/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ services:
2222
NMP_CONFIG_FILE_PATH: /etc/nmp/config.yaml
2323
NMP_CONFIG_WARNINGS_DISABLED: "1"
2424
NMP_AUTH_POLICY_DECISION_POINT_BASE_URL: http://127.0.0.1:8080
25+
NMP_AUTH_TOKEN_SIGNING__PRIVATE_KEY_FILE: /var/run/secrets/nemo-platform/workload-token-signing/private-key.pem
2526
NMP_SEED_ON_STARTUP: "true"
2627
NMP_PLATFORM_SEED_MODEL_PROVIDER_ENABLED: "false"
2728
NMP_SECRETS_ALLOW_KEY_CREATION: "1"

contrib/auth/authentik/compose/implementation-details.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ share local keys:
5656
The workload-token private key is mounted into `nemo` at
5757
`/var/run/secrets/nemo-platform/workload-token-signing/private-key.pem`.
5858
`platform-compose-authentik.yaml` points
59-
`auth.oidc.workload_token_private_key_file` at that mounted path. The NeMo auth
60-
service uses the private key to sign workload-exchange access tokens, and Envoy
61-
validates those exchanged tokens through the NeMo auth service JWKS endpoint.
59+
`auth.token_signing.private_key_file` at that mounted path. The NeMo auth
60+
service uses the private key to sign workload-exchange access tokens and Scoped
61+
Access Key JWTs, and Envoy validates those tokens through the NeMo auth service
62+
JWKS endpoints.
6263

6364
The gateway TLS files are copied into the `gateway-tls` named volume by
6465
`gateway-tls-init`. The `gateway` service uses that volume to serve HTTPS, and
@@ -117,15 +118,22 @@ Envoy is the public entrypoint for the Compose example. It routes:
117118
NeMo and Authentik through their upstream clusters.
118119
- Authentik paths to `authentik-server`.
119120

120-
Before JWT validation, Envoy removes incoming `X-NMP-Principal-*` headers so a
121-
client cannot spoof identity headers. For `/apis/` requests, Envoy accepts
122-
either:
123-
124-
- Authentik-issued tokens from the demo providers.
125-
- NeMo-issued workload-exchange tokens from `/apis/auth/token`.
126-
127-
Envoy copies the validated `sub` and `groups` claims into NeMo's principal
128-
headers. NeMo then applies its normal workspace authorization checks.
121+
Before authentication, Envoy removes incoming `X-NMP-Principal-*` and
122+
`X-NMP-Scopes` headers so a client cannot spoof identity or scopes. For
123+
protected `/apis/` requests, Envoy calls NeMo's
124+
`/apis/auth/authenticate` endpoint with the presented bearer token. The auth
125+
service validates Authentik OIDC tokens, NeMo workload-exchange access tokens,
126+
and NeMo Scoped Access Keys, then returns trusted `X-NMP-Principal-*` and
127+
`X-NMP-Scopes` headers for Envoy to forward upstream.
128+
129+
The gateway callout is required for dynamic or revocable Scoped Access Keys
130+
because Envoy JWKS validation can only prove token signature, issuer, audience,
131+
and time claims. It cannot check NeMo's access-key lifecycle state. Compose
132+
keeps `auth.access_keys.enabled=true` so Scoped Access Keys can be created and
133+
validated; Envoy performs the bearer-to-header mapping before the request
134+
reaches service middleware.
135+
Scoped Access Keys are enabled in the checked-in Compose config because the Compose
136+
test runtime advertises the `platform_access_keys` capability.
129137

130138
## Workload Token Exchange
131139

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ auth:
1212
policy_data_refresh_interval: 2
1313
bundle_cache_seconds: 15
1414
admin_email: "admin@example.com"
15+
token_signing:
16+
issuer: "https://nemo-gateway:8080/apis/auth"
17+
key_id: "nemo-platform-signing"
18+
private_key_file: "/var/run/secrets/nemo-platform/workload-token-signing/private-key.pem"
19+
access_keys:
20+
enabled: true
1521
oidc:
1622
enabled: true
1723
issuer: "http://authentik-server:9000/application/o/nemo-cli/"
@@ -26,9 +32,7 @@ auth:
2632
workload_client_id: "nemo-platform-workload"
2733
workload_audience: "nemo-platform"
2834
workload_scope: "openid email groups"
29-
workload_token_issuer: "https://nemo-gateway:8080/apis/auth"
3035
workload_token_endpoint: "https://nemo-gateway:8080/apis/auth/token"
31-
workload_token_private_key_file: "/var/run/secrets/nemo-platform/workload-token-signing/private-key.pem"
3236
workload_subject_jwks_uri: "http://authentik-server:9000/application/o/nemo-workload/jwks/"
3337
workload_subject_issuers:
3438
- "http://authentik-server:9000/application/o/nemo-workload/"

contrib/auth/authentik/gateway/envoy.yaml

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,62 @@ static_resources:
3131
prefix: "/.well-known/nemo-platform/"
3232
route:
3333
cluster: nemo
34+
typed_per_filter_config:
35+
envoy.filters.http.ext_authz:
36+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
37+
disabled: true
38+
request_headers_to_add:
39+
- header:
40+
key: x-forwarded-proto
41+
value: https
42+
append_action: OVERWRITE_IF_EXISTS_OR_ADD
43+
- match:
44+
path: "/apis/auth/discovery"
45+
route:
46+
cluster: nemo
47+
typed_per_filter_config:
48+
envoy.filters.http.ext_authz:
49+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
50+
disabled: true
51+
request_headers_to_add:
52+
- header:
53+
key: x-forwarded-proto
54+
value: https
55+
append_action: OVERWRITE_IF_EXISTS_OR_ADD
56+
- match:
57+
path: "/apis/auth/authenticate"
58+
route:
59+
cluster: nemo
60+
typed_per_filter_config:
61+
envoy.filters.http.ext_authz:
62+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
63+
disabled: true
64+
request_headers_to_add:
65+
- header:
66+
key: x-forwarded-proto
67+
value: https
68+
append_action: OVERWRITE_IF_EXISTS_OR_ADD
69+
- match:
70+
path: "/apis/auth/jwks"
71+
route:
72+
cluster: nemo
73+
typed_per_filter_config:
74+
envoy.filters.http.ext_authz:
75+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
76+
disabled: true
77+
request_headers_to_add:
78+
- header:
79+
key: x-forwarded-proto
80+
value: https
81+
append_action: OVERWRITE_IF_EXISTS_OR_ADD
82+
- match:
83+
path: "/apis/auth/token"
84+
route:
85+
cluster: nemo
86+
typed_per_filter_config:
87+
envoy.filters.http.ext_authz:
88+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
89+
disabled: true
3490
request_headers_to_add:
3591
- header:
3692
key: x-forwarded-proto
@@ -51,10 +107,18 @@ static_resources:
51107
status: 503
52108
body:
53109
inline_string: '{"status":"not_ready"}'
110+
typed_per_filter_config:
111+
envoy.filters.http.ext_authz:
112+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
113+
disabled: true
54114
- match:
55115
prefix: "/health/"
56116
route:
57117
cluster: nemo
118+
typed_per_filter_config:
119+
envoy.filters.http.ext_authz:
120+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
121+
disabled: true
58122
request_headers_to_add:
59123
- header:
60124
key: x-forwarded-proto
@@ -64,6 +128,10 @@ static_resources:
64128
path: "/status"
65129
route:
66130
cluster: nemo
131+
typed_per_filter_config:
132+
envoy.filters.http.ext_authz:
133+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
134+
disabled: true
67135
request_headers_to_add:
68136
- header:
69137
key: x-forwarded-proto
@@ -73,6 +141,10 @@ static_resources:
73141
prefix: "/studio/"
74142
route:
75143
cluster: nemo
144+
typed_per_filter_config:
145+
envoy.filters.http.ext_authz:
146+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
147+
disabled: true
76148
request_headers_to_add:
77149
- header:
78150
key: x-forwarded-proto
@@ -82,6 +154,10 @@ static_resources:
82154
prefix: "/"
83155
route:
84156
cluster: authentik
157+
typed_per_filter_config:
158+
envoy.filters.http.ext_authz:
159+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
160+
disabled: true
85161
request_headers_to_add:
86162
- header:
87163
key: x-forwarded-proto
@@ -125,6 +201,8 @@ static_resources:
125201
headers:remove("x-nmp-principal-on-behalf-of")
126202
headers:remove("x-nmp-principal-on-behalf-of-email")
127203
headers:remove("x-nmp-principal-on-behalf-of-groups")
204+
headers:remove("x-nmp-authorized")
205+
headers:remove("x-nmp-scopes")
128206
129207
if headers:get(":path") ~= "/health/gateway/ready" then
130208
return
@@ -142,58 +220,30 @@ static_resources:
142220
string.format('{"status":"not_ready","nemo":"%s","authentik":"%s"}', nemo_status, authentik_status)
143221
)
144222
end
145-
- name: envoy.filters.http.jwt_authn
223+
- name: envoy.filters.http.ext_authz
146224
typed_config:
147-
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
148-
providers:
149-
authentik_workload:
150-
audiences:
151-
- "nemo-platform"
152-
- "nemo-platform-cli"
153-
- "nemo-platform-workload"
154-
remote_jwks:
155-
http_uri:
156-
uri: "http://authentik-server:9000/application/o/nemo/jwks/"
157-
cluster: authentik
158-
timeout: 5s
159-
cache_duration: 600s
160-
claim_to_headers:
161-
- header_name: "X-NMP-Principal-Id"
162-
claim_name: "sub"
163-
- header_name: "X-NMP-Principal-Groups"
164-
claim_name: "groups"
165-
workload_exchange:
166-
audiences:
167-
- "nemo-platform"
168-
remote_jwks:
169-
http_uri:
170-
uri: "http://nemo:8080/apis/auth/jwks"
171-
cluster: nemo
172-
timeout: 5s
173-
cache_duration: 600s
174-
claim_to_headers:
175-
- header_name: "X-NMP-Principal-Id"
176-
claim_name: "sub"
177-
- header_name: "X-NMP-Principal-Groups"
178-
claim_name: "groups"
179-
rules:
180-
- match:
181-
path: "/apis/auth/discovery"
182-
- match:
183-
path: "/apis/auth/jwks"
184-
- match:
185-
path: "/apis/auth/token"
186-
- match:
187-
prefix: "/health/"
188-
- match:
189-
path: "/status"
190-
- match:
191-
prefix: "/apis/"
192-
requires:
193-
requires_any:
194-
requirements:
195-
- provider_name: "authentik_workload"
196-
- provider_name: "workload_exchange"
225+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
226+
transport_api_version: V3
227+
failure_mode_allow: false
228+
status_on_error:
229+
code: ServiceUnavailable
230+
http_service:
231+
server_uri:
232+
uri: "http://nemo:8080"
233+
cluster: nemo
234+
timeout: 5s
235+
path_prefix: "/apis/auth/authenticate"
236+
authorization_response:
237+
allowed_upstream_headers:
238+
patterns:
239+
- exact: x-nmp-principal-id
240+
- exact: x-nmp-principal-email
241+
- exact: x-nmp-principal-groups
242+
- exact: x-nmp-scopes
243+
allowed_client_headers:
244+
patterns:
245+
- exact: content-type
246+
- exact: www-authenticate
197247
- name: envoy.filters.http.router
198248
typed_config:
199249
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

0 commit comments

Comments
 (0)