Allow Trusted Images with ImagePolicyWebhook

Configure Kubernetes ImagePolicyWebhook with an HTTPS ImageReview service, digest rules, TTL caching, and fail-open or fail-closed admission tests.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Kubernetes ImagePolicyWebhook admission flow with API server sending ImageReview requests to an HTTPS policy service that allows or denies container images
Tested on Rocky Linux 10.2 (Red Quartz) — kubeadm control plane 192.168.56.108
Package kubectl 1.36.3
kube-apiserver 1.36.3
Python 3.12 (stdlib HTTPS webhook)
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKS
Lab environment Multi-node kubeadm cluster with SSH to the control plane — install Kubernetes with kubeadm; pair with Cosign signing for digest trust, not signature checks inside this webhook
Privilege root on the control-plane node for static Pod manifest edits and webhook TLS files; cluster-admin kubectl for Pod tests
Scope ImagePolicyWebhook admission flow, trust policy design, minimal HTTPS ImageReview service, webhook TLS and kubeconfig, AdmissionConfiguration with defaultAllow and TTL caching, kube-apiserver plugin enablement, allowed and denied Pod tests, webhook-unavailable fail-closed versus fail-open behavior, and brief modern alternatives. Does not cover Cosign reimplementation, vulnerability scanning, registry setup, Kyverno or Gatekeeper policy languages, or application image builds.
Related guides Harden Kubernetes API server access
Kubernetes RBAC

ImagePolicyWebhook lets the API server ask an external HTTPS service whether Pod images are trusted before the Pod is stored. This walkthrough builds a small ImageReview server on a kubeadm control plane running Kubernetes 1.36.3, configures defaultAllow and TTL caching, enables the admission plugin on the static kube-apiserver manifest, and tests allowed, denied, and webhook-unavailable scenarios.

IMPORTANT
ImagePolicyWebhook uses defaultAllow when the backend is unreachable. failurePolicy belongs to ValidatingWebhookConfiguration and MutatingWebhookConfiguration—a different admission path. Do not mix the terminology.

Correct failure-behavior terminology

Mechanism Configuration field When it applies
ImagePolicyWebhook defaultAllow in AdmissionConfiguration Webhook backend unreachable or errors after retries
ValidatingWebhookConfiguration failurePolicy: Fail or Ignore Dynamic admission webhook call fails
MutatingWebhookConfiguration failurePolicy: Fail or Ignore Mutating webhook call fails

defaultAllow: false means fail-closed: if the ImageReview service is down, image admission is denied. defaultAllow: true means fail-open: Pods may be created without a policy decision when the backend is unavailable.


Understand the admission flow

text
Pod create/update request
Authentication
Authorization (RBAC)
ImagePolicyWebhook admission plugin
HTTPS POST with ImageReview JSON
Webhook returns allowed or denied
API server admits or rejects the Pod object

ImagePolicyWebhook runs during admission, before the Pod is persisted and scheduled. The webhook receives image reference strings from containers, initContainers, and ephemeralContainers. It does not pull images from a registry or scan layers.

Admission rejection happens before a Pod exists in etcd. Denied Pods are not persisted, so they have no Pod status or container logs. The denial is returned to the client and can also appear in API-server or audit logs.


Define a simple trust policy

The lab webhook enforces a narrow policy suitable for learning:

  • Allow only registry.k8s.io images
  • Require an immutable digest (@sha256:…)
  • Deny the :latest tag
  • Deny images outside registry.k8s.io
  • Return a clear reason string on denial

The code allows every repository under registry.k8s.io; it does not maintain a repository-level allow list.

Cosign verification and Trivy scanning stay outside this service. Production teams often call those tools from CI and store results as signed attestations; this webhook only enforces registry and digest rules at admission time.


Build the webhook service

Create the webhook directory on the control-plane node. The API server mounts the host path /opt/image-policy-webhook at /etc/kubernetes/image-policy-webhook inside the static Pod:

