Zot

Private container registry that scans images for known vulnerabilities

Zot is the private OCI registry for every image built in-repo. Project builds push multi-arch images here, and ArgoCD-managed apps pull from it. It also scans images for known vulnerabilities.

ArgoCD configuration

Excerpt from argocd-apps/values.yaml in the argocd-apps chart, with annotations added for this site:

zot:
  # true
  application: true
  project: cluster-services
  # false
  autoSync: false
#redis:
#  application: true
#  project: cluster-services
#  autoSync: false

Chart values

The full zot/values.yaml from the service's own chart:

zot:
  replicaCount: 1
  image:
    repository: ghcr.io/project-zot/zot
    pullPolicy: IfNotPresent
    tag: "v2.1.20"

  serviceAccount:
    create: true
    name: "zot-service-account"

  serviceHeadless:
    enabled: false
    port: 5000
  service:
    type: ClusterIP
    port: 5000

  ingress:
    enabled: false

  httproute:
    enabled: true
    annotations:
      link.argocd.argoproj.io/external-link: "https://registry.opi5cluster.co.uk"
    parentRefs:
      - name: istio-gateway
        namespace: istio
        sectionName: websecure
    hostnames:
      - registry.opi5cluster.co.uk
    pathType: PathPrefix
    path: /
    rules:
      - backendRefs:
          - name: zot
            port: 5000
        timeouts:
          request: 0s
          backendRequest: 0s

  httpGet:
    scheme: HTTP
    port: 5000
  startupProbe:
    initialDelaySeconds: 20
    periodSeconds: 60
    failureThreshold: 10

  # If mountConfig is true the configMap named $CHART_RELEASE-config is mounted
  # on the pod's '/etc/zot' directory
  mountConfig: true
  configFiles:
    config.json: |-
      {
        "storage": {
          "rootDirectory": "/var/lib/registry",
          "gc": true
        },
        "log": { "level": "debug" },
        "http": {
            "address": "0.0.0.0",
            "port": "5000",
            "compat": ["docker2s2"]
        },
        "extensions": {
          "ui": {
            "enable": true
          },
          "sync": {
              "enable": true,
              "credentialsFile": "/secrets/credentials.json",
              "registries": [
                  {
                      "urls": ["https://registry.gitlab.com"],
                      "content": [
                        {
                          "prefix": "opi5-cluster/**",
                          "destination": "/",
                          "stripPrefix": true
                        }
                      ],
                      "onDemand": true,
                      "tlsVerify": true
                  },
                  {
                      "urls": ["https://index.docker.io"],
                      "content": [
                        {
                          "prefix": "metabase/metabase",
                          "tags": {
                            "regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
                            "semver": true
                          },
                          "destination": "/metabase",
                          "stripPrefix": true
                        }
                      ],
                      "onDemand": true,
                      "tlsVerify": true
                  }
               ]
           },
          "search": {
            "enable": true,
            "cve": {
              "updateInterval": "2h",
              "trivy": {
                "dbRepository": "registry.opi5cluster.co.uk/aquasecurity/trivy-db",
                "javaDbRepository": "registry.opi5cluster.co.uk/aquasecurity/trivy-java-db"
              }
            }
          },
          "metrics": {
            "enable": true,
            "prometheus": {
              "path": "/metrics"
            }
          }
        }
      }
  externalSecrets:
    - secretName: registry-credentials
      mountPath: /secrets

    # If persistence is 'true' the service uses a persistentVolumeClaim to mount a
    # volume for zot on '/var/lib/registry'; by default the pvc used is named
    # '$CHART_RELEASE-pvc', but the name can be changed below
  persistence: true
  # PVC data, only used if persistence is 'true'
  pvc:
    create: true
    name: zot-pvc
    accessModes: ["ReadWriteOnce"]
    storage: 500Gi
    storageClassName: longhorn-ssd-large

  # Persistent cache for the Trivy vulnerability database. Trivy backs zot's
  # CVE scanning; without a stable cache, the ~150MB DB is re-fetched on
  # every pod restart and scans can race while it's empty. The PVC is
  # created by templates/trivy-cache-pvc.yaml in this chart, then mounted
  # into the upstream zot pod via extraVolumes/extraVolumeMounts below.
  trivyCache:
    enabled: true
    # Path inside the container; also exported as $TRIVY_CACHE_DIR so trivy
    # writes here regardless of its compiled-in default.
    mountPath: /tmp/trivy
    pvc:
      # PVC name. Must match the claimName in extraVolumes below.
      # Defaults to "zot-trivy-cache".
      name: null
      accessModes: ["ReadWriteOnce"]
      # trivy-db is ~150 MB today; 5 Gi leaves room for historical revisions.
      storage: 5Gi
      storageClassName: longhorn-ssd-large

  # Mount the trivy cache PVC into the zot container. The volume name
  # ("trivy-cache") and mountPath must match trivyCache.mountPath above.
  extraVolumes:
    - name: trivy-cache
      persistentVolumeClaim:
        claimName: zot-trivy-cache
  extraVolumeMounts:
    - name: trivy-cache
      mountPath: /tmp/trivy

  # Container env vars. We add TRIVY_CACHE_DIR so trivy's DB lives on the PVC.
  env:
    - name: TRIVY_CACHE_DIR
      value: /tmp/trivy

  # Deployment strategy type
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%

  # Extra args to pass to the deployment's container
  extraArgs: []
  podAnnotations: {}
  podLabels: {}
  deploymentAnnotations: {}
  priorityClassName: ""
  dnsConfig: {}
  dnsPolicy: "ClusterFirst"

  metrics:
    enabled: true

