CKS — ImagePolicyWebhook

10 minute read

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:

  1. User submits a Pod creation request
  2. API server hits the ImagePolicyWebhook admission controller
  3. The controller constructs an ImageReview object containing all container images
  4. The ImageReview is sent to the external webhook backend via HTTPS
  5. The backend responds with allow: true or allow: false for each image
  6. 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 to true means 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 certificate
  • client-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:

  1. ImagePolicyWebhook to --enable-admission-plugins
  2. --admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
  3. 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:

  1. Listens on HTTPS
  2. Receives ImageReview POST requests
  3. Checks images against a policy (allowlist, scan results, signatures)
  4. Returns an ImageReview response

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:

  1. /etc/kubernetes/admission/admission-config.yaml — AdmissionConfiguration with defaultAllow, kubeConfigFile, TTL settings
  2. /etc/kubernetes/admission/webhook-kubeconfig.yaml — Kubeconfig with backend server URL, CA cert, client certs
  3. 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: false in 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:

  1. Missing volume mount — The file exists on the host at /etc/kubernetes/admission/admission-config.yaml but 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
    
  2. File doesn’t exist — The file was not created at the expected path.
    ls -la /etc/kubernetes/admission/admission-config.yaml
    
  3. File permissions — The file is not readable by the API server process.
    sudo chmod 644 /etc/kubernetes/admission/admission-config.yaml
    
  4. Path mismatch — The path in --admission-control-config-file doesn’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.)

Updated: