Skip to main content

Zot Registry Keycloak SSO Integration

This guide covers integrating Keycloak Single Sign-On into the Zot OCI registry deployed on Kubernetes via the project-zot Helm chart, using ArgoCD for GitOps and AWS Secrets Manager (via External Secrets Operator) for credential management.

Reference: Zot OpenID/OAuth2 social login


Prerequisites

  • Access to the Keycloak admin console for the target realm
  • Permission to create secrets in AWS Secrets Manager
  • Write access to the sparqd-gitops-master GitOps repository
  • Zot is deployed on an Amazon EKS cluster with a public URL behind an ALB
  • External Secrets Operator is already installed in the cluster
  • A Kubernetes namespace zot with an IAM-bound ServiceAccount that can read secrets from AWS Secrets Manager

1. Register the Zot Client in Keycloak

Keycloak treats Zot as a confidential OIDC client.

Steps

  1. Open the Keycloak admin console and switch to the target realm (e.g. quant-data).

  2. Navigate to ClientsCreate client.

  3. General settings:

    FieldValue
    Client typeOpenID Connect
    Client IDzot
  4. Capability config:

    FieldValue
    Client authenticationOn (confidential client)
    AuthorizationOff
    Standard flowEnabled
    Direct access grantsDisabled
  5. Login settings:

    FieldValue
    Root URLhttps://registry-dev.cogrion.com
    Valid redirect URIshttps://registry-dev.cogrion.com/zot/auth/callback/oidc
    Valid post logout redirect URIshttps://registry-dev.cogrion.com/*
    Web originshttps://registry-dev.cogrion.com
  6. Click Save.

  7. Open the Credentials tab and copy the generated Client secret. This is needed in the next step.

Note: Zot constructs callback URLs using a /zot/auth/callback/<provider> prefix, where <provider> matches the provider key in the Zot config. Mismatched URIs cause Invalid parameter: redirect_uri at the Keycloak step.


2. Store the Client Secret in AWS Secrets Manager

The client secret is mounted into the Zot pod as a file. Zot expects a single JSON document containing both clientid and clientsecret.

  1. Open AWS Secrets Manager in the cluster's region (e.g. ap-southeast-1).
  2. Click Store a new secretOther type of secret.
  3. Use the Plaintext tab and paste a single JSON value:
{"clientid":"zot","clientsecret":"<paste-from-keycloak>"}
  1. Secret name: core-platform/zot/oidc-client-secret
  2. Skip rotation. Save.

Note: Storing the value as a single JSON blob — instead of two key/value pairs — lets Kubernetes mount it directly as a file without any post-processing.


3. Sync the Secret into the Cluster

External Secrets Operator pulls the value from AWS Secrets Manager and creates a Kubernetes secret in the zot namespace.

SecretStore

In core-platform-argocd/infra/deployments/config/external-secret/values/dev.yaml, add a SecretStore for the zot namespace under secretStores:

- name: aws-secretsmanager
type: SecretStore
provider: aws
region: ap-southeast-1
role: arn:aws:iam::<account-id>:role/core-platform-external-secrets-role
credentialsSecretNamespace: zot
serviceAccountName: zot-sa

ExternalSecret

In the same file, append a new ExternalSecret entry under externalSecrets:

- name: zot-oidc-client-secret
namespace: zot
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: zot-oidc-client-secret
creationPolicy: Owner
data:
- secretKey: credentials.json
remoteRef:
key: core-platform/zot/oidc-client-secret
property: credentials.json

After ArgoCD syncs the external-secrets application, a Kubernetes secret zot-oidc-client-secret is created in the zot namespace with a single key credentials.json.


4. Update the Zot Helm Values

In core-platform-argocd/infra/deployments/core/zot/values/dev.yaml, two changes are required.

Mount the Credentials Secret

Add the following section, e.g. after resources:

extraVolumes:
- name: zot-oidc-credentials
secret:
secretName: zot-oidc-client-secret

extraVolumeMounts:
- name: zot-oidc-credentials
mountPath: /secret-oidc
readOnly: true

This mounts the secret as a file at /secret-oidc/credentials.json inside the Zot container.

Enable OIDC in the Zot Config

Update the http block inside configFiles.config.json:

"http": {
"address": "0.0.0.0",
"port": "5000",
"realm": "openid",
"externalUrl": "https://registry-dev.cogrion.com",
"auth": {
"htpasswd": {
"path": "/secret/htpasswd"
},
"failDelay": 5,
"openid": {
"providers": {
"oidc": {
"name": "Keycloak SSO",
"issuer": "https://sso.dev.sparq-qd.com/realms/quant-data",
"credentialsFile": "/secret-oidc/credentials.json",
"keypath": "",
"scopes": ["openid", "profile", "email"]
}
}
}
}
}

Key fields:

FieldPurpose
realmSet to openid to enable the OIDC realm endpoint
externalUrlRequired when Zot runs behind an ALB. Used to build the OAuth callback URL
credentialsFilePath to the JSON file containing clientid and clientsecret
issuerKeycloak realm URL — Zot auto-discovers OAuth endpoints from <issuer>/.well-known/openid-configuration

htpasswd is retained as a break-glass admin login alongside SSO.

Note: Adding the groups scope only works if a groups client scope is configured in Keycloak and assigned to the client. Omit it unless group-based authorization is needed.


5. Apply via ArgoCD

Commit and push the changes to the branch the Zot ArgoCD application watches (typically main):

git add infra/deployments/config/external-secret/values/dev.yaml \
infra/deployments/core/zot/values/dev.yaml
git commit -m "feat(zot): enable Keycloak SSO via OIDC"
git push

Sync these ArgoCD applications, in order:

  1. external-secrets — creates the SecretStore and ExternalSecret
  2. zot — picks up the new helm values and restarts the pod

Important: Verify that the zot ArgoCD Application's targetRevision points at the branch you pushed. A stale targetRevision is a common cause of "synced but no change applied" symptoms.


6. Verify the Integration

After the Zot pod restarts cleanly:

# Secret exists with one key
kubectl get secret zot-oidc-client-secret -n zot
# DATA should be 1

# Configmap reflects the new OIDC block
kubectl get configmap zot-config -n zot -o jsonpath='{.data.config\.json}' | grep -i openid

# Pod is running
kubectl get pods -n zot

Open the Zot UI at https://registry-dev.cogrion.com. The login screen should display a Sign in with Keycloak SSO button alongside the htpasswd login fields. Clicking it should redirect to Keycloak, prompt for credentials, and redirect back into Zot as an authenticated user.


Troubleshooting

ProblemLikely CauseFix
Invalid parameter: redirect_uri at KeycloakRedirect URI in Keycloak client doesn't match what Zot sentRegister the exact URI Zot uses, including the /zot/ prefix
invalid_scope: Invalid scopes: ... groupsThe groups scope isn't defined or assigned in KeycloakRemove groups from scopes in the Zot config, or create and assign the scope in Keycloak
Configmap doesn't contain the new OIDC block after syncArgoCD targetRevision points at a stale branchUpdate the application's targetRevision to the branch with the latest changes
ExternalSecret is Ready=FalseIAM role can't be assumed by the namespace's ServiceAccountVerify the role's trust policy allows system:serviceaccount:zot:zot-sa
Pod is Running but /secret-oidc/credentials.json is missingThe chart's extraVolumes didn't take effectConfirm the chart version honors extraVolumes and that ArgoCD reconciled the StatefulSet

Security Considerations

  • Never commit clientsecret values to git. The secret only exists in AWS Secrets Manager and is mounted into the pod at runtime.
  • Restrict the IAM role used by External Secrets Operator to read access on the core-platform/zot/* path only.
  • Keep htpasswd as a break-glass mechanism only — rotate the bcrypt hash regularly and avoid sharing the credential.
  • For machine-to-machine access (CI, docker push), use Zot API keys generated by individual users rather than the htpasswd account.
  • Use a separate Keycloak client (e.g. zot-prod) and a separate AWS Secrets Manager secret for each environment.