Skip to content

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:

  1. ImagePolicyWebhook -- a built-in admission controller that delegates image decisions to an external webhook
  2. OPA/Gatekeeper -- a policy engine that can enforce image registry allowlists and naming conventions
  3. AlwaysPullImages -- an admission controller that forces fresh image pulls to prevent using stale cached images
  4. 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.

yaml
# /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: false

Key fields explained:

FieldDescription
kubeConfigFilePath to the kubeconfig file for connecting to the webhook
allowTTLSeconds to cache an "allow" decision
denyTTLSeconds to cache a "deny" decision
retryBackoffMilliseconds to wait before retrying a failed webhook call
defaultAllowWhether 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.

yaml
# /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.key

Understanding 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 service
  • clusters[].cluster.certificate-authority -- CA cert to verify the webhook server
  • users[].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:

yaml
# /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: File

Important Details

  1. Add ImagePolicyWebhook to the --enable-admission-plugins flag (comma-separated, no spaces)
  2. Set --admission-control-config-file to point to the AdmissionConfiguration file
  3. Ensure all referenced files are mounted into the API server pod via hostPath volumes
  4. After saving the manifest, wait for the API server to restart (kubelet manages static pods automatically)

Step 4: Verify the Configuration

bash
# 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 image

The ImageReview API

When the API server sends a request to the webhook, it sends an ImageReview object:

Request (API Server to Webhook)

json
{
  "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:

json
{
  "apiVersion": "imagepolicy.k8s.io/v1alpha1",
  "kind": "ImageReview",
  "status": {
    "allowed": true
  }
}

Deny:

json
{
  "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:

bash
# 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:

yaml
# BAD: Mutable tag -- can change without notice
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx:1.25
yaml
# GOOD: Immutable digest -- always the same image
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764e2d42b2f4627c31f2a616affb1

Finding the Digest

bash
# 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.25

AlwaysPullImages Admission Controller

The AlwaysPullImages admission controller mutates every new Pod to set imagePullPolicy: Always, regardless of what was specified.

Why It Matters

Without AlwaysPullImages:

  1. A node caches images after the first pull
  2. Any Pod on that node can use the cached image without authentication
  3. This allows unauthorized access to private images

Enabling AlwaysPullImages

yaml
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
    - name: kube-apiserver
      command:
        - kube-apiserver
        - --enable-admission-plugins=NodeRestriction,AlwaysPullImages

When 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

PolicyBehaviorSecurity Implication
AlwaysAlways pulls from registryMost secure; validates credentials every time
IfNotPresentUses cache if availableCached images bypass registry auth
NeverOnly uses cached imagesNo 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

yaml
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

yaml
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

bash
# 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 created

Constraint to Require Image Digests

yaml
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])
        }
yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireDigest
metadata:
  name: require-image-digest
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - production

Comparing Image Policy Approaches

FeatureImagePolicyWebhookOPA/Gatekeeper
Built into KubernetesYes (admission plugin)No (requires installation)
ComplexityHigh (multiple config files)Medium (CRDs + policies)
FlexibilityLimited (allow/deny binary)High (any OPA/Rego logic)
Custom logicRequires external webhook serverRego policies in-cluster
Error messagesBasic reason fieldDetailed violation messages
CKS exam focusHigh -- expect configuration tasksMedium -- understand concepts
CachingBuilt-in TTL cachingNo built-in caching
Failure modeConfigurable (defaultAllow)Configurable (failurePolicy)

Complete ImagePolicyWebhook Setup Checklist

Exam Checklist

Use this checklist when configuring ImagePolicyWebhook on the exam:

  1. Create or edit /etc/kubernetes/pki/admission_configuration.yaml
    • Set defaultAllow: false
    • Point kubeConfigFile to the webhook kubeconfig
  2. Create or edit the webhook kubeconfig file
    • Set the webhook server URL
    • Configure TLS certificates
  3. Edit /etc/kubernetes/manifests/kube-apiserver.yaml
    • Add ImagePolicyWebhook to --enable-admission-plugins
    • Add --admission-control-config-file pointing to the AdmissionConfiguration
    • Mount all referenced files as volumes
  4. Wait for the API server to restart
  5. Test with kubectl run test --image=<image> to verify policy enforcement

Key Takeaways

Summary

  1. ImagePolicyWebhook delegates image admission decisions to an external webhook service
  2. The AdmissionConfiguration file links the admission controller to the webhook kubeconfig
  3. defaultAllow: false is critical -- denies images when the webhook is unreachable
  4. Image digests (@sha256:...) are immutable and more secure than tags
  5. AlwaysPullImages admission controller prevents unauthorized use of cached images
  6. OPA/Gatekeeper provides flexible image policy enforcement with custom Rego policies
  7. All configuration files must be mounted into the API server pod via hostPath volumes

Common Exam Pitfalls

  • Forgetting to add --admission-control-config-file flag 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 the AlwaysPullImages admission controller
  • Not waiting for the API server to restart after modifying the static pod manifest

Released under the MIT License.