Files
zot/examples/README-OIDC-WORKLOAD-IDENTITY.md
Matheus Pimenta c8fae88e37 feat(oidc): support per-issuer CA (#3760)
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
2026-02-01 23:57:27 +02:00

14 KiB

OIDC Workload Identity Authentication

This document describes how to configure Zot to authenticate workloads using OIDC ID tokens, enabling secret-less authentication for automated workflows.

Overview

OIDC Workload Identity authentication allows workloads (e.g., Kubernetes pods, CI/CD pipelines) to authenticate to Zot using OIDC ID tokens instead of static credentials. This is similar to how cloud providers implement "workload identity" features and how Kubernetes handles external OIDC authentication.

Benefits

  • Secret-less Authentication: No need to manage static credentials
  • Automatic Credential Rotation: Tokens are short-lived and automatically rotated
  • Fine-grained Access Control: Map OIDC claims to Zot identities and groups using CEL expressions
  • Kubernetes Native: Works seamlessly with Kubernetes ServiceAccount tokens
  • Multi-Provider Support: Configure multiple OIDC issuers for different workload types (e.g. multiple clusters)
  • Standards-based: Uses standard OIDC protocols

Configuration

Basic Configuration

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

{
  "http": {
    "auth": {
      "bearer": {
        "realm": "zot",
        "service": "zot-service",
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot"]
          }
        ]
      }
    }
  }
}

Configuration Options

  • issuer (required): The OIDC issuer URL. This is the identity provider that signs the tokens.

    • Example: "https://kubernetes.default.svc.cluster.local"
    • Example: "https://token.actions.githubusercontent.com"
  • audiences (required): List of acceptable audiences for the OIDC token. At least one must be specified.

    • Example: ["zot", "https://zot.example.com"]
  • claimMapping (optional): CEL-based configuration for validating and mapping OIDC claims.

    • variables: List of variables to extract from claims using CEL expressions
    • validations: List of validation rules with CEL expressions
    • username: CEL expression to extract the username. Default: "claims.iss + '/' + claims.sub"
    • groups: CEL expression to extract groups. Default: none (no groups extracted)
  • certificateAuthority (optional): PEM-encoded CA certificate to validate the OIDC provider's TLS certificate. Useful when the OIDC issuer uses a private CA (e.g., Kubernetes API server with a self-signed certificate). Mutually exclusive with certificateAuthorityFile.

  • certificateAuthorityFile (optional): Path to a PEM-encoded CA certificate file to validate the OIDC provider's TLS certificate. Mutually exclusive with certificateAuthority.

  • skipIssuerVerification (optional): Skip issuer verification (for testing only). Default: false.

CEL Expressions

Zot uses Common Expression Language (CEL) for flexible claim validation and mapping. CEL expressions have access to:

  • claims: The OIDC token claims as a map (e.g., claims.sub, claims.email)
  • vars: Previously extracted variables (for use in validations and username/groups expressions)

Example CEL Expressions

Expression Description
claims.sub Extract the subject claim
claims.email Extract the email claim
claims.groups Extract the groups claim
claims['kubernetes.io/serviceaccount/namespace'] Extract claims with special characters
claims.repository_owner + '/' + claims.sub Concatenate multiple claims
claims.email.split('@')[0] Extract username from email
claims.org in ['allowed-org-1', 'allowed-org-2'] Check if org is in allowed list
claims.email_verified == true Validate email is verified

Complete Example

In the example below, the username is mapped from both the issuer and subject claims to uniquely identify Kubernetes ServiceAccounts across different clusters. Note that claims.iss + '/' + claims.sub is the default username mapping if none is specified (so the whole claimMapping section could be omitted in this example).

{
  "distSpecVersion": "1.1.1",
  "storage": {
    "rootDirectory": "/tmp/zot"
  },
  "http": {
    "address": "127.0.0.1",
    "port": "8080",
    "auth": {
      "bearer": {
        "realm": "zot",
        "service": "zot-service",
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot", "https://zot.example.com"],
            "claimMapping": {
              "username": "claims.iss + '/' + claims.sub"
            }
          }
        ]
      }
    },
    "accessControl": {
      "repositories": {
        "**": {
          "policies": [
            {
              "users": ["https://kubernetes.default.svc.cluster.local/system:serviceaccount:flux-system:source-controller"],
              "actions": ["read", "create", "update", "delete"]
            }
          ]
        }
      }
    }
  },
  "log": {
    "level": "info"
  }
}

Configuration with Custom CA

When the OIDC issuer uses a private CA (e.g., Kubernetes API server), you can configure the CA certificate inline or via a file path:

{
  "http": {
    "auth": {
      "bearer": {
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot"],
            "certificateAuthorityFile": "/etc/zot/k8s-ca.pem"
          }
        ]
      }
    }
  }
}

Alternatively, you can embed the CA certificate directly using certificateAuthority:

{
  "http": {
    "auth": {
      "bearer": {
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot"],
            "certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----"
          }
        ]
      }
    }
  }
}

Advanced Configuration with CEL Validations

Use CEL expressions to validate claims and extract complex usernames:

{
  "http": {
    "auth": {
      "bearer": {
        "oidc": [
          {
            "issuer": "https://token.actions.githubusercontent.com",
            "audiences": ["zot"],
            "claimMapping": {
              "variables": [
                {
                  "name": "repo",
                  "expression": "claims.repository"
                },
                {
                  "name": "owner",
                  "expression": "claims.repository_owner"
                }
              ],
              "validations": [
                {
                  "expression": "vars.owner == 'my-org'",
                  "message": "only my-org repositories are allowed"
                },
                {
                  "expression": "claims.ref.startsWith('refs/heads/')",
                  "message": "must be a branch reference"
                }
              ],
              "username": "vars.repo",
              "groups": "['github-actions', 'ci']"
            }
          }
        ]
      }
    }
  }
}

Multiple OIDC Providers

Configure multiple OIDC providers to support different identity sources. Zot will try each provider in order until one successfully authenticates the token:

{
  "http": {
    "auth": {
      "bearer": {
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot"],
            "claimMapping": {
              "variables": [
                {
                  "name": "ns",
                  "expression": "claims['kubernetes.io/serviceaccount/namespace']"
                },
                {
                  "name": "sa",
                  "expression": "claims['kubernetes.io/serviceaccount/service-account.name']"
                }
              ],
              "username": "vars.ns + ':' + vars.sa",
              "groups": "['k8s-workloads']"
            }
          },
          {
            "issuer": "https://token.actions.githubusercontent.com",
            "audiences": ["zot"],
            "claimMapping": {
              "username": "claims.repository",
              "groups": "['github-actions']"
            }
          }
        ]
      }
    }
  }
}

Usage

Kubernetes ServiceAccount Tokens

When running in Kubernetes, workloads can use their ServiceAccount tokens to authenticate:

# Get the ServiceAccount token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# Use it to authenticate to Zot
curl -H "Authorization: Bearer $TOKEN" https://zot.example.com/v2/_catalog

Flux Integration

Flux can use Kubernetes ServiceAccount tokens to authenticate to Zot without secrets:

apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
  name: zot-repo
  namespace: flux-system
spec:
  url: oci://zot.example.com/v2/manifests
  credentials: ServiceAccountToken
  serviceAccountName: my-tenant-sa # optional. if omitted, defaults to the source-controller ServiceAccount

Note: The configuration above is currently a proposal from the Flux maintainers and may change until officially released. For more details, see this RFC.

GitHub Actions

GitHub Actions can use OIDC tokens to authenticate:

- name: Login to Zot
  run: |
    TOKEN=$(curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=zot" | jq -r .value)
    echo $TOKEN | docker login -u oauth --password-stdin zot.example.com

Token Claims

Required Claims

  • iss: Issuer URL (must match configured issuer)
  • aud: Audience (must match one of the configured audiences)
  • sub: Subject (used as username by default)
  • exp: Expiration time
  • iat: Issued at time

Optional Claims

  • groups: Array of group names for authorization (requires CEL groups expression)
  • preferred_username: Can be used as username with CEL expression
  • email: Can be used as username with CEL expression
  • name: Can be used as username with CEL expression

Example Token Payload

{
  "iss": "https://kubernetes.default.svc.cluster.local",
  "aud": ["zot"],
  "sub": "system:serviceaccount:flux-system:source-controller",
  "exp": 1705258800,
  "iat": 1705255200,
  "kubernetes.io/serviceaccount/namespace": "flux-system",
  "kubernetes.io/serviceaccount/service-account.name": "source-controller"
}

Access Control

Use Zot's access control policies to grant permissions based on the OIDC identity:

{
  "accessControl": {
    "repositories": {
      "app/**": {
        "policies": [
          {
            "users": ["system:serviceaccount:prod:app-controller"],
            "actions": ["read", "create", "update"]
          }
        ]
      },
      "**": {
        "policies": [
          {
            "users": ["system:serviceaccount:default:admin"],
            "actions": ["read", "create", "update", "delete"]
          }
        ],
        "defaultPolicy": ["read"]
      }
    }
  }
}

Compatibility

Traditional Bearer Authentication

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:

{
  "http": {
    "auth": {
      "bearer": {
        "realm": "https://auth.myreg.io/auth/token",
        "service": "myauth",
        "cert": "/etc/zot/auth.crt",
        "oidc": [
          {
            "issuer": "https://kubernetes.default.svc.cluster.local",
            "audiences": ["zot"]
          }
        ]
      }
    }
  }
}

Other Authentication Methods

OIDC workload identity is only available with bearer authentication. For other authentication methods (htpasswd, LDAP, OAuth2 for humans), continue using the existing configuration options.

Troubleshooting

Enable Debug Logging

Set log level to debug to see detailed authentication logs:

{
  "log": {
    "level": "debug"
  }
}

Common Issues

  1. Token verification failed: Check that the issuer URL is correct and reachable from Zot.

  2. Audience not accepted: Ensure the token's aud claim matches one of the configured audiences.

  3. Token expired: OIDC tokens are typically short-lived. Ensure your workload is obtaining fresh tokens.

  4. CEL expression error: Check the CEL expression syntax. Use claims.field for simple fields or claims['field-name'] for fields with special characters.

  5. Validation failed: Check that your token claims satisfy all configured validation expressions.

  6. JWKS endpoint not reachable: Verify network connectivity to the OIDC issuer's JWKS endpoint. Note: Zot lazily initializes the OIDC provider on first authentication, so startup won't fail if the issuer is temporarily unreachable. If the issuer uses a private CA, configure certificateAuthority or certificateAuthorityFile for the corresponding OIDC provider.

  7. No username found: Ensure the CEL expression for username evaluates to a non-empty string. Check that the required claims exist in the token.

Security Considerations

  1. Token Expiration: Always use short-lived tokens (typically 1 hour or less).

  2. Audience Validation: Always specify audiences to prevent token reuse across services.

  3. TLS: Use TLS for all communication to protect tokens in transit.

  4. Issuer Verification: Never disable issuer verification in production.

  5. Access Control: Always configure access control policies to limit what authenticated workloads can do.

  6. CEL Validations: Use CEL validations to enforce additional security constraints (e.g., require email verification, restrict to specific organizations).

References