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-masterGitOps 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
zotwith 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
-
Open the Keycloak admin console and switch to the target realm (e.g.
quant-data). -
Navigate to Clients → Create client.
-
General settings:
Field Value Client type OpenID Connect Client ID zot -
Capability config:
Field Value Client authentication On (confidential client) Authorization Off Standard flow Enabled Direct access grants Disabled -
Login settings:
Field Value Root URL https://registry-dev.cogrion.comValid redirect URIs https://registry-dev.cogrion.com/zot/auth/callback/oidcValid post logout redirect URIs https://registry-dev.cogrion.com/*Web origins https://registry-dev.cogrion.com -
Click Save.
-
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 causeInvalid parameter: redirect_uriat 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.
- Open AWS Secrets Manager in the cluster's region (e.g.
ap-southeast-1). - Click Store a new secret → Other type of secret.
- Use the Plaintext tab and paste a single JSON value:
{"clientid":"zot","clientsecret":"<paste-from-keycloak>"}
- Secret name:
core-platform/zot/oidc-client-secret - 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:
| Field | Purpose |
|---|---|
realm | Set to openid to enable the OIDC realm endpoint |
externalUrl | Required when Zot runs behind an ALB. Used to build the OAuth callback URL |
credentialsFile | Path to the JSON file containing clientid and clientsecret |
issuer | Keycloak 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
groupsscope only works if agroupsclient 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:
external-secrets— creates the SecretStore and ExternalSecretzot— picks up the new helm values and restarts the pod
Important: Verify that the
zotArgoCD Application'stargetRevisionpoints at the branch you pushed. A staletargetRevisionis 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
| Problem | Likely Cause | Fix |
|---|---|---|
Invalid parameter: redirect_uri at Keycloak | Redirect URI in Keycloak client doesn't match what Zot sent | Register the exact URI Zot uses, including the /zot/ prefix |
invalid_scope: Invalid scopes: ... groups | The groups scope isn't defined or assigned in Keycloak | Remove groups from scopes in the Zot config, or create and assign the scope in Keycloak |
| Configmap doesn't contain the new OIDC block after sync | ArgoCD targetRevision points at a stale branch | Update the application's targetRevision to the branch with the latest changes |
ExternalSecret is Ready=False | IAM role can't be assumed by the namespace's ServiceAccount | Verify the role's trust policy allows system:serviceaccount:zot:zot-sa |
Pod is Running but /secret-oidc/credentials.json is missing | The chart's extraVolumes didn't take effect | Confirm the chart version honors extraVolumes and that ArgoCD reconciled the StatefulSet |
Security Considerations
- Never commit
clientsecretvalues 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
htpasswdas 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.