Skip to content

Commit 4cb8ea9

Browse files
authored
feat(oidc): support login flow for OIDC tokens (#4171)
* feat(oidc): support login flow for OIDC tokens Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): add docs for login flow Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): support all bearer types for login flow Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): address review comments Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): harden OIDC token exchange ownership and proxy fallback Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): address review comments Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> * feat(oidc): address review comments Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com> --------- Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
1 parent d3a06c9 commit 4cb8ea9

18 files changed

Lines changed: 2418 additions & 234 deletions

errors/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ var (
182182
ErrReceivedUnexpectedAuthHeader = errors.New("received unexpected www-authenticate header")
183183
ErrNoBearerToken = errors.New("no bearer token given")
184184
ErrInvalidBearerToken = errors.New("invalid bearer token given")
185+
ErrInvalidUpstreamTokenEndpoint = errors.New("invalid upstream token endpoint realm")
186+
ErrInvalidTokenProxyForm = errors.New("invalid token proxy form body")
185187
ErrInvalidOrUnreachableOIDCIssuer = errors.New("invalid or unreachable oidc issuer")
186188
ErrInsufficientScope = errors.New("bearer token does not have sufficient scope")
187189
ErrCouldNotLoadPublicKey = errors.New("failed to load public key")

examples/README-AWS-SECRETS-MANAGER-BEARER-AUTH.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,19 @@ To rotate keys without downtime:
177177

178178
### With OIDC Workload Identity
179179

180-
AWS Secrets Manager bearer authentication can coexist with OIDC workload identity. If both are configured, Zot will try OIDC authentication first, then fall back to traditional bearer token authentication using keys from AWS Secrets Manager:
180+
AWS Secrets Manager bearer authentication can coexist with OIDC workload identity. Direct bearer tokens are still verified using keys from AWS Secrets Manager after OIDC authentication fails. For challenge-based OCI username/password login, configure the optional `upstreamTokenEndpoint` object so Zot's OIDC token exchange endpoint can forward token requests that are not owned by local token backends to the existing traditional bearer token service. `upstreamTokenEndpoint.realm` must use HTTPS by default; plaintext HTTP requires the explicit `upstreamTokenEndpoint.allowInsecureHttp` opt-in and should only be used in controlled test environments:
181181

182182
```json
183183
{
184184
"http": {
185185
"auth": {
186186
"bearer": {
187-
"realm": "zot",
188-
"service": "zot-service",
187+
"realm": "https://zot.example.com/zot/auth/token",
188+
"service": "zot.example.com",
189+
"upstreamTokenEndpoint": {
190+
"realm": "https://auth.myreg.io/auth/token",
191+
"service": "myauth"
192+
},
189193
"awsSecretsManager": {
190194
"region": "us-east-1",
191195
"secretName": "zot/jwt-verification-keys"

examples/README-OIDC-WORKLOAD-IDENTITY.md

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@ OIDC Workload Identity authentication allows workloads (e.g., Kubernetes pods, C
1919

2020
### Basic Configuration
2121

22-
Add OIDC workload identity configuration to your bearer authentication settings. The `oidc` field accepts an array of provider configurations:
22+
Add OIDC workload identity configuration to your bearer authentication settings. The `oidc` field accepts an array of provider configurations.
2323

2424
```json
2525
{
2626
"http": {
2727
"auth": {
2828
"bearer": {
29-
"realm": "zot",
30-
"service": "zot-service",
3129
"oidc": [
3230
{
3331
"issuer": "https://kubernetes.default.svc.cluster.local",
@@ -49,6 +47,16 @@ Add OIDC workload identity configuration to your bearer authentication settings.
4947
- **`audiences`** (required): List of acceptable audiences for the OIDC token. At least one must be specified.
5048
- Example: `["zot", "https://zot.example.com"]`
5149

50+
- **`realm`** (required for challenge-based OCI username/password login flows): Absolute URL of the bearer token service advertised in `WWW-Authenticate` challenges. Set this to Zot token exchange endpoint, normally `https://<registry-host>/zot/auth/token`, when OCI clients should exchange an OIDC token password with Zot itself.
51+
52+
- **`service`** (recommended): Registry service name advertised in the bearer challenge. Use the registry host and port that clients connect to.
53+
54+
- **`upstreamTokenEndpoint.realm`** (optional; configure inside `upstreamTokenEndpoint` with `service`): HTTPS URL for an existing traditional bearer token service. Zot uses this only as a compatibility fallback when `/zot/auth/token` receives credentials that are not owned by any local token backend.
55+
56+
- **`upstreamTokenEndpoint.service`** (optional; configure inside `upstreamTokenEndpoint` with `realm`): Service value Zot sends to the upstream traditional bearer token service. Zot preserves the original token request method, headers, query parameters, and body, rewriting only `service` to this value.
57+
58+
- **`upstreamTokenEndpoint.allowInsecureHttp`** (optional; default `false`): Allows `upstreamTokenEndpoint.realm` to use plaintext HTTP. This can expose proxied credentials and should only be used in controlled test environments.
59+
5260
- **`claimMapping`** (optional): CEL-based configuration for validating and mapping OIDC claims.
5361
- **`variables`**: List of variables to extract from claims using CEL expressions
5462
- **`validations`**: List of validation rules with CEL expressions
@@ -87,6 +95,10 @@ In the example below, the username is mapped from both the issuer and subject
8795
claims to uniquely identify Kubernetes ServiceAccounts across different clusters.
8896
Note that `claims.iss + '/' + claims.sub` is the default username mapping if none
8997
is specified (so the whole `claimMapping` section could be omitted in this example).
98+
The example also advertises Zot token exchange endpoint for OCI username/password
99+
login flows. If the same bearer configuration also uses traditional bearer
100+
authentication, add `upstreamTokenEndpoint` as shown in the compatibility
101+
section so non-OIDC token requests can fall back to the external token service.
90102

91103
```json
92104
{
@@ -99,8 +111,8 @@ is specified (so the whole `claimMapping` section could be omitted in this examp
99111
"port": "8080",
100112
"auth": {
101113
"bearer": {
102-
"realm": "zot",
103-
"service": "zot-service",
114+
"realm": "https://zot.example.com/zot/auth/token",
115+
"service": "zot.example.com",
104116
"oidc": [
105117
{
106118
"issuer": "https://kubernetes.default.svc.cluster.local",
@@ -265,7 +277,7 @@ Configure multiple OIDC providers to support different identity sources. Zot wil
265277

266278
### Kubernetes ServiceAccount Tokens
267279

268-
When running in Kubernetes, workloads can use their ServiceAccount tokens to authenticate:
280+
When running in Kubernetes, workloads can use their ServiceAccount tokens directly as bearer tokens:
269281

270282
```bash
271283
# Get the ServiceAccount token
@@ -275,6 +287,72 @@ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
275287
curl -H "Authorization: Bearer $TOKEN" https://zot.example.com/v2/_catalog
276288
```
277289

290+
### OCI Username/Password Login Flow
291+
292+
Zot also supports the OCI token service flow for clients that only know how to send registry credentials as username/password pairs. When `bearer.oidc` is configured, Zot exposes `GET` and `POST` on `/zot/auth/token`. For local OIDC workload login, the submitted credential must be an OIDC JWT trusted by one of the configured `bearer.oidc` providers.
293+
294+
To make Docker, containerd, or kubelet discover Zot token exchange endpoint automatically, configure `bearer.realm` as an externally reachable URL for `/zot/auth/token` and set `bearer.service` to the registry host clients use. In mixed deployments with traditional bearer authentication, set `upstreamTokenEndpoint` so Zot can forward token requests that are not owned by local token backends to the existing traditional bearer token service. Browser `openid.providers` logins continue to use `/zot/auth/login`, not this token endpoint.
295+
296+
Example token exchange request:
297+
298+
```bash
299+
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
300+
301+
curl -u "zot:$TOKEN" \
302+
"https://zot.example.com/zot/auth/token?service=zot.example.com&scope=repository:app:pull"
303+
```
304+
305+
The response contains the same OIDC token in both `token` and `access_token`, plus `expires_in` and `issued_at` derived from the JWT claims, allowing OCI clients to retry the original registry request with `Authorization: Bearer <token>`.
306+
307+
Docker-compatible login works the same way:
308+
309+
```bash
310+
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
311+
312+
echo "$TOKEN" | docker login -u zot --password-stdin zot.example.com
313+
```
314+
315+
This requires the registry challenge to advertise `realm: "https://zot.example.com/zot/auth/token"` and a `service` value matching the registry host clients use.
316+
317+
### Kubernetes Image Pull Secrets
318+
319+
For kubelet image pulls, store a projected ServiceAccount token as the password in a `kubernetes.io/dockerconfigjson` Secret. The username can be any non-empty value.
320+
321+
```yaml
322+
apiVersion: v1
323+
kind: Secret
324+
metadata:
325+
name: zot-oidc-pull-secret
326+
namespace: workloads
327+
type: kubernetes.io/dockerconfigjson
328+
stringData:
329+
.dockerconfigjson: |
330+
{
331+
"auths": {
332+
"zot.example.com": {
333+
"username": "zot",
334+
"password": "<kubernetes-service-account-token>"
335+
}
336+
}
337+
}
338+
```
339+
340+
Attach the Secret to the Pod or ServiceAccount that pulls from Zot:
341+
342+
```yaml
343+
apiVersion: v1
344+
kind: Pod
345+
metadata:
346+
name: zot-pull-test
347+
namespace: workloads
348+
spec:
349+
imagePullSecrets:
350+
- name: zot-oidc-pull-secret
351+
containers:
352+
- name: app
353+
image: zot.example.com/app:v1
354+
```
355+
278356
### Flux Integration
279357
280358
Flux can use Kubernetes ServiceAccount tokens to authenticate to Zot without secrets:
@@ -372,15 +450,21 @@ Use Zot's access control policies to grant permissions based on the OIDC identit
372450

373451
### Traditional Bearer Authentication
374452

375-
OIDC workload identity can coexist with traditional bearer authentication. If both are configured, Zot will try OIDC authentication first, then fall back to traditional bearer token authentication:
453+
OIDC workload identity can coexist with traditional bearer authentication. If both are configured, Zot will try OIDC authentication first, then fall back to traditional bearer token authentication.
454+
455+
There is still only one `bearer.realm` value in the challenge, so mixed deployments should advertise Zot `/zot/auth/token` and configure the existing traditional bearer token service as a proxy fallback. Zot first checks whether the submitted credential is owned by a local token backend such as `bearer.oidc` or `openid.providers`. Locally owned credentials are never proxied: they either authenticate locally or fail with 401. If no local backend owns the credential, Zot proxies the token request to `upstreamTokenEndpoint.realm`, preserving the request shape and rewriting `service` to `upstreamTokenEndpoint.service`:
376456

377457
```json
378458
{
379459
"http": {
380460
"auth": {
381461
"bearer": {
382-
"realm": "https://auth.myreg.io/auth/token",
383-
"service": "myauth",
462+
"realm": "https://zot.example.com/zot/auth/token",
463+
"service": "zot.example.com",
464+
"upstreamTokenEndpoint": {
465+
"realm": "https://auth.myreg.io/auth/token",
466+
"service": "myauth"
467+
},
384468
"cert": "/etc/zot/auth.crt",
385469
"oidc": [
386470
{

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,8 @@ NOTE: The separate file for storing DN and password credentials must be created.
375375
}
376376
```
377377

378+
When OIDC workload identity/federation uses Zot `/zot/auth/token` but the same deployment still needs this traditional bearer token service, configure the optional `upstreamTokenEndpoint` object. `upstreamTokenEndpoint.realm` points to the existing traditional bearer token service and `upstreamTokenEndpoint.service` is the upstream service value; Zot preserves the token request and rewrites only `service` before proxying requests that are not owned by local token backends. `upstreamTokenEndpoint.realm` must use HTTPS by default; plaintext HTTP requires `upstreamTokenEndpoint.allowInsecureHttp: true` and should only be used in controlled test environments.
379+
378380
### OpenID/OAuth2 social login
379381

380382
zot supports several openID/OAuth2 providers:

0 commit comments

Comments
 (0)