bash
mkdir -p /opt/image-policy-webhook/certs

Save the policy server as /opt/image-policy-webhook/server.py. It uses only the Python standard library—no Flask or FastAPI dependency:

python
#!/usr/bin/env python3
"""Minimal ImageReview webhook for ImagePolicyWebhook admission plugin."""
import json
import logging
import re
import ssl
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer

PORT = 8443
CERT = "/opt/image-policy-webhook/certs/tls.crt"
KEY = "/opt/image-policy-webhook/certs/tls.key"

SHA256_DIGEST = re.compile(r"^[0-9a-f]{64}$")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler("/var/log/image-policy-webhook.log"),
    ],
)
log = logging.getLogger("image-policy-webhook")


def evaluate_image(image: str) -> tuple[bool, str]:
    if not image:
        return False, "denied: empty image reference"

    if not image.startswith("registry.k8s.io/"):
        return False, f"denied: only registry.k8s.io is allowed: {image}"

    image_name = image.split("@", 1)[0]
    if image_name.endswith(":latest"):
        return False, f"denied: :latest is not allowed: {image}"

    _, separator, digest = image.rpartition("@sha256:")
    if not separator or not SHA256_DIGEST.fullmatch(digest):
        return False, f"denied: image must use a complete @sha256 digest: {image}"

    return True, ""


def evaluate_review(spec: dict) -> tuple[bool, str, list[str]]:
    containers = spec.get("containers") or []
    images = [container.get("image", "") for container in containers]

    if not images:
        return False, "denied: ImageReview contains no images", images

    for image in images:
        allowed, reason = evaluate_image(image)
        if not allowed:
            return False, reason, images

    return True, "", images


class Handler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        log.info("%s - %s", self.address_string(), fmt % args)

    def do_GET(self):
        if self.path in ("/healthz", "/healthz/"):
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"ok\n")
            return
        self.send_response(404)
        self.end_headers()

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)
        log.info("ImageReview request: %s", body.decode("utf-8", errors="replace")[:2000])
        review = json.loads(body)
        spec = review.get("spec") or {}
        allowed, reason, images = evaluate_review(spec)
        response = {
            "apiVersion": review.get("apiVersion", "imagepolicy.k8s.io/v1alpha1"),
            "kind": review.get("kind", "ImageReview"),
            "status": {"allowed": allowed, "reason": reason},
        }
        log.info(
            "ImageReview response: allowed=%s reason=%s images=%s",
            allowed,
            reason,
            images,
        )
        out = json.dumps(response).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(out)


def main():
    httpd = HTTPServer(("127.0.0.1", PORT), Handler)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(CERT, KEY)
    httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
    log.info("Image policy webhook listening on https://127.0.0.1:%s", PORT)
    httpd.serve_forever()


if __name__ == "__main__":
    main()
Output

Generate a TLS certificate signed by the cluster CA. The API server must trust the CA that signed the webhook certificate. This lab signs the certificate with the kubeadm cluster CA and embeds that CA in the webhook kubeconfig. A separate self-signed certificate can also work when that certificate is explicitly configured as the trusted CA.

bash
cd /opt/image-policy-webhook/certs
openssl req -newkey rsa:2048 -keyout tls.key -out tls.csr -nodes \
  -subj "/CN=image-policy-webhook" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:192.168.56.108"
openssl x509 -req -in tls.csr \
  -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key \
  -CAcreateserial -out tls.crt -days 365 \
  -extensions v3_req \
  -extfile <(printf "[v3_req]\nsubjectAltName=DNS:localhost,IP:127.0.0.1,IP:192.168.56.108\n")
bash
chmod 600 /opt/image-policy-webhook/certs/tls.key

Install a systemd unit so the webhook starts on boot:

bash
cat >/etc/systemd/system/image-policy-webhook.service <<'EOF'
[Unit]
Description=Kubernetes ImagePolicyWebhook backend
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/python3 /opt/image-policy-webhook/server.py
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF
bash
systemctl daemon-reload
systemctl enable --now image-policy-webhook

Confirm the health endpoint responds over HTTPS from the control-plane host:

bash
curl --cacert /etc/kubernetes/pki/ca.crt https://127.0.0.1:8443/healthz
output
ok

The API server static Pod calls the webhook at https://127.0.0.1:8443 from inside the control-plane node network namespace.


Configure webhook TLS and kubeconfig

The API server reads a kubeconfig file that points at the webhook HTTPS endpoint and trusts the CA certificate.

Create /opt/image-policy-webhook/kubeconfig with the cluster CA the API server already trusts:

bash
CA_DATA=$(base64 -w0 /etc/kubernetes/pki/ca.crt)

cat >/opt/image-policy-webhook/kubeconfig <<EOF
apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority-data: ${CA_DATA}
    server: https://127.0.0.1:8443
  name: image-policy-webhook
contexts:
- context:
    cluster: image-policy-webhook
  name: default
current-context: default
users: []
EOF

Verify that the placeholder is gone:

bash
grep certificate-authority-data /opt/image-policy-webhook/kubeconfig

certificate-authority-data must contain base64-encoded PEM data.

Paths under /etc/kubernetes/image-policy-webhook/ are valid inside the API server container because the host directory is bind-mounted there.

Test reachability from the control-plane host before you edit the API server:

bash
curl --cacert /etc/kubernetes/pki/ca.crt https://127.0.0.1:8443/healthz
output
ok

The health check proves only that HTTPS responds. Before you modify the API server, send an ImageReview that contains one trusted and one untrusted image:

bash
curl --cacert /etc/kubernetes/pki/ca.crt \
  -H 'Content-Type: application/json' \
  --data-binary @- \
  https://127.0.0.1:8443/ <<'EOF'
{
  "apiVersion": "imagepolicy.k8s.io/v1alpha1",
  "kind": "ImageReview",
  "spec": {
    "namespace": "image-policy-test",
    "containers": [
      {
        "image": "registry.k8s.io/pause@sha256:278fb9dbcca9518083ad1e11276933a2e96f23de604a3a08cc3c80002767d24c"
      },
      {
        "image": "registry.k8s.io/pause:3.10"
      }
    ]
  }
}
EOF
output
{"apiVersion": "imagepolicy.k8s.io/v1alpha1", "kind": "ImageReview", "status": {"allowed": false, "reason": "denied: image must use a complete @sha256 digest: registry.k8s.io/pause:3.10"}}

status.allowed: false on the second image proves the webhook evaluates every entry in spec.containers[], not only the first container.

Client certificate authentication is optional for this lab. Production deployments often add mutual TLS between the API server and the policy service.


Create ImagePolicyWebhook configuration

This lab keeps the ImagePolicyWebhook settings in a separate plugin-config.yaml and references it through plugins[].path. Upstream also supports inline plugins[].configuration.imagePolicy; an unknown field "imagePolicy" error usually means the file was malformed or decoded as the wrong configuration type, not that inline configuration is unsupported generally.

Create /opt/image-policy-webhook/admission-config.yaml:

yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
  path: /etc/kubernetes/image-policy-webhook/plugin-config.yaml

Create /opt/image-policy-webhook/plugin-config.yaml:

yaml
imagePolicy:
  kubeConfigFile: /etc/kubernetes/image-policy-webhook/kubeconfig
  allowTTL: 30
  denyTTL: 30
  retryBackoff: 500
  defaultAllow: false

Field meanings:

Field Lab value Effect
kubeConfigFile Path inside API server mount HTTPS endpoint and CA trust
allowTTL 30 seconds Cache allow decisions to reduce webhook load
denyTTL 30 seconds Cache deny decisions
retryBackoff 500 ms Wait between webhook retries on error
defaultAllow false Fail-closed when the webhook is unreachable

Start with defaultAllow: false in production-minded labs. Changing defaultAllow requires editing plugin-config.yaml and restarting the API server—the admission files are read at apiserver start, not hot-reloaded.


