CKS — ImagePolicyWebhook
ImagePolicyWebhook
CKS Exam Domain: Supply Chain Security (20%)
ImagePolicyWebhook is a built-in Kubernetes admission controller that intercepts Pod creation requests and sends the list of container images to an external webhook backend for approval. The backend decides whether each image is allowed to run. If any image is denied, the entire Pod is rejected.
This is a CKS-exam-relevant topic. You may be asked to configure the API server, create the admission configuration, and set up the kubeconfig file for the webhook backend.
1. How ImagePolicyWebhook Works
kubectl create pod kube-apiserver
│ │
└──────────────────────────────────────┤
│
┌─────────────▼──────────────┐
│ Admission Controllers │
│ ├── NamespaceLifecycle │
│ ├── RBAC │
│ ├── ImagePolicyWebhook ────┼──→ External Backend
│ │ (sends ImageReview) │ (approves/denies)
│ └── ... │
└────────────────────────────┘
│
ImageReview response: allow=true/false
│
▼
Pod created or REJECTED
The flow:
- User submits a Pod creation request
- API server hits the
ImagePolicyWebhookadmission controller - The controller constructs an ImageReview object containing all container images
- The ImageReview is sent to the external webhook backend via HTTPS
- The backend responds with
allow: trueorallow: falsefor each image - If denied, the API server rejects the Pod creation
2. Enabling ImagePolicyWebhook
The admission controller must be enabled in the kube-apiserver manifest.
Edit the API Server Manifest
# On the control plane node
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add ImagePolicyWebhook to the admission plugins and point to the configuration file:
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/admission/admission-config.yaml
# ... other flags
volumeMounts:
- name: admission-config
mountPath: /etc/kubernetes/admission
readOnly: true
- name: webhook-certs
mountPath: /etc/kubernetes/admission/certs
readOnly: true
volumes:
- name: admission-config
hostPath:
path: /etc/kubernetes/admission
type: DirectoryOrCreate
- name: webhook-certs
hostPath:
path: /etc/kubernetes/admission/certs
type: DirectoryOrCreate
CKS Exam Tip: After modifying
/etc/kubernetes/manifests/kube-apiserver.yaml, the kubelet automatically restarts the API server. Wait for it to stabilize. Check with:kubectl get pods -n kube-system -l component=kube-apiserver crictl ps | grep apiserver
3. AdmissionConfiguration
The AdmissionConfiguration file tells the API server how to reach the webhook backend and what default behavior to apply.
/etc/kubernetes/admission/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/admission/webhook-kubeconfig.yaml
allowTTL: 50 # seconds to cache allow decisions
denyTTL: 50 # seconds to cache deny decisions
retryBackoff: 500 # milliseconds between retries
defaultAllow: false # CRITICAL: deny if backend is unreachable
defaultAllow Behavior
| Setting | Backend Reachable | Backend Unreachable |
|---|---|---|
defaultAllow: true |
Backend decides (allow/deny) | All images allowed (unsafe!) |
defaultAllow: false |
Backend decides (allow/deny) | All images denied (fail-closed, secure) |
Security Best Practice: Always set
defaultAllow: false. This ensures that if the webhook backend goes down, no new Pods can be created — fail-closed behavior. Setting it totruemeans an attacker could DoS the backend to bypass image policy.
4. Webhook Kubeconfig
The kubeconfig file configures how the API server communicates with the webhook backend (server URL, TLS certificates).
/etc/kubernetes/admission/webhook-kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: image-policy-backend
cluster:
server: https://image-policy.example.com:8443/image-policy
certificate-authority: /etc/kubernetes/admission/certs/ca.crt
users:
- name: kube-apiserver
user:
client-certificate: /etc/kubernetes/admission/certs/apiserver-client.crt
client-key: /etc/kubernetes/admission/certs/apiserver-client.key
contexts:
- name: image-policy
context:
cluster: image-policy-backend
user: kube-apiserver
current-context: image-policy
Key points:
server— The URL of the webhook backend (HTTPS required)certificate-authority— CA certificate to verify the backend’s TLS certificateclient-certificate/client-key— mTLS client certificate for the API server to authenticate to the backend- The API server uses this kubeconfig to make HTTPS calls to the backend
5. The ImageReview API
When a Pod creation request includes container images, the API server sends an ImageReview object to the backend.
Request (API Server → Backend)
{
"apiVersion": "imagepolicy.k8s.io/v1alpha1",
"kind": "ImageReview",
"spec": {
"containers": [
{
"image": "registry.example.com/app:v2.0"
},
{
"image": "nginx:1.27"
}
],
"annotations": {
"kubernetes.io/namespace": "production"
},
"namespace": "production"
}
}
Response (Backend → 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 'nginx:1.27' is not from an approved registry. Only registry.example.com is allowed."
}
}
6. Step-by-Step Configuration
This section provides the complete, ordered steps to configure ImagePolicyWebhook on a kubeadm cluster.
Step 1: Create the Directory Structure
sudo mkdir -p /etc/kubernetes/admission/certs
Step 2: Generate TLS Certificates (Simplified for Lab)
# Generate CA
openssl genrsa -out /etc/kubernetes/admission/certs/ca.key 2048
openssl req -x509 -new -nodes \
-key /etc/kubernetes/admission/certs/ca.key \
-days 365 -out /etc/kubernetes/admission/certs/ca.crt \
-subj "/CN=ImagePolicyCA"
# Generate backend server certificate
openssl genrsa -out /etc/kubernetes/admission/certs/server.key 2048
openssl req -new \
-key /etc/kubernetes/admission/certs/server.key \
-out /etc/kubernetes/admission/certs/server.csr \
-subj "/CN=image-policy.example.com"
openssl x509 -req \
-in /etc/kubernetes/admission/certs/server.csr \
-CA /etc/kubernetes/admission/certs/ca.crt \
-CAkey /etc/kubernetes/admission/certs/ca.key \
-CAcreateserial \
-out /etc/kubernetes/admission/certs/server.crt \
-days 365
# Generate API server client certificate
openssl genrsa -out /etc/kubernetes/admission/certs/apiserver-client.key 2048
openssl req -new \
-key /etc/kubernetes/admission/certs/apiserver-client.key \
-out /etc/kubernetes/admission/certs/apiserver-client.csr \
-subj "/CN=kube-apiserver"
openssl x509 -req \
-in /etc/kubernetes/admission/certs/apiserver-client.csr \
-CA /etc/kubernetes/admission/certs/ca.crt \
-CAkey /etc/kubernetes/admission/certs/ca.key \
-CAcreateserial \
-out /etc/kubernetes/admission/certs/apiserver-client.crt \
-days 365
Step 3: Create the AdmissionConfiguration
cat <<EOF | sudo tee /etc/kubernetes/admission/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/admission/webhook-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false
EOF
Step 4: Create the Webhook Kubeconfig
cat <<EOF | sudo tee /etc/kubernetes/admission/webhook-kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: image-policy-backend
cluster:
server: https://image-policy.example.com:8443/image-policy
certificate-authority: /etc/kubernetes/admission/certs/ca.crt
users:
- name: kube-apiserver
user:
client-certificate: /etc/kubernetes/admission/certs/apiserver-client.crt
client-key: /etc/kubernetes/admission/certs/apiserver-client.key
contexts:
- name: image-policy
context:
cluster: image-policy-backend
user: kube-apiserver
current-context: image-policy
EOF
Step 5: Update the API Server Manifest
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add:
ImagePolicyWebhookto--enable-admission-plugins--admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml- Volume mounts for
/etc/kubernetes/admission
Step 6: Wait for API Server Restart
# Watch the API server come back
watch crictl ps | grep apiserver
# Or check with kubectl (may temporarily fail during restart)
kubectl get pods -n kube-system -l component=kube-apiserver
# Check API server logs for errors
sudo crictl logs $(sudo crictl ps --name kube-apiserver -q) 2>&1 | tail -20
Step 7: Test the Configuration
# With defaultAllow: false and no backend running, all Pods should be denied
kubectl run test --image=nginx:1.27
# Error from server (Forbidden): pods "test" is forbidden:
# image policy webhook backend denied one or more images: ...
7. Backend Server Implementation (Conceptual)
The webhook backend is a web server that:
- Listens on HTTPS
- Receives
ImageReviewPOST requests - Checks images against a policy (allowlist, scan results, signatures)
- Returns an
ImageReviewresponse
Minimal Go Backend (Concept)
package main
import (
"encoding/json"
"net/http"
"strings"
)
var allowedRegistries = []string{
"registry.company.com/",
"gcr.io/company-project/",
}
type ImageReview struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Spec ImageReviewSpec `json:"spec"`
Status ImageReviewStatus `json:"status"`
}
type ImageReviewSpec struct {
Containers []Container `json:"containers"`
Namespace string `json:"namespace"`
}
type Container struct {
Image string `json:"image"`
}
type ImageReviewStatus struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason,omitempty"`
}
func handleImagePolicy(w http.ResponseWriter, r *http.Request) {
var review ImageReview
json.NewDecoder(r.Body).Decode(&review)
allowed := true
reason := ""
for _, c := range review.Spec.Containers {
imageAllowed := false
for _, reg := range allowedRegistries {
if strings.HasPrefix(c.Image, reg) {
imageAllowed = true
break
}
}
if !imageAllowed {
allowed = false
reason = "Image " + c.Image + " is not from an allowed registry"
break
}
}
review.Status = ImageReviewStatus{
Allowed: allowed,
Reason: reason,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(review)
}
func main() {
http.HandleFunc("/image-policy", handleImagePolicy)
http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
}
CKS Exam Note: You likely won’t need to write a backend server on the exam. The exam tests your ability to configure the API server, create the AdmissionConfiguration, and set up the kubeconfig. The backend is provided or pre-configured.
8. ImagePolicyWebhook vs Other Admission Controls
| Feature | ImagePolicyWebhook | OPA Gatekeeper | Kyverno |
|---|---|---|---|
| Type | Built-in admission controller | Third-party webhook | Third-party webhook |
| Configuration | API server flags + config files | CRDs (ConstraintTemplate + Constraint) | CRDs (ClusterPolicy) |
| Policy language | External backend (any language) | Rego | YAML |
| Install complexity | Moderate (API server restart) | Helm chart | Helm chart |
| Image verification | Via backend logic | Via Rego rules | Native verifyImages |
| CKS exam relevance | High (API server config) | High (policy writing) | High (policy writing) |
| defaultAllow/Deny | Configurable | N/A (always deny on match) | N/A (Enforce/Audit) |
When to Use Each
- ImagePolicyWebhook — When you need a centralized, external decision service that the API server consults. Good for organizations with existing policy backends.
- OPA Gatekeeper — When you want policy-as-code with Rego. Powerful but steeper learning curve.
- Kyverno — When you want YAML-native policies with image verification built in. Easiest to write.
9. Troubleshooting
API Server Won’t Start After Changes
# Check the API server container logs
sudo crictl ps -a | grep apiserver
sudo crictl logs <container-id> 2>&1 | tail -30
# Common errors:
# 1. "failed to read admission config" → File path wrong or file doesn't exist
# 2. "unable to load kubeconfig" → Kubeconfig path wrong in admission-config.yaml
# 3. "certificate signed by unknown authority" → CA cert mismatch
# 4. "connection refused" → Backend not running (check defaultAllow setting)
Volume Mount Issues
# Verify the hostPath exists on the control plane node
ls -la /etc/kubernetes/admission/
ls -la /etc/kubernetes/admission/certs/
# Verify the volume mount in the API server manifest
kubectl get pod kube-apiserver-controlplane -n kube-system -o yaml | grep -A5 volumeMounts
Test with defaultAllow: true First
For debugging, temporarily set defaultAllow: true to confirm the API server starts correctly with the admission controller enabled. Then switch to false once the backend is working.
Summary
| Component | File / Flag | Purpose |
|---|---|---|
| Enable admission plugin | --enable-admission-plugins=ImagePolicyWebhook |
Activates the controller |
| Admission config | --admission-control-config-file=<path> |
Points to AdmissionConfiguration |
| AdmissionConfiguration | admission-config.yaml |
Configures TTLs, default behavior, kubeconfig path |
| Webhook kubeconfig | webhook-kubeconfig.yaml |
Backend URL, TLS certs, authentication |
defaultAllow: false |
In AdmissionConfiguration | Fail-closed — deny all if backend unreachable |
| Volume mounts | API server manifest | Makes config files available inside the static Pod |
Practice Questions
Question 1
You need to enable ImagePolicyWebhook on a kubeadm cluster. List the files you need to create and the API server flags you need to set.
Show Answer
Files to create:
/etc/kubernetes/admission/admission-config.yaml— AdmissionConfiguration withdefaultAllow,kubeConfigFile, TTL settings/etc/kubernetes/admission/webhook-kubeconfig.yaml— Kubeconfig with backend server URL, CA cert, client certs- TLS certificates in
/etc/kubernetes/admission/certs/— CA, server cert/key, client cert/key
API server flags to add in /etc/kubernetes/manifests/kube-apiserver.yaml:
--enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
--admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
Volume mounts to add:
volumeMounts:
- name: admission-config
mountPath: /etc/kubernetes/admission
readOnly: true
volumes:
- name: admission-config
hostPath:
path: /etc/kubernetes/admission
type: DirectoryOrCreate
Question 2
What happens when the ImagePolicyWebhook backend is unreachable and defaultAllow is set to true?
Show Answer
When the backend is unreachable and defaultAllow: true:
- All image pull requests are allowed — the admission controller silently approves every image
- This means any image from any registry can run in the cluster
- An attacker could intentionally DoS the backend to bypass the image policy
This is fail-open behavior and is insecure.
With defaultAllow: false:
- All image pull requests are denied when the backend is unreachable
- No new Pods can be created until the backend recovers
- This is fail-closed behavior and is the secure default
Always use
defaultAllow: falsein production.
Question 3
The API server is crashing after you enabled ImagePolicyWebhook. The logs show failed to read admission config at /etc/kubernetes/admission/admission-config.yaml. What is the likely cause?
Show Answer
The most likely causes:
-
Missing volume mount — The file exists on the host at
/etc/kubernetes/admission/admission-config.yamlbut the API server Pod doesn’t have a volume mount for that path.Fix: Add the volume and volume mount to the API server manifest:
volumeMounts: - name: admission-config mountPath: /etc/kubernetes/admission readOnly: true volumes: - name: admission-config hostPath: path: /etc/kubernetes/admission type: DirectoryOrCreate - File doesn’t exist — The file was not created at the expected path.
ls -la /etc/kubernetes/admission/admission-config.yaml - File permissions — The file is not readable by the API server process.
sudo chmod 644 /etc/kubernetes/admission/admission-config.yaml - Path mismatch — The path in
--admission-control-config-filedoesn’t match the actual hostPath + mountPath combination.
Question 4
Write the ImageReview JSON response that a backend would return to deny an image docker.io/malicious/backdoor:latest with a reason.
Show Answer
{
"apiVersion": "imagepolicy.k8s.io/v1alpha1",
"kind": "ImageReview",
"status": {
"allowed": false,
"reason": "Image 'docker.io/malicious/backdoor:latest' is not from an approved registry. Only images from registry.company.com are permitted."
}
}
The backend inspects the spec.containers[].image fields in the incoming ImageReview request, evaluates them against its policy, and returns status.allowed: false with a reason explaining why the image was rejected.
Question 5
Compare ImagePolicyWebhook with OPA Gatekeeper for restricting container images. When would you choose each?
Show Answer
| Criteria | ImagePolicyWebhook | OPA Gatekeeper |
|---|---|---|
| Setup | API server restart required | Helm install, no API server change |
| Policy location | External backend server | In-cluster CRDs (ConstraintTemplate + Constraint) |
| Flexibility | Any logic in any language (backend decides) | Rego-based policy (runs in cluster) |
| Availability | Depends on external backend uptime | Runs as Deployment in cluster |
| defaultAllow | Configurable fail-open/fail-closed | Always fail-closed on match |
| Audit mode | Not built-in | Yes (dryrun enforcement) |
Choose ImagePolicyWebhook when:
- You have an existing external policy service
- You need centralized policy decisions across multiple clusters
- The exam explicitly asks for it
Choose OPA Gatekeeper when:
- You want policies defined as Kubernetes resources (GitOps-friendly)
- You don’t want to manage an external backend
- You need audit mode to evaluate policy impact before enforcement
- You want to combine image restrictions with other policies (labels, resources, etc.)