Stop untrusted images at the API with ImagePolicyWebhook

Scanning images in CI is necessary. It is not sufficient. Someone can still kubectl run an image the pipeline never saw. ImagePolicyWebhook is the admission plugin that asks an external service, at create time, whether those image names are allowed.

If the webhook is down and defaultAllow is false, the API server denies the pod. That is the secure default. It is also how you lock yourself out if the webhook is broken.

Enable the plugin

Edit the API server static pod:

sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
--enable-admission-plugins=NodeRestriction,ResourceQuota,ImagePolicyWebhook
--admission-control-config-file=/etc/kubernetes/admission-control.yaml

Admission config

# /etc/kubernetes/admission-control.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
  configuration:
    imagePolicy:
      kubeConfigFile: /etc/kubernetes/imagepolicy-webhook.kubeconfig
      allowTTL: 30
      denyTTL: 30
      retryBackoff: 500
      defaultAllow: false

defaultAllow: false means a timeout or a dead webhook is a deny. Set it true only to recover, not as policy.

Webhook kubeconfig

# /etc/kubernetes/imagepolicy-webhook.kubeconfig
apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority: /etc/kubernetes/pki/ca.crt
    server: https://image-policy-webhook.example.com:8443/image-policy
  name: image-policy-webhook
contexts:
- context:
    cluster: image-policy-webhook
    user: api-server
  name: image-policy-webhook
current-context: image-policy-webhook
users:
- name: api-server
  user:
    client-certificate: /etc/kubernetes/pki/apiserver.crt
    client-key: /etc/kubernetes/pki/apiserver.key

The API server is the client. The webhook must present TLS. Paths in this file have to exist on the control plane node, inside the API server pod’s filesystem.

What the webhook sees

The review object is imagepolicy.k8s.io/v1alpha1 ImageReview. The service looks at spec.containers[].image and returns status.allowed plus an optional reason. Policy applies to every container in the pod. One bad image fails the whole pod.

A typical allow-list is registries, not image names:

  • gcr.io/my-project
  • registry.k8s.io
  • docker.io/library

Common extra rules: no :latest, no path that looks like an untrusted mirror.

Prove it

kubectl run test-pod --image=malicious-registry.com/bad-image:latest
kubectl run good-pod --image=gcr.io/my-company/app:v1.0

The first should fail at admission. The second should schedule.

When it does nothing

kubectl get pod -n kube-system kube-apiserver-master -o yaml | grep admission-plugins
sudo cat /etc/kubernetes/admission-control.yaml
kubectl logs -n kube-system kube-apiserver-master | grep ImagePolicyWebhook
kubectl get pods -n kube-system -l app=image-policy-webhook

Wrong kubeconfig path, expired webhook certs, and defaultAllow: true are the usual reasons a cluster looks like it has a policy and still accepts anything.

Files to remember:

  • /etc/kubernetes/manifests/kube-apiserver.yaml
  • /etc/kubernetes/admission-control.yaml
  • /etc/kubernetes/imagepolicy-webhook.kubeconfig