Enable the admission plugin

Back up the static Pod manifest outside the watched manifests directory, then add the ImagePolicyWebhook plugin and admission configuration file.

bash
install -d -m 700 /etc/kubernetes/backup

cp -a /etc/kubernetes/manifests/kube-apiserver.yaml \
  "/etc/kubernetes/backup/kube-apiserver.yaml.$(date +%F-%H%M%S)"

Add or extend these flags in the kube-apiserver container command array:

text
--enable-admission-plugins=NodeRestriction,ImagePolicyWebhook
--admission-control-config-file=/etc/kubernetes/image-policy-webhook/admission-config.yaml
--runtime-config=imagepolicy.k8s.io/v1alpha1=true

Upstream documentation still instructs administrators to enable the alpha ImageReview API group. This lab cluster appeared to operate without the flag during testing, but include it for a reproducible workflow.

ImagePolicyWebhook does not control the actual kubeadm static Pods because the kubelet starts them directly without API-server admission. It can reject their mirror Pod objects, while the underlying static Pods continue running. With this digest-only policy, mirror objects for the API server, scheduler, controller manager, or etcd may fail to register unless their image references also satisfy the policy.

Static Pods explicitly bypass API-server admission; only their mirror objects are submitted to the API.

Add a volume mount and hostPath volume so the API server container can read webhook files:

yaml
volumeMounts:
    - mountPath: /etc/kubernetes/image-policy-webhook
      name: image-policy-webhook
      readOnly: true
    volumes:
    - hostPath:
        path: /opt/image-policy-webhook
        type: Directory
      name: image-policy-webhook

The kubelet recreates the API server Pod when the manifest changes. Wait until the API server returns a successful /readyz response:

bash
crictl ps --name kube-apiserver
output
CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD                     NAMESPACE
dd97d10404b81       3400718b4dd57       2 minutes ago       Running             kube-apiserver      1                   6e99995046d89       kube-apiserver-k8s-cp   kube-system
bash
until kubectl get --raw=/readyz >/dev/null 2>&1; do
  sleep 2
done

kubectl get --raw=/readyz
kubectl get nodes
output
ok
NAME       STATUS   ROLES           AGE   VERSION
k8s-cp     Ready    control-plane   19h   v1.36.3
worker01   Ready    <none>          19h   v1.36.3

The API-server container can be healthy in crictl even when its mirror Pod is missing from kubectl get pods -n kube-system. Static Pods run under kubelet supervision; mirror creation is a separate API admission step that this digest-only policy can reject.

Confirm kubectl get nodes still succeeds before you run Pod admission tests.


Test an allowed Pod

Create a test namespace and a Pod that satisfies the lab policy—registry.k8s.io image with a pinned digest:

bash
kubectl create namespace image-policy-test
bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: ipw-test-a
  namespace: image-policy-test
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause@sha256:278fb9dbcca9518083ad1e11276933a2e96f23de604a3a08cc3c80002767d24c
EOF
output
pod/ipw-test-a created

Admission succeeded. Confirm the Pod object exists separately from image pull:

bash
kubectl get pod ipw-test-a -o wide
output
NAME         READY   STATUS    RESTARTS   AGE   IP       NODE       NOMINATED NODE   READINESS GATES
ipw-test-a   0/1     Pending   0          12s   <none>   worker01   <none>           <none>

A Pending Pod still means admission passed. Image pull success depends on node connectivity to registry.k8s.io. Use the repository manifest digest from crictl inspecti or skopeo inspect, not a local image layer ID—a wrong digest such as sha256:873ed751… can pass admission but fail with ImagePullBackOff.


Test denied Pods

Mutable tag without digest

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: tag-only-pause
  namespace: image-policy-test
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause:3.10
EOF
output
Error from server (Forbidden): error when creating "STDIN": pods "tag-only-pause" is forbidden: image policy webhook backend denied one or more images: denied: image must use a complete @sha256 digest: registry.k8s.io/pause:3.10

