Image Policies and Enforcement
Overview
Even with image scanning in your CI/CD pipeline, you need a last line of defense at the Kubernetes admission gate to prevent unauthorized or unscanned images from being deployed. Image policies enforce rules about which images are allowed to run in your cluster.
Kubernetes provides several mechanisms for image policy enforcement:
- ImagePolicyWebhook -- a built-in admission controller that delegates image decisions to an external webhook
- OPA/Gatekeeper -- a policy engine that can enforce image registry allowlists and naming conventions
- AlwaysPullImages -- an admission controller that forces fresh image pulls to prevent using stale cached images
- Image Digest Pinning -- using immutable SHA256 digests instead of mutable tags
CKS Exam Relevance
ImagePolicyWebhook is one of the most complex and frequently tested topics in the Supply Chain Security domain. You must know how to configure the admission controller, the webhook kubeconfig, and the AdmissionConfiguration file. Expect a multi-step configuration task.
ImagePolicyWebhook Flow
Configuring ImagePolicyWebhook
Setting up ImagePolicyWebhook requires several interconnected configuration files. Here is the complete process:
Step 1: Create the AdmissionConfiguration File
This file tells the API server how the ImagePolicyWebhook admission controller should behave.
# /etc/kubernetes/pki/admission_configuration.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/pki/admission_kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: falseKey fields explained:
| Field | Description |
|---|---|
kubeConfigFile | Path to the kubeconfig file for connecting to the webhook |
allowTTL | Seconds to cache an "allow" decision |
denyTTL | Seconds to cache a "deny" decision |
retryBackoff | Milliseconds to wait before retrying a failed webhook call |
defaultAllow | Whether to allow images if the webhook is unreachable |
Critical Setting
defaultAllow: false is the secure default. If set to true, images will be allowed when the webhook is unreachable, which defeats the purpose of the policy. On the CKS exam, always set this to false unless explicitly told otherwise.
Step 2: Create the Webhook Kubeconfig
This kubeconfig file tells the API server how to connect to the external webhook service.
# /etc/kubernetes/pki/admission_kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: image-policy-webhook
cluster:
server: https://image-policy-webhook.default.svc:443/image_policy
certificate-authority: /etc/kubernetes/pki/webhook-ca.crt
contexts:
- name: image-policy-webhook
context:
cluster: image-policy-webhook
user: api-server
current-context: image-policy-webhook
users:
- name: api-server
user:
client-certificate: /etc/kubernetes/pki/apiserver.crt
client-key: /etc/kubernetes/pki/apiserver.keyUnderstanding the Kubeconfig
This is not a regular kubectl kubeconfig. It defines how the API server (acting as a client) connects to the webhook server:
clusters[].cluster.server-- the URL of the webhook serviceclusters[].cluster.certificate-authority-- CA cert to verify the webhook serverusers[].user.client-certificate/key-- client cert the API server uses to authenticate itself to the webhook
Step 3: Enable the Admission Controller on the API Server
Edit the kube-apiserver static pod manifest to enable the ImagePolicyWebhook admission controller:
# /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
- --admission-control-config-file=/etc/kubernetes/pki/admission_configuration.yaml
# ... other flags
volumeMounts:
- name: admission-config
mountPath: /etc/kubernetes/pki/admission_configuration.yaml
readOnly: true
- name: admission-kubeconfig
mountPath: /etc/kubernetes/pki/admission_kubeconfig.yaml
readOnly: true
- name: webhook-ca
mountPath: /etc/kubernetes/pki/webhook-ca.crt
readOnly: true
volumes:
- name: admission-config
hostPath:
path: /etc/kubernetes/pki/admission_configuration.yaml
type: File
- name: admission-kubeconfig
hostPath:
path: /etc/kubernetes/pki/admission_kubeconfig.yaml
type: File
- name: webhook-ca
hostPath:
path: /etc/kubernetes/pki/webhook-ca.crt
type: FileImportant Details
- Add
ImagePolicyWebhookto the--enable-admission-pluginsflag (comma-separated, no spaces) - Set
--admission-control-config-fileto point to the AdmissionConfiguration file - Ensure all referenced files are mounted into the API server pod via hostPath volumes
- After saving the manifest, wait for the API server to restart (kubelet manages static pods automatically)
Step 4: Verify the Configuration
# Wait for the API server to restart
kubectl get pods -n kube-system | grep apiserver
# Test with an image that should be denied
kubectl run test --image=docker.io/nginx:latest
# Expected: Error from server (Forbidden): ...
# Test with an allowed image
kubectl run test --image=registry.company.com/nginx:1.25
# Expected: pod/test created
# Check API server logs for webhook activity
kubectl logs -n kube-system kube-apiserver-controlplane | grep -i imageThe ImageReview API
When the API server sends a request to the webhook, it sends an ImageReview object:
Request (API Server to Webhook)
{
"apiVersion": "imagepolicy.k8s.io/v1alpha1",
"kind": "ImageReview",
"spec": {
"containers": [
{
"image": "docker.io/nginx:latest"
}
],
"annotations": {
"kubernetes.io/namespace": "default"
},
"namespace": "default"
}
}Response (Webhook to API Server)
Allow:
{
"apiVersion": "imagepolicy.k8s.io/v1alpha1",
"kind": "ImageReview",
"status": {
"allowed": true
}
}Deny:
{
"apiVersion": "imagepolicy.k8s.io/v1alpha1",
"kind": "ImageReview",
"status": {
"allowed": false,
"reason": "Image docker.io/nginx:latest is not from a trusted registry"
}
}Image Digest Pinning vs Tags
The Problem with Tags
Container image tags are mutable -- the same tag can point to different image contents over time:
# Today: nginx:1.25 points to image sha256:abc123
# Tomorrow: nginx:1.25 could point to sha256:def456 (re-pushed tag)This creates a security gap: even if you scanned nginx:1.25 yesterday, the image behind that tag may have changed.
The Solution: Digest Pinning
Use the immutable SHA256 digest instead of a mutable tag:
# BAD: Mutable tag -- can change without notice
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx:1.25# GOOD: Immutable digest -- always the same image
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764e2d42b2f4627c31f2a616affb1Finding the Digest
# Get the digest of an image
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25
# Or pull and check
docker pull nginx:1.25
# Output includes: Digest: sha256:6db391d1c0cfb30...
# Using crane (lightweight tool)
crane digest nginx:1.25AlwaysPullImages Admission Controller
The AlwaysPullImages admission controller mutates every new Pod to set imagePullPolicy: Always, regardless of what was specified.
Why It Matters
Without AlwaysPullImages:
- A node caches images after the first pull
- Any Pod on that node can use the cached image without authentication
- This allows unauthorized access to private images
Enabling AlwaysPullImages
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- name: kube-apiserver
command:
- kube-apiserver
- --enable-admission-plugins=NodeRestriction,AlwaysPullImagesWhen to Use
AlwaysPullImages is most useful in multi-tenant clusters where you do not want tenants to access images they are not authorized to pull. The trade-off is increased registry load and longer Pod startup times.
imagePullPolicy Options
| Policy | Behavior | Security Implication |
|---|---|---|
Always | Always pulls from registry | Most secure; validates credentials every time |
IfNotPresent | Uses cache if available | Cached images bypass registry auth |
Never | Only uses cached images | No registry validation at all |
OPA/Gatekeeper for Image Policy Enforcement
OPA Gatekeeper provides a more flexible and maintainable approach to image policy enforcement than ImagePolicyWebhook for many use cases.
Architecture
ConstraintTemplate: Allowed Registries
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedregistries
spec:
crd:
spec:
names:
kind: K8sAllowedRegistries
validation:
openAPIV3Schema:
type: object
properties:
registries:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedregistries
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not registry_allowed(container.image)
msg := sprintf("Image '%v' is not from an allowed registry. Allowed registries: %v", [container.image, input.parameters.registries])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not registry_allowed(container.image)
msg := sprintf("Init container image '%v' is not from an allowed registry. Allowed registries: %v", [container.image, input.parameters.registries])
}
registry_allowed(image) {
registry := input.parameters.registries[_]
startswith(image, registry)
}Constraint: Enforce Specific Registries
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRegistries
metadata:
name: allowed-registries
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- default
- production
parameters:
registries:
- "registry.company.com/"
- "gcr.io/my-project/"Testing the Constraint
# This should be DENIED
kubectl run test --image=docker.io/nginx:latest -n default
# Error: Image 'docker.io/nginx:latest' is not from an allowed registry.
# Allowed registries: ["registry.company.com/", "gcr.io/my-project/"]
# This should be ALLOWED
kubectl run test --image=registry.company.com/nginx:1.25 -n default
# pod/test createdConstraint to Require Image Digests
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredigest
spec:
crd:
spec:
names:
kind: K8sRequireDigest
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredigest
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, "@sha256:")
msg := sprintf("Container image '%v' must use a digest (sha256) instead of a tag", [container.image])
}apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireDigest
metadata:
name: require-image-digest
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- productionComparing Image Policy Approaches
| Feature | ImagePolicyWebhook | OPA/Gatekeeper |
|---|---|---|
| Built into Kubernetes | Yes (admission plugin) | No (requires installation) |
| Complexity | High (multiple config files) | Medium (CRDs + policies) |
| Flexibility | Limited (allow/deny binary) | High (any OPA/Rego logic) |
| Custom logic | Requires external webhook server | Rego policies in-cluster |
| Error messages | Basic reason field | Detailed violation messages |
| CKS exam focus | High -- expect configuration tasks | Medium -- understand concepts |
| Caching | Built-in TTL caching | No built-in caching |
| Failure mode | Configurable (defaultAllow) | Configurable (failurePolicy) |
Complete ImagePolicyWebhook Setup Checklist
Exam Checklist
Use this checklist when configuring ImagePolicyWebhook on the exam:
- Create or edit
/etc/kubernetes/pki/admission_configuration.yaml- Set
defaultAllow: false - Point
kubeConfigFileto the webhook kubeconfig
- Set
- Create or edit the webhook kubeconfig file
- Set the webhook
serverURL - Configure TLS certificates
- Set the webhook
- Edit
/etc/kubernetes/manifests/kube-apiserver.yaml- Add
ImagePolicyWebhookto--enable-admission-plugins - Add
--admission-control-config-filepointing to the AdmissionConfiguration - Mount all referenced files as volumes
- Add
- Wait for the API server to restart
- Test with
kubectl run test --image=<image>to verify policy enforcement
Key Takeaways
Summary
- ImagePolicyWebhook delegates image admission decisions to an external webhook service
- The AdmissionConfiguration file links the admission controller to the webhook kubeconfig
defaultAllow: falseis critical -- denies images when the webhook is unreachable- Image digests (
@sha256:...) are immutable and more secure than tags - AlwaysPullImages admission controller prevents unauthorized use of cached images
- OPA/Gatekeeper provides flexible image policy enforcement with custom Rego policies
- All configuration files must be mounted into the API server pod via hostPath volumes
Common Exam Pitfalls
- Forgetting to add
--admission-control-config-fileflag to the API server - Not mounting the kubeconfig and CA certificate files into the API server pod
- Setting
defaultAllow: true(insecure default) - Confusing
imagePullPolicy: Always(Pod spec) with theAlwaysPullImagesadmission controller - Not waiting for the API server to restart after modifying the static pod manifest