How to Enforce Allowed Kubernetes Image Registries with Kyverno

How to Enforce Allowed Kubernetes Image Registries with Kyverno

Source: Dev.to

If you prefer learning by watching rather than reading, the full lab walkthrough is available in video form on my YouTube channel. Kubernetes Image Security with Kyverno (Real-World Lab) - Part 1 Controlling which container registries can be used inside a Kubernetes cluster is a core part of supply chain security. If workloads can pull images from any external source, you lose visibility and risk introducing untrusted software. A simple way to lock this down at admission time is to use Kyverno. This guide walks through building policies that only allow images from approved registries and block everything else. The same approach applies in real production clusters and aligns well with CKS preparation. Only permit images from: harbor.internal.local Any Pod pulling from an unapproved registry, including Docker Hub, should be denied during admission. Confirming the Active Cluster Context Before you start, verify you're working on the correct cluster: If the context looks wrong, switch: Move forward once your nodes show Ready. Kyverno operates as a set of controllers inside the cluster. If it's not already deployed, install it with Helm. Check whether it exists: If nothing shows up, add the repo: Create the namespace if needed: Verify the controllers are running: Once everything is Running, Kyverno begins intercepting admission requests. Building the Global Registry Restriction Policy Create a ClusterPolicy that validates image sources at admission. File: restrict-registries.yaml You should see restrict-image-registries in the list. You need to validate both failure and success paths. Test 1: Disallowed registry This is denied because the image does not match the allowed patterns. The admission error confirms the rule is active. Test 2: Allowed registry Admission succeeds. The Pod may still fail to pull if the registry does not exist, but that is unrelated to the policy. This proves the global restriction works. Helpful Verification Commands Extending the Model for Multi-Environment Clusters Most real clusters run several environments under the same control plane. A common setup is separate dev and prod namespaces. Each environment may rely on different registries, so policy enforcement must reflect that. The global policy stays in place. Now add namespace-specific rules. Creating Environment Namespaces Applying Namespace-Based Registry Policies Create a second ClusterPolicy: Testing Namespace Restrictions This should be rejected. The nginx Pod fails. The registry-approved one passes admission. How the Layered Approach Works The cluster-wide policy limits image usage to internal registries only. The per-namespace policy adds a second filter: dev uses only dev registry images prod uses only prod registry images Both policies apply together. A Pod must satisfy both to be admitted. This structure prevents accidental cross-environment deployments and tightens supply chain controls without complicating developer workflows. You installed Kyverno, created a global restriction policy for image registries, and validated correct admission behavior. You then extended the scenario with environment-specific rules, ensuring dev and prod cannot cross-pull container images. This is a core security pattern for Kubernetes and aligns directly with CKS study requirements. Once registry control is established, you can layer in tag policies, digest enforcement, and signing rules. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
kubectl config current-context
kubectl get nodes Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl config current-context
kubectl get nodes COMMAND_BLOCK:
kubectl config current-context
kubectl get nodes COMMAND_BLOCK:
kubectl config use-context <context>
kubectl get nodes Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl config use-context <context>
kubectl get nodes COMMAND_BLOCK:
kubectl config use-context <context>
kubectl get nodes COMMAND_BLOCK:
kubectl get pods -n kyverno Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl get pods -n kyverno COMMAND_BLOCK:
kubectl get pods -n kyverno CODE_BLOCK:
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update CODE_BLOCK:
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update COMMAND_BLOCK:
kubectl create namespace kyverno Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl create namespace kyverno COMMAND_BLOCK:
kubectl create namespace kyverno CODE_BLOCK:
helm install kyverno kyverno/kyverno -n kyverno Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
helm install kyverno kyverno/kyverno -n kyverno CODE_BLOCK:
helm install kyverno kyverno/kyverno -n kyverno COMMAND_BLOCK:
kubectl get pods -n kyverno Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl get pods -n kyverno COMMAND_BLOCK:
kubectl get pods -n kyverno CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: restrict-image-registries
spec: validationFailureAction: enforce background: true rules: - name: validate-registries match: resources: kinds: - Pod validate: message: "Only registry.company.io or harbor.internal.local are allowed." pattern: spec: containers: - image: "registry.company.io/* | harbor.internal.local/*" Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: restrict-image-registries
spec: validationFailureAction: enforce background: true rules: - name: validate-registries match: resources: kinds: - Pod validate: message: "Only registry.company.io or harbor.internal.local are allowed." pattern: spec: containers: - image: "registry.company.io/* | harbor.internal.local/*" CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: restrict-image-registries
spec: validationFailureAction: enforce background: true rules: - name: validate-registries match: resources: kinds: - Pod validate: message: "Only registry.company.io or harbor.internal.local are allowed." pattern: spec: containers: - image: "registry.company.io/* | harbor.internal.local/*" COMMAND_BLOCK:
kubectl apply -f restrict-registries.yaml Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl apply -f restrict-registries.yaml COMMAND_BLOCK:
kubectl apply -f restrict-registries.yaml COMMAND_BLOCK:
kubectl get clusterpolicy Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl get clusterpolicy COMMAND_BLOCK:
kubectl get clusterpolicy COMMAND_BLOCK:
kubectl run testbad --image=nginx Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl run testbad --image=nginx COMMAND_BLOCK:
kubectl run testbad --image=nginx COMMAND_BLOCK:
kubectl run testgood --image=registry.company.io/app:v1
kubectl get pods Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl run testgood --image=registry.company.io/app:v1
kubectl get pods COMMAND_BLOCK:
kubectl run testgood --image=registry.company.io/app:v1
kubectl get pods COMMAND_BLOCK:
kubectl get clusterpolicy
kubectl describe clusterpolicy restrict-image-registries
kubectl get pods
kubectl describe pod testbad
kubectl describe pod testgood Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl get clusterpolicy
kubectl describe clusterpolicy restrict-image-registries
kubectl get pods
kubectl describe pod testbad
kubectl describe pod testgood COMMAND_BLOCK:
kubectl get clusterpolicy
kubectl describe clusterpolicy restrict-image-registries
kubectl get pods
kubectl describe pod testbad
kubectl describe pod testgood COMMAND_BLOCK:
kubectl delete pod testbad testgood --ignore-not-found Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl delete pod testbad testgood --ignore-not-found COMMAND_BLOCK:
kubectl delete pod testbad testgood --ignore-not-found COMMAND_BLOCK:
kubectl create namespace dev
kubectl create namespace prod
kubectl get namespaces Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl create namespace dev
kubectl create namespace prod
kubectl get namespaces COMMAND_BLOCK:
kubectl create namespace dev
kubectl create namespace prod
kubectl get namespaces CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: registry-per-namespace
spec: validationFailureAction: Enforce background: true rules: - name: dev-registry-only match: resources: kinds: - Pod namespaces: - dev validate: message: "Dev namespace must use registry.dev.company.io." pattern: spec: containers: - image: "registry.dev.company.io/*" - name: prod-registry-only match: resources: kinds: - Pod namespaces: - prod validate: message: "Prod namespace must use registry.prod.company.io." pattern: spec: containers: - image: "registry.prod.company.io/*" Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: registry-per-namespace
spec: validationFailureAction: Enforce background: true rules: - name: dev-registry-only match: resources: kinds: - Pod namespaces: - dev validate: message: "Dev namespace must use registry.dev.company.io." pattern: spec: containers: - image: "registry.dev.company.io/*" - name: prod-registry-only match: resources: kinds: - Pod namespaces: - prod validate: message: "Prod namespace must use registry.prod.company.io." pattern: spec: containers: - image: "registry.prod.company.io/*" CODE_BLOCK:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: name: registry-per-namespace
spec: validationFailureAction: Enforce background: true rules: - name: dev-registry-only match: resources: kinds: - Pod namespaces: - dev validate: message: "Dev namespace must use registry.dev.company.io." pattern: spec: containers: - image: "registry.dev.company.io/*" - name: prod-registry-only match: resources: kinds: - Pod namespaces: - prod validate: message: "Prod namespace must use registry.prod.company.io." pattern: spec: containers: - image: "registry.prod.company.io/*" COMMAND_BLOCK:
kubectl apply -f registry-per-namespace.yaml Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl apply -f registry-per-namespace.yaml COMMAND_BLOCK:
kubectl apply -f registry-per-namespace.yaml COMMAND_BLOCK:
kubectl run bad-dev --image=nginx -n dev Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl run bad-dev --image=nginx -n dev COMMAND_BLOCK:
kubectl run bad-dev --image=nginx -n dev COMMAND_BLOCK:
kubectl run good-dev --image=registry.dev.company.io/app:1.0 -n dev Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl run good-dev --image=registry.dev.company.io/app:1.0 -n dev COMMAND_BLOCK:
kubectl run good-dev --image=registry.dev.company.io/app:1.0 -n dev COMMAND_BLOCK:
kubectl run bad-prod --image=nginx -n prod
kubectl run good-prod --image=registry.prod.company.io/web:1.0 -n prod Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
kubectl run bad-prod --image=nginx -n prod
kubectl run good-prod --image=registry.prod.company.io/web:1.0 -n prod COMMAND_BLOCK:
kubectl run bad-prod --image=nginx -n prod
kubectl run good-prod --image=registry.prod.company.io/web:1.0 -n prod