Unapproved registry

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: dockerhub-nginx
  namespace: image-policy-test
spec:
  containers:
  - name: nginx
    image: docker.io/nginx:latest
EOF
output
Error from server (Forbidden): error when creating "STDIN": pods "dockerhub-nginx" is forbidden: image policy webhook backend denied one or more images: denied: only registry.k8s.io is allowed: docker.io/nginx:latest

The registry allow list is checked before digest and :latest rules, so foreign images are denied with the registry reason first.

Explicit :latest tag

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: latest-pause
  namespace: image-policy-test
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause:latest
EOF
output
Error from server (Forbidden): error when creating "STDIN": pods "latest-pause" is forbidden: image policy webhook backend denied one or more images: denied: :latest is not allowed: registry.k8s.io/pause:latest

Denied Pods never reach the API store, so kubectl logs returns NotFound and no workload logs exist.

bash
kubectl get pod tag-only-pause -n image-policy-test
output
Error from server (NotFound): pods "tag-only-pause" not found

Test webhook unavailability

Stop the webhook service and attempt another allowed Pod with defaultAllow: false. Add a unique forwarded annotation so the result is not served from the allowTTL: 30 cache:

bash
systemctl stop image-policy-webhook
bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: ipw-test-fail-closed
  namespace: image-policy-test
  annotations:
    lab.image-policy.k8s.io/cache-buster: "fail-closed-1"
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause@sha256:278fb9dbcca9518083ad1e11276933a2e96f23de604a3a08cc3c80002767d24c
EOF
output
Error from server (Forbidden): error when creating "STDIN": pods "ipw-test-fail-closed" is forbidden: error while calling image policy webhook: Post "https://127.0.0.1:8443/?timeout=30s": dial tcp 127.0.0.1:8443: connect: connection refused

With defaultAllow: false, the API server fails closed and rejects the Pod.

Edit /opt/image-policy-webhook/plugin-config.yaml to set defaultAllow: true, then restart the API server so it reloads admission configuration. plugin-config.yaml is read at API-server startup; touching an unchanged static-Pod manifest does not provide a reliable content change:

bash
APISERVER_ID=$(
  crictl ps --name kube-apiserver -q | head -n 1
)

crictl stop "$APISERVER_ID"

until kubectl get --raw=/readyz >/dev/null 2>&1; do
  sleep 2
done

Wait until the API server returns a successful /readyz response, then re-run the Pod manifest with a different cache-buster value. The webhook is still stopped from the fail-closed test:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: ipw-test-fail-open
  namespace: image-policy-test
  annotations:
    lab.image-policy.k8s.io/cache-buster: "fail-open-1"
spec:
  containers:
  - name: pause
    image: registry.k8s.io/pause@sha256:278fb9dbcca9518083ad1e11276933a2e96f23de604a3a08cc3c80002767d24c
EOF
output
pod/ipw-test-fail-open created

Fail-open behavior keeps Pod admission available when the policy service is down, but Pods may be admitted without an image-policy decision. The scheduler acts after admission; defaultAllow controls the admission decision when the webhook call fails. Restore production posture after the test:

bash
systemctl start image-policy-webhook

Set defaultAllow back to false in plugin-config.yaml and stop the API server container with the same crictl stop method so the kubelet recreates it with the restored settings.


ImagePolicyWebhook alternatives

ImagePolicyWebhook is a built-in plugin with a fixed ImageReview contract. Many teams adopt one of these instead or in addition:

Approach Best for
ValidatingAdmissionPolicy In-cluster CEL policies without operating a separate language runtime
ValidatingAdmissionWebhook Custom HTTP admission with failurePolicy and match constraints
Kyverno YAML-native policies including image registry rules
Gatekeeper OPA Rego policies with audit and enforce modes
Sigstore policy-controller Cosign signature and attestation enforcement at admission

This article implements ImagePolicyWebhook only. Link to Cosign for signature verification rather than reimplementing it in Python.


