Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
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"
- Example:
-
audiences(required): List of acceptable audiences for the OIDC token. At least one must be specified.- Example:
["zot", "https://zot.example.com"]
- Example:
-
claimMapping(optional): CEL-based configuration for validating and mapping OIDC claims.variables: List of variables to extract from claims using CEL expressionsvalidations: List of validation rules with CEL expressionsusername: 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 withcertificateAuthorityFile. -
certificateAuthorityFile(optional): Path to a PEM-encoded CA certificate file to validate the OIDC provider's TLS certificate. Mutually exclusive withcertificateAuthority. -
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 timeiat: Issued at time
Optional Claims
groups: Array of group names for authorization (requires CELgroupsexpression)preferred_username: Can be used as username with CEL expressionemail: Can be used as username with CEL expressionname: 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
-
Token verification failed: Check that the issuer URL is correct and reachable from Zot.
-
Audience not accepted: Ensure the token's
audclaim matches one of the configured audiences. -
Token expired: OIDC tokens are typically short-lived. Ensure your workload is obtaining fresh tokens.
-
CEL expression error: Check the CEL expression syntax. Use
claims.fieldfor simple fields orclaims['field-name']for fields with special characters. -
Validation failed: Check that your token claims satisfy all configured validation expressions.
-
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
certificateAuthorityorcertificateAuthorityFilefor the corresponding OIDC provider. -
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
-
Token Expiration: Always use short-lived tokens (typically 1 hour or less).
-
Audience Validation: Always specify audiences to prevent token reuse across services.
-
TLS: Use TLS for all communication to protect tokens in transit.
-
Issuer Verification: Never disable issuer verification in production.
-
Access Control: Always configure access control policies to limit what authenticated workloads can do.
-
CEL Validations: Use CEL validations to enforce additional security constraints (e.g., require email verification, restrict to specific organizations).