CKS — Image Allowlisting
Image Allowlisting
CKS Exam Domain: Supply Chain Security (20%)
Without image restrictions, any user with Pod creation privileges can deploy any image from any registry — including Docker Hub, personal registries, or malicious sources. Image allowlisting ensures that only images from trusted, approved registries are permitted to run in the cluster, forming a critical supply chain security gate.
1. The Threat: Unrestricted Image Sources
Without allowlisting:
Developer A → kubectl run test --image=nginx ✅ Docker Hub (public)
Developer B → kubectl run app --image=registry.evil.com/backdoor ✅ Malicious registry!
Developer C → kubectl run miner --image=cryptominer:latest ✅ Crypto mining image!
Attacker → kubectl run shell --image=attacker/reverse-shell ✅ Reverse shell!
With allowlisting:
Developer A → kubectl run test --image=registry.company.com/nginx:1.27 ✅ Trusted registry
Developer B → kubectl run app --image=registry.evil.com/backdoor ❌ DENIED
Developer C → kubectl run miner --image=cryptominer:latest ❌ DENIED
Attacker → kubectl run shell --image=attacker/reverse-shell ❌ DENIED
Supply chain attack vectors that allowlisting blocks:
- Typosquatting —
ngixninstead ofnginxon Docker Hub - tag hijacking —
latesttag replaced with malicious image - Registry compromise — Public registry serves tampered images
- Shadow IT — Developers pulling unvetted images
2. OPA Gatekeeper: Allowed Registries
OPA Gatekeeper is a Kubernetes-native admission controller that enforces policies using Rego. This is a CKS-exam-relevant approach.
Step 1: ConstraintTemplate
The ConstraintTemplate defines the policy logic — what registries are allowed.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
description: "List of allowed container image registries"
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, input.parameters.repos[_])
msg := sprintf("Container '%v' uses image '%v' which is not from an allowed registry. Allowed registries: %v",
[container.name, container.image, input.parameters.repos])
}
# Also check init containers
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not startswith(container.image, input.parameters.repos[_])
msg := sprintf("Init container '%v' uses image '%v' which is not from an allowed registry. Allowed registries: %v",
[container.name, container.image, input.parameters.repos])
}
# Also check ephemeral containers
violation[{"msg": msg}] {
container := input.review.object.spec.ephemeralContainers[_]
not startswith(container.image, input.parameters.repos[_])
msg := sprintf("Ephemeral container '%v' uses image '%v' which is not from an allowed registry. Allowed registries: %v",
[container.name, container.image, input.parameters.repos])
}
Step 2: Constraint
The Constraint applies the template to specific resources with specific parameters.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
enforcementAction: deny # deny | warn | dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces:
- production
- staging
excludedNamespaces:
- kube-system # Don't block system images
parameters:
repos:
- "registry.company.com/"
- "gcr.io/company-project/"
- "012345678901.dkr.ecr.us-east-1.amazonaws.com/"
Test: Verify the Policy
# Apply the policy
kubectl apply -f constrainttemplate.yaml
kubectl apply -f constraint.yaml
# This should SUCCEED
kubectl run trusted-app \
--image=registry.company.com/nginx:1.27 \
-n production
# This should be DENIED
kubectl run untrusted-app \
--image=nginx:1.27 \
-n production
Expected error:
Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request:
[allowed-repos] Container 'untrusted-app' uses image 'nginx:1.27' which is not from an allowed registry.
Allowed registries: ["registry.company.com/", "gcr.io/company-project/"]
3. Kyverno: Image Validation Policy
Kyverno is a Kubernetes-native policy engine that uses YAML (no Rego needed). It’s simpler to write and read.
ClusterPolicy: Restrict Image Registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
annotations:
policies.kyverno.io/title: Restrict Image Registries
policies.kyverno.io/category: Supply Chain Security
policies.kyverno.io/severity: high
policies.kyverno.io/description: >-
Only allow images from trusted registries to run in the cluster.
Blocks Docker Hub, quay.io, and other public registries.
spec:
validationFailureAction: Enforce # Enforce | Audit
background: true
rules:
- name: validate-registries
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- kube-system
- kyverno
validate:
message: >-
Image '{{request.object.spec.containers[].image}}' is not from an allowed registry.
Only images from registry.company.com and gcr.io/company-project are permitted.
pattern:
spec:
containers:
- image: "registry.company.com/* | gcr.io/company-project/*"
=(initContainers):
- image: "registry.company.com/* | gcr.io/company-project/*"
Test the Kyverno Policy
# Apply the policy
kubectl apply -f restrict-image-registries.yaml
# Allowed — matches registry pattern
kubectl run test --image=registry.company.com/app:v1.0 -n default
# Denied — Docker Hub image
kubectl run test --image=nginx:1.27 -n default
# Error from server: resource Pod/default/test was blocked due to the following policies:
# restrict-image-registries:
# validate-registries: Image 'nginx:1.27' is not from an allowed registry.
OPA Gatekeeper vs Kyverno
| Feature | OPA Gatekeeper | Kyverno |
|---|---|---|
| Policy language | Rego | YAML |
| Learning curve | Steep | Easy |
| CKS exam relevance | High (exam may provide Rego) | High (simpler to write under exam pressure) |
| Mutation support | Yes (via assign) | Yes (native) |
| Generation support | No | Yes (generate resources) |
| Audit mode | Yes (dryrun) | Yes (Audit) |
4. Private Registry Pull Secrets
To pull images from a private registry, Kubernetes needs authentication credentials stored as a Secret.
Create a Pull Secret
kubectl create secret docker-registry reg-cred \
--docker-server=registry.company.com \
--docker-username=k8s-puller \
--docker-password='P@ss1234' \
--docker-email=k8s@company.com \
-n production
Use in a Pod
apiVersion: v1
kind: Pod
metadata:
name: private-app
namespace: production
spec:
containers:
- name: app
image: registry.company.com/app:v2.1
imagePullSecrets:
- name: reg-cred
Attach to a ServiceAccount (Automatic for All Pods)
# Patch the default ServiceAccount to always use this pull secret
kubectl patch serviceaccount default \
-n production \
-p '{"imagePullSecrets": [{"name": "reg-cred"}]}'
Now every Pod in the production namespace using the default ServiceAccount will automatically use reg-cred for image pulls.
Security Note: Pull secrets are stored base64-encoded in etcd. Combine with encryption at rest (EncryptionConfiguration) to protect registry credentials.
5. imagePullPolicy Security Implications
The imagePullPolicy field controls when the kubelet pulls the container image.
| Policy | Behavior | Security Implication |
|---|---|---|
Always |
Pull every time a Pod starts | Detects tag overwrites — if someone pushes a new image to the same tag, you get the new one |
IfNotPresent |
Pull only if image not cached locally | Uses cached image — won’t detect tag overwrite (stale but predictable) |
Never |
Never pull, image must exist locally | Only works with pre-loaded images (air-gapped environments) |
The Problem with Mutable Tags
# ❌ DANGEROUS: "latest" or "v1" tags can be overwritten
image: nginx:latest
imagePullPolicy: IfNotPresent
# Node A caches nginx:latest from Monday
# Attacker pushes malicious image as nginx:latest on Tuesday
# Node A still runs Monday's image (cached)
# Node B pulls Tuesday's malicious image (not cached)
# → Different nodes run different versions!
# ⚠️ BETTER but still not fully secure
image: nginx:latest
imagePullPolicy: Always
# Always pulls, so all nodes get the same version
# BUT if the tag was overwritten, all nodes get the malicious version!
# ✅ SECURE: Digest pinning — immutable reference
image: nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764facf3b22...
# Cannot be overwritten
# Unique to this exact image content
# Same image on every node, always
6. Image Digest Pinning
A digest is a SHA-256 hash of the image manifest. It uniquely and immutably identifies an image. Tags are mutable pointers; digests are not.
Get an Image Digest
# From a pulled image
docker pull nginx:1.27
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.27
# nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764facf3b22...
# From a remote registry (without pulling)
crane digest nginx:1.27
# sha256:6db391d1c0cfb30588ba0bf72ea999404f2764facf3b22...
# Using skopeo
skopeo inspect docker://nginx:1.27 | jq -r '.Digest'
Use in Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: registry.company.com/nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764facf3b22abc123def456
ports:
- containerPort: 80
Enforce Digest Pinning with Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
rules:
- name: check-digest
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must use digest (@sha256:...) instead of tags."
pattern:
spec:
containers:
- image: "*@sha256:*"
7. Combining Allowlisting Controls
A defense-in-depth approach combines multiple layers:
┌──────────────────────────────────────────────────────┐
│ Image Pull Request │
│ │
│ 1. Registry Allowlist (Gatekeeper/Kyverno) │
│ └── Is the image from a trusted registry? ───→ ❌ │
│ │
│ 2. Digest Requirement (Kyverno) │
│ └── Does the image use @sha256:...? ──────→ ❌ │
│ │
│ 3. ImagePolicyWebhook │
│ └── Does the backend approve? ────────────→ ❌ │
│ │
│ 4. Pull Secret Authentication │
│ └── Can we authenticate to the registry? ─→ ❌ │
│ │
│ 5. Image Pull │
│ └── ✅ Image runs in the cluster │
└──────────────────────────────────────────────────────┘
Summary
| Control | Tool | What It Blocks |
|---|---|---|
| Registry allowlist | OPA Gatekeeper, Kyverno | Images from untrusted registries |
| Digest pinning | Kyverno policy | Mutable tag references |
| Private registry + pull secrets | Kubernetes Secrets | Unauthenticated pulls |
imagePullPolicy: Always |
Pod spec | Stale cached images (but doesn’t prevent tag overwrite) |
| ImagePolicyWebhook | API server admission | Any image not approved by external backend |
Practice Questions
Question 1
Write an OPA Gatekeeper Constraint that allows images only from gcr.io/myproject/ and docker.io/library/ in the production namespace.
Show Answer
First, ensure the K8sAllowedRepos ConstraintTemplate is applied (see section 2 above), then apply the Constraint:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: production-repos
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- production
parameters:
repos:
- "gcr.io/myproject/"
- "docker.io/library/"
Question 2
A developer reports that kubectl run test --image=nginx:1.27 -n staging is being rejected. The error message says the image is not from an allowed registry. However, they need to use this image temporarily for debugging. What are your options?
Show Answer
Options (from most to least secure):
- Mirror the image to the trusted registry:
docker pull nginx:1.27 docker tag nginx:1.27 registry.company.com/nginx:1.27 docker push registry.company.com/nginx:1.27 kubectl run test --image=registry.company.com/nginx:1.27 -n staging - Temporarily switch the Gatekeeper constraint to
warnmode:spec: enforcementAction: warn # Changed from denyThis logs a warning but allows the image. Reset to
denyafter debugging. -
Exclude the
stagingnamespace from the constraint (if acceptable by policy). - Use
kubectl debugwith an ephemeral container instead of running a new Pod.
Option 1 is the correct approach — always mirror to your trusted registry.
Question 3
What is the security risk of using imagePullPolicy: IfNotPresent with mutable tags like latest?
Show Answer
With IfNotPresent, the kubelet uses the cached image if it exists on the node. Risks:
-
Inconsistency across nodes — Node A has the cached (old) image, Node B pulls the new (possibly malicious) image. Different Pods in the same Deployment run different code.
-
Tag overwrite not detected — If an attacker pushes a new image to the same tag, nodes with the cached version are safe but new nodes get the attacker’s image.
-
No rollback visibility —
kubectl describe podshowsnginx:lateston both nodes, hiding the fact that they’re running different content.
Mitigation: Use image digests (@sha256:...) instead of mutable tags. Digests are immutable — the same digest always references the same image content.
Question 4
How do you configure a ServiceAccount so that all Pods using it automatically pull from a private registry?
Show Answer
-
Create the pull secret:
kubectl create secret docker-registry reg-cred \ --docker-server=registry.company.com \ --docker-username=deployer \ --docker-password='s3cur3' \ -n production -
Patch the ServiceAccount:
kubectl patch serviceaccount default \ -n production \ -p '{"imagePullSecrets": [{"name": "reg-cred"}]}' -
Verify:
kubectl get serviceaccount default -n production -o yaml # Should show: # imagePullSecrets: # - name: reg-cred
Now every Pod in production that uses the default ServiceAccount will automatically have reg-cred as its image pull secret without specifying it in the Pod spec.
Question 5
Write a Kyverno ClusterPolicy that requires all images in the secure namespace to use SHA256 digests instead of tags.
Show Answer
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-digest
spec:
validationFailureAction: Enforce
background: true
rules:
- name: require-image-digest
match:
any:
- resources:
kinds:
- Pod
namespaces:
- secure
validate:
message: >-
All images in the 'secure' namespace must reference a digest
(@sha256:...) rather than a tag. Image '{{element.image}}'
does not use a digest.
foreach:
- list: "request.object.spec.containers"
deny:
conditions:
any:
- key: "{{element.image}}"
operator: NotEquals
value: "*@sha256:*"
Alternatively, using the simpler pattern syntax:
validate:
message: "Images must use digest references (@sha256:...)."
pattern:
spec:
containers:
- image: "*@sha256:*"
Test:
# Denied
kubectl run test --image=nginx:1.27 -n secure
# Allowed
kubectl run test --image=nginx@sha256:6db391d1c0cfb30588ba... -n secure