Manifests & templates

templates/registry-secret.yaml

Image pull secret for authenticating against Zot.

Show manifest
apiVersion: external-secrets.io/v1

kind: ExternalSecret
metadata:
  name: registry-external-secret
  namespace: {{ .Release.Namespace }}
spec:
  refreshInterval: 24h
  secretStoreRef:
    kind: ClusterSecretStore
    name: vault-cluster-secret-store
  target:
    name: registry-credentials
    creationPolicy: Owner
    template:
      type: Opaque
      engineVersion: v2
      data:
        credentials.json: |
          {
            "registry.gitlab.com": {
              "username": "{{ `{{ .gitlabUserName }}` }}",
              "password": "{{ `{{ .gitlabUserPass }}` }}"
            }
          }
  data:
  - secretKey: gitlabUserName
    remoteRef:
      key: gitlab
      property: ci_user
  - secretKey: gitlabUserPass
    remoteRef:
      key: gitlab
      property: ci_password

templates/trivy-cache-pvc.yaml

PVC holding the Trivy vulnerability database cache.

Show manifest
{{- if .Values.zot.trivyCache.enabled }}
{{- $pvcName := default "zot-trivy-cache" .Values.zot.trivyCache.pvc.name }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ $pvcName }}
  namespace: {{ .Release.Namespace }}
  labels:
    app.kubernetes.io/name: zot
    app.kubernetes.io/instance: {{ .Release.Name }}
    app.kubernetes.io/component: trivy-cache
spec:
  accessModes:
    {{- toYaml .Values.zot.trivyCache.pvc.accessModes | nindent 4 }}
  resources:
    requests:
      storage: {{ .Values.zot.trivyCache.pvc.storage }}
  {{- if .Values.zot.trivyCache.pvc.storageClassName }}
  storageClassName: "{{ .Values.zot.trivyCache.pvc.storageClassName }}"
  {{- end }}
{{- end }}

Trade-offs

Decision. Self-host the registry

Alternative. Docker Hub or another hosted registry

Why. No pull rate limits, images stay on the LAN for fast pulls, and the cluster remains fully self-contained.

← Back to Platform & Infrastructure · All service groups