Inspect verification output

When debugging, tail webhook logs:

bash
journalctl -u image-policy-webhook -f
output
ImageReview request: {"apiVersion":"imagepolicy.k8s.io/v1alpha1","kind":"ImageReview","spec":{"containers":[{"image":"registry.k8s.io/pause:3.10"}]}}
ImageReview response: allowed=False reason=denied: image must use a complete @sha256 digest: registry.k8s.io/pause:3.10 images=['registry.k8s.io/pause:3.10']

Each log line corresponds to an API server ImageReview POST. Pair logs with kubectl errors to see which image reference triggered a deny reason.

ImageReview responses include:

  • status.allowed — boolean admission decision
  • status.reason — human-readable deny text returned to the API server
  • Request spec.containers[].image — evaluated references
  • Optional spec.annotations matching *.image-policy.k8s.io/* for break-glass workflows

Troubleshooting

Symptom Likely cause Fix
API server CrashLoop after manifest edit Invalid flag or missing volume mount Restore backup from /etc/kubernetes/backup/; fix paths under /opt/image-policy-webhook
defaultAllow change has no effect Admission config read only at apiserver start Stop the kube-apiserver container with crictl stop after editing plugin-config.yaml
connection refused on Pod create Webhook service stopped or wrong port systemctl status image-policy-webhook; confirm 8443 listening
TLS verify error from API server Webhook CA not trusted by API server Sign the webhook cert with /etc/kubernetes/pki/ca.crt; set certificate-authority-data to that CA in kubeconfig
unknown field "imagePolicy" on apiserver start Malformed nesting or wrong configuration file type Use plugins[].path pointing at a separate plugin-config.yaml, or fix inline configuration.imagePolicy structure
Allowed Pod still ImagePullBackOff Wrong digest (layer ID vs manifest digest) Resolve manifest digest with skopeo inspect; admission can pass while pull fails
Policy change not effective immediately allowTTL / denyTTL cache Wait for TTL expiry or restart API server during lab tests

What's Next


References


Summary

ImagePolicyWebhook sends ImageReview requests to your HTTPS backend before Pods are admitted. Configure defaultAllow in plugin-config.yaml, TTL caching, and a kubeconfig backed by the cluster CA, then enable the plugin on the static kube-apiserver manifest.

The lab allowed registry.k8s.io/pause@sha256:278fb9dbcca9518083ad1e11276933a2e96f23de604a3a08cc3c80002767d24c, denied mutable tags and foreign registries with explicit reasons, and demonstrated fail-closed behavior when the webhook stopped with defaultAllow: false. Pair digest and registry rules with Cosign verification and Trivy scanning in your supply-chain pipeline.


Frequently Asked Questions

1. What is the difference between defaultAllow and failurePolicy?

ImagePolicyWebhook uses defaultAllow in AdmissionConfiguration to decide whether Pods are admitted when the webhook backend is unreachable. ValidatingWebhookConfiguration and MutatingWebhookConfiguration use failurePolicy instead. Do not call defaultAllow failurePolicy—they are different admission mechanisms.

2. Does ImagePolicyWebhook pull or scan container images?

No. The API server sends image reference strings in an ImageReview object. Your webhook evaluates registry, tag, digest, or annotations. Vulnerability scanning and Cosign verification belong in separate pipeline stages or policy engines.

3. Should defaultAllow be true or false in production?

Set defaultAllow to false for fail-closed behavior when the webhook is down. Set it to true only when you accept fail-open risk during migration or when another control still blocks untrusted images. Document the trade-off in your runbook.

4. Is ImagePolicyWebhook the only way to restrict images in Kubernetes?

No. Modern clusters often use ValidatingAdmissionPolicy, ValidatingAdmissionWebhook, Kyverno, Gatekeeper, or Sigstore policy-controller. ImagePolicyWebhook is a built-in plugin with a fixed ImageReview contract—useful for CKS labs and simple registry allow lists.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)