Setting up Zot on Kubernetes with S3 Storage and OIDC
I wanted a light weight container registry that could run on Kubernetes, store images in S3-compatible storage.
Zot ended up being a good fit for this setup. It supports S3 storage, has a built-in web UI, works with OpenID Connect, and can still use htpasswd for Docker CLI logins.
That last part matters because Docker does not handle browser-based OIDC login flows.
The web UI can use Authentik, while docker login can use a normal username and password from an htpasswd file.
This post walks through the setup I used with Kubernetes, Helm, S3 , and Authentik.
Requirements
You will need:
- A Kubernetes cluster
- Helm
- S3-compatible storage, such as RustFS, MinIO, or AWS S3
- A Bucket already created for Zot
Step 1: Set Up the Access Policy for Bucket
The policy needs to allow:
- Reading, writing, and deleting objects
- Multipart uploads (this is critical for Docker pushes)
Here's the policy I used:
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "AllowBucketAccess",
6 "Effect": "Allow",
7 "Action": [
8 "s3:ListBucket",
9 "s3:GetBucketLocation",
10 "s3:ListBucketMultipartUploads"
11 ],
12 "Resource": ["arn:aws:s3:::zot-registry"]
13 },
14 {
15 "Sid": "AllowObjectAccess",
16 "Effect": "Allow",
17 "Action": [
18 "s3:PutObject",
19 "s3:GetObject",
20 "s3:DeleteObject",
21 "s3:ListMultipartUploadParts",
22 "s3:AbortMultipartUpload"
23 ],
24 "Resource": ["arn:aws:s3:::zot-registry/*"]
25 }
26 ]
27}
Step 2: Configure Authentik
In Authentik, create a new application and provider.
Use:
1OAuth2/OpenID Connect
For the redirect URI, use your Zot public URL with the OIDC callback path:
1https://registry.homelab.pk/zot/auth/callback/oidc
Set this as a strict redirect URI.
From the Authentik provider page, copy the following values:
- Client ID
- Client Secret
- Issuer URL
You will use these later in the Zot configuration.
Step 3: Create the required Kubernetes secrets
This setup uses three secrets:
- S3 access credentials
- OIDC client credentials
- An
htpasswdfile for Docker CLI login
For Docker CLI access, generate an htpasswd file locally:
1htpasswd -Bbn youruser 'yourpassword' > htpasswd
Zot expects the OIDC credentials as a small JSON file in this format:
1{
2 "clientid": "YOUR_CLIENT_ID",
3 "clientsecret": "YOUR_CLIENT_SECRET"
4}
I use External Secrets, so the secrets are created before Zot starts.
1apiVersion: external-secrets.io/v1
2kind: ExternalSecret
3metadata:
4 name: zot-oidc-creds
5 namespace: zot
6 annotations:
7 argocd.argoproj.io/sync-wave: "-1"
8 argocd.argoproj.io/hook: PreSync
9spec:
10 secretStoreRef:
11 name: bitwarden-cluster-secretsmanager
12 kind: ClusterSecretStore
13 refreshInterval: "24h"
14 target:
15 name: zot-oidc-creds
16 template:
17 engineVersion: v2
18 data:
19 oidc-credentials.json: |
20 {
21 "clientid": "{{ .client_id }}",
22 "clientsecret": "{{ .client_secret }}"
23 }
24 data:
25 - secretKey: client_id
26 remoteRef:
27 key: zot-oidc-client-id
28 - secretKey: client_secret
29 remoteRef:
30 key: zot-oidc-client-secret
31---
32apiVersion: external-secrets.io/v1
33kind: ExternalSecret
34metadata:
35 name: zot-htpasswd
36 namespace: zot
37 annotations:
38 argocd.argoproj.io/sync-wave: "-1"
39 argocd.argoproj.io/hook: PreSync
40spec:
41 secretStoreRef:
42 name: bitwarden-cluster-secretsmanager
43 kind: ClusterSecretStore
44 refreshInterval: "24h"
45 target:
46 name: zot-htpasswd
47 template:
48 engineVersion: v2
49 data:
50 htpasswd: '{{ .htpasswd }}'
51 data:
52 - secretKey: htpasswd
53 remoteRef:
54 key: zot-htpasswd
55---
56apiVersion: external-secrets.io/v1
57kind: ExternalSecret
58metadata:
59 name: zot-rustfs-creds
60 namespace: zot
61 annotations:
62 argocd.argoproj.io/sync-wave: "-1"
63 argocd.argoproj.io/hook: PreSync
64spec:
65 secretStoreRef:
66 name: bitwarden-cluster-secretsmanager
67 kind: ClusterSecretStore
68 refreshInterval: "24h"
69 target:
70 name: zot-rustfs-creds
71 data:
72 - secretKey: accesskey
73 remoteRef:
74 key: zot-rustfs-access
75 - secretKey: secretkey
76 remoteRef:
77 key: zot-rustfs-secret
Adjust the secretStoreRef and remoteRef values to match your own secret backend.
Step 4: Configure Zot with Helm
Here is the complete values.yaml used for the deployment.
1replicaCount: 1
2
3image:
4 repository: ghcr.io/project-zot/zot-linux-amd64
5 pullPolicy: IfNotPresent
6 tag: "v2.1.17"
7
8namespace: ""
9
10serviceAccount:
11 create: true
12 name: ""
13
14service:
15 type: ClusterIP
16 port: 5000
17 nodePort: null
18 annotations: {}
19 clusterIP: null
20
21env:
22 - name: AWS_ACCESS_KEY_ID
23 valueFrom:
24 secretKeyRef:
25 name: zot-rustfs-creds
26 key: accesskey
27 - name: AWS_SECRET_ACCESS_KEY
28 valueFrom:
29 secretKeyRef:
30 name: zot-rustfs-creds
31 key: secretkey
32
33externalSecrets:
34 - secretName: "zot-oidc-creds"
35 mountPath: "/etc/zot/oidc"
36 - secretName: "zot-htpasswd"
37 mountPath: "/etc/zot/htpasswd"
38
39mountConfig: true
40
41configFiles:
42 config.json: |-
43 {
44 "storage": {
45 "rootDirectory": "/var/lib/registry",
46 "dedupe": false,
47 "storageDriver": {
48 "name": "s3",
49 "region": "us-east-1",
50 "regionendpoint": "http://192.168.0.20:9000",
51 "bucket": "zot-registry",
52 "secure": false,
53 "skipverify": false,
54 "forcepathstyle": true
55 }
56 },
57 "http": {
58 "address": "0.0.0.0",
59 "port": "5000",
60 "readTimeout": "60s",
61 "writeTimeout": "60s",
62 "externalUrl": "https://registry.homelab.pk",
63 "compat": ["docker2s2"],
64 "auth": {
65 "htpasswd": {
66 "path": "/etc/zot/htpasswd/htpasswd"
67 },
68 "openid": {
69 "providers": {
70 "oidc": {
71 "name": "Authentik",
72 "credentialsFile": "/etc/zot/oidc/oidc-credentials.json",
73 "issuer": "https://auth.homelab.pk/application/o/zot/",
74 "keypath": "",
75 "scopes": ["openid", "profile", "email", "groups"]
76 }
77 }
78 }
79 }
80 },
81 "extensions": {
82 "search": {
83 "enable": true
84 },
85 "ui": {
86 "enable": true
87 }
88 },
89 "log": {
90 "level": "info"
91 }
92 }
93
94strategy:
95 type: RollingUpdate
Replace these values with your own:
1regionendpoint: "http://192.168.0.20:9000"
2bucket: "zot-registry"
3externalUrl: "https://registry.homelab.pk"
4issuer: "https://auth.homelab.pk/application/o/zot/"
The issuer value should match the issuer URL shown in your Authentik provider.
Notes on the Zot configuration
A few settings are worth calling out.
externalUrl
This must match the public URL you use for the registry.
For example:
1"externalUrl": "https://registry.homelab.pk"
If this does not match the URL used by Docker, pushes can fail with confusing manifest errors.
compat: ["docker2s2"]
Keep this enabled:
1"compat": ["docker2s2"]
Without it, Docker may upload the blobs but fail when pushing the final manifest. A common error is HTTP 415.
dedupe: false
Zot supports deduplication, but with S3 it requires Redis.
For a single-replica homelab deployment, disabling it keeps the setup simpler:
1"dedupe": false
rootDirectory
Even though image layers are stored in S3, Zot still needs a local root directory:
1"rootDirectory": "/var/lib/registry"
Zot uses this for local metadata and runtime data.
forcepathstyle: true
Use this for self-hosted S3-compatible storage such as RustFS or MinIO:
1"forcepathstyle": true
For AWS S3, you may not need it.
Secret mount paths
The mounted secret paths must match the paths in config.json.
In this example:
1externalSecrets:
2 - secretName: "zot-oidc-creds"
3 mountPath: "/etc/zot/oidc"
4 - secretName: "zot-htpasswd"
5 mountPath: "/etc/zot/htpasswd"
And in the Zot config:
1"credentialsFile": "/etc/zot/oidc/oidc-credentials.json"
1"path": "/etc/zot/htpasswd/htpasswd"
If the file names or paths do not match, authentication will fail.
Step 5: Deploy Zot
Deploy Zot with Helm:
1helm upgrade --install zot project-zot/zot \
2 --namespace zot \
3 --create-namespace \
4 -f values.yaml
Wait for the deployment to become ready:
1kubectl -n zot rollout status deployment/zot-zot
Step 6: Check the mounted files
Once the pod is running, check that the secrets were mounted correctly:
1kubectl exec -it -n zot deploy/zot-zot -- sh
Then inside the pod:
1ls -l /etc/zot/oidc
2ls -l /etc/zot/htpasswd
You should see:
1oidc-credentials.json
2htpasswd
If either file is missing, check your ExternalSecret configuration before troubleshooting Zot itself.
Step 7: Test the Authentik login
Open the registry URL in your browser:
1https://registry.homelab.pk
You should see a sign-in option for OIDC.
Click it, log in through Authentik, and you should be redirected back to Zot as an authenticated user.
If this fails, check:
- The
externalUrlin Zot - The redirect URI in Authentik
- The Authentik issuer URL
- The mounted OIDC credentials file
- The Client ID and Client Secret
Step 8: Test Docker CLI access
Now test Docker login and push:
1docker login registry.homelab.pk
Use the username and password from your htpasswd file.
Then push a test image:
1docker pull alpine:3.21.3
2docker tag alpine:3.21.3 registry.homelab.pk/alpine:3.21.3
3docker push registry.homelab.pk/alpine:3.21.3
If the push succeeds, Zot is working with S3 storage and Docker CLI authentication.
Troubleshooting
NoSuchBucket
The S3 bucket does not exist.
Create the bucket first, then restart Zot.
Access Denied during large uploads
Your S3 policy may be missing multipart upload permissions.
Check that the policy allows actions such as:
1ListMultipartUploadParts
2AbortMultipartUpload
Manifest errors during Docker push
Check these two settings:
1"externalUrl": "https://registry.homelab.pk"
1"compat": ["docker2s2"]
A missing docker2s2 compatibility setting can cause the blobs to upload successfully, then fail when Docker pushes the manifest.
Web UI login works, but docker login fails
OIDC and Docker CLI authentication are separate in this setup.
If the web UI works but Docker login fails, check:
- The
htpasswdfile - The mounted secret path
- The password hash format
- The path configured in Zot:
1"path": "/etc/zot/htpasswd/htpasswd"
Final notes
The main things to get right are the Authentik redirect URI, the Zot externalUrl, the S3 bucket and permissions, and the compat: ["docker2s2"] setting.
Once those are in place, Zot works well as a container registry with S3 storage, Authentik login for the web UI, and normal docker login support for CLI access.