Skip to main content
CleanStart

Knowledge Hub

OPA Gatekeeper Policies for CleanStart Image Enforcement

Why OPA Gatekeeper

Open Policy Agent (OPA) Gatekeeper is a Kubernetes admission controller that uses Rego — a purpose-built policy language — to define and enforce policies. If your organization already uses OPA for API authorization, Terraform policy checks, or microservice authorization, Gatekeeper gives you a unified policy language across your entire stack.

For Kyverno policies (YAML-native, no Rego required), see: Kyverno Policies

OPA Gatekeeper has several key strengths. Rego is expressive and handles complex conditional logic, external data, and aggregation. It provides a shared language across Kubernetes, Terraform (Sentinel/Conftest), CI/CD, and APIs. The ConstraintTemplate pattern separates policy logic from parameters. Dry-run mode and audit capabilities are built-in. There is a large community library (gatekeeper-library) available for reference implementations.

Installing Gatekeeper

# Install Gatekeeper via Helmhelm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/chartshelm repo update helm install gatekeeper gatekeeper/gatekeeper \  --namespace gatekeeper-system \  --create-namespace \  --set replicas=3 \  --set audit.replicas=1 \  --set audit.logLevel=INFO # Verify installationkubectl get pods -n gatekeeper-system# Expected: 3 gatekeeper-controller pods + 1 gatekeeper-audit pod # Verify webhookkubectl get validatingwebhookconfigurations | grep gatekeeper

How Gatekeeper Works: Templates + Constraints

Gatekeeper uses a two-layer pattern to separate policy definition from configuration:

  1. ConstraintTemplate — Defines the reusable policy logic in Rego
  2. Constraint — Instantiates the template with specific parameters

The template serves as the rule engine, while the constraint provides the configuration. For example, a ConstraintTemplate called "Allowed Registries" contains the Rego logic to check whether an image registry is in the allowed list. You then create individual constraints from this template, such as "CleanStart Only" (which allows only registry.cleanstart.com) or "CleanStart + Istio" (which allows both registry.cleanstart.com and docker.io/istio). This separation allows you to reuse the same policy logic across multiple configurations.

Policy 1: Require CleanStart Registry

ConstraintTemplate

# template-allowed-registries.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:  name: k8sallowedregistries  annotations:    description: Restricts container images to approved registriesspec:  crd:    spec:      names:        kind: K8sAllowedRegistries      validation:        openAPIV3Schema:          type: object          properties:            registries:              type: array              description: List of allowed container image registries              items:                type: string            exemptImages:              type: array              description: Images exempt from registry check (for sidecars)              items:                type: string  targets:    - target: admission.k8s.gatekeeper.sh      rego: |        package k8sallowedregistries         violation[{"msg": msg}] {          container := input_containers[_]          not is_exempt(container.image)          not registry_allowed(container.image)          msg := sprintf(            "Container '%s' uses image '%s' which is not from an approved registry. Approved registries: %v",            [container.name, container.image, input.parameters.registries]          )        }         registry_allowed(image) {          registry := input.parameters.registries[_]          startswith(image, registry)        }         is_exempt(image) {          exempt := input.parameters.exemptImages[_]          startswith(image, exempt)        }         input_containers[c] {          c := input.review.object.spec.containers[_]        }        input_containers[c] {          c := input.review.object.spec.initContainers[_]        }        input_containers[c] {          c := input.review.object.spec.ephemeralContainers[_]        }

Constraint

# constraint-cleanstart-registry.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sAllowedRegistriesmetadata:  name: require-cleanstart-registryspec:  enforcementAction: deny    # deny | dryrun | warn  match:    kinds:      - apiGroups: [""]        kinds: ["Pod"]    excludedNamespaces:      - kube-system      - gatekeeper-system      - monitoring  parameters:    registries:      - "registry.cleanstart.com/"    exemptImages:      # Add sidecars and infrastructure images here      # - "docker.io/istio/"      # - "gcr.io/datadoghq/"

Apply and test:

kubectl apply -f template-allowed-registries.yamlkubectl apply -f constraint-cleanstart-registry.yaml # Wait for template to syncsleep 5 # Test: CleanStart image (should SUCCEED)kubectl run test-pass \  --image=registry.cleanstart.com/python3:3.12.5-prod \  --dry-run=server# Output: pod/test-pass created (server dry run) # Test: Docker Hub image (should FAIL)kubectl run test-fail --image=nginx:latest --dry-run=server# Output: Error: admission webhook "validation.gatekeeper.sh" denied the request:#   Container 'test-fail' uses image 'nginx:latest' which is not from an approved registry.

Policy 2: Enforce Pod Security Standards

ConstraintTemplate

# template-cleanstart-pod-security.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:  name: k8scleanstartpodsecurity  annotations:    description: Enforces CleanStart pod security requirementsspec:  crd:    spec:      names:        kind: K8sCleanStartPodSecurity      validation:        openAPIV3Schema:          type: object          properties:            allowedRunAsUser:              type: integer              description: Required UID for containers (default 65532)            requireReadOnlyRootFilesystem:              type: boolean              description: Whether read-only root FS is required            requireDropAllCapabilities:              type: boolean              description: Whether dropping ALL capabilities is required  targets:    - target: admission.k8s.gatekeeper.sh      rego: |        package k8scleanstartpodsecurity         # Rule 1: Must run as non-root        violation[{"msg": msg}] {          container := input_containers[_]          not has_field(container, "securityContext")          msg := sprintf("Container '%s' must define securityContext with runAsNonRoot: true", [container.name])        }         violation[{"msg": msg}] {          container := input_containers[_]          container.securityContext.runAsNonRoot != true          msg := sprintf("Container '%s' must set securityContext.runAsNonRoot to true", [container.name])        }         # Rule 2: Read-only root filesystem        violation[{"msg": msg}] {          input.parameters.requireReadOnlyRootFilesystem == true          container := input_containers[_]          not container.securityContext.readOnlyRootFilesystem          msg := sprintf(            "Container '%s' must set readOnlyRootFilesystem: true. Use emptyDir volumes for writable paths (/tmp, /var/run, etc.)",            [container.name]          )        }         # Rule 3: No privilege escalation        violation[{"msg": msg}] {          container := input_containers[_]          container.securityContext.allowPrivilegeEscalation == true          msg := sprintf("Container '%s' must not allow privilege escalation", [container.name])        }         # Rule 4: No privileged containers        violation[{"msg": msg}] {          container := input_containers[_]          container.securityContext.privileged == true          msg := sprintf("Container '%s' must not run as privileged", [container.name])        }         # Rule 5: Drop ALL capabilities        violation[{"msg": msg}] {          input.parameters.requireDropAllCapabilities == true          container := input_containers[_]          not has_drop_all(container)          msg := sprintf(            "Container '%s' must drop ALL capabilities. Set securityContext.capabilities.drop: [\"ALL\"]",            [container.name]          )        }         # Rule 6: No hostPath volumes        violation[{"msg": msg}] {          volume := input.review.object.spec.volumes[_]          has_field(volume, "hostPath")          msg := sprintf("Volume '%s' uses hostPath which is not allowed", [volume.name])        }         # Rule 7: No host networking        violation[{"msg": msg}] {          input.review.object.spec.hostNetwork == true          msg := "Host networking is not allowed"        }         violation[{"msg": msg}] {          input.review.object.spec.hostPID == true          msg := "Host PID namespace sharing is not allowed"        }         # Helper functions        has_drop_all(container) {          caps := container.securityContext.capabilities.drop          caps[_] == "ALL"        }         has_field(obj, field) {          _ = obj[field]        }         input_containers[c] {          c := input.review.object.spec.containers[_]        }        input_containers[c] {          c := input.review.object.spec.initContainers[_]        }

Constraint

# constraint-cleanstart-pod-security.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sCleanStartPodSecuritymetadata:  name: enforce-cleanstart-pod-securityspec:  enforcementAction: deny  match:    kinds:      - apiGroups: [""]        kinds: ["Pod"]    excludedNamespaces:      - kube-system      - gatekeeper-system  parameters:    allowedRunAsUser: 65532    requireReadOnlyRootFilesystem: true    requireDropAllCapabilities: true

Test:

kubectl apply -f template-cleanstart-pod-security.yamlkubectl apply -f constraint-cleanstart-pod-security.yaml # Compliant pod (should SUCCEED)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata:  name: secure-podspec:  securityContext:    runAsNonRoot: true    runAsUser: 65532  containers:    - name: app      image: registry.cleanstart.com/python3:3.12.5-prod      securityContext:        runAsNonRoot: true        readOnlyRootFilesystem: true        allowPrivilegeEscalation: false        privileged: false        capabilities:          drop: ["ALL"]      volumeMounts:        - name: tmp          mountPath: /tmp  volumes:    - name: tmp      emptyDir: {}EOF # Non-compliant pod (should FAIL with multiple violations)cat <<'EOF' | kubectl apply --dry-run=server -f -apiVersion: v1kind: Podmetadata:  name: insecure-podspec:  hostNetwork: true  containers:    - name: app      image: registry.cleanstart.com/python3:3.12.5-prod      securityContext:        privileged: true        allowPrivilegeEscalation: true  volumes:    - name: host      hostPath:        path: /etcEOF# Output: multiple violations (privileged, hostNetwork, hostPath, no drop ALL, etc.)

Policy 3: Block Latest and Mutable Tags

# template-block-latest-tag.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:  name: k8sblocklatestagspec:  crd:    spec:      names:        kind: K8sBlockLatestTag      validation:        openAPIV3Schema:          type: object          properties:            blockedTags:              type: array              items:                type: string  targets:    - target: admission.k8s.gatekeeper.sh      rego: |        package k8sblocklatestag         violation[{"msg": msg}] {          container := input_containers[_]          tag := get_tag(container.image)          blocked := input.parameters.blockedTags[_]          tag == blocked          msg := sprintf(            "Container '%s' uses blocked tag ':%s'. Use a specific version tag (e.g., :3.12.5-prod) or digest (@sha256:...).",            [container.name, tag]          )        }         violation[{"msg": msg}] {          container := input_containers[_]          not contains(container.image, ":")          not contains(container.image, "@")          msg := sprintf(            "Container '%s' uses image '%s' without a tag or digest. Specify a version tag or digest.",            [container.name, container.image]          )        }         get_tag(image) = tag {          contains(image, ":")          not contains(image, "@")          parts := split(image, ":")          tag := parts[count(parts) - 1]        }         input_containers[c] {          c := input.review.object.spec.containers[_]        }        input_containers[c] {          c := input.review.object.spec.initContainers[_]        }---# constraint-block-latest-tag.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sBlockLatestTagmetadata:  name: block-mutable-tagsspec:  enforcementAction: deny  match:    kinds:      - apiGroups: [""]        kinds: ["Pod"]  parameters:    blockedTags:      - "latest"      - "dev"      - "nightly"      - "edge"

Policy 4: Require Resource Limits

# template-require-resources.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:  name: k8srequireresourcesspec:  crd:    spec:      names:        kind: K8sRequireResources      validation:        openAPIV3Schema:          type: object          properties:            maxMemoryLimit:              type: string              description: Maximum memory limit allowed            maxCpuLimit:              type: string              description: Maximum CPU limit allowed  targets:    - target: admission.k8s.gatekeeper.sh      rego: |        package k8srequireresources         violation[{"msg": msg}] {          container := input_containers[_]          not has_resources(container)          msg := sprintf(            "Container '%s' must define resource requests and limits. CleanStart images are minimal — start with cpu: 100m/500m and memory: 128Mi/512Mi.",            [container.name]          )        }         violation[{"msg": msg}] {          container := input_containers[_]          has_resources(container)          not container.resources.requests.cpu          msg := sprintf("Container '%s' must define resources.requests.cpu", [container.name])        }         violation[{"msg": msg}] {          container := input_containers[_]          has_resources(container)          not container.resources.requests.memory          msg := sprintf("Container '%s' must define resources.requests.memory", [container.name])        }         violation[{"msg": msg}] {          container := input_containers[_]          has_resources(container)          not container.resources.limits.memory          msg := sprintf("Container '%s' must define resources.limits.memory", [container.name])        }         has_resources(container) {          _ = container.resources        }         input_containers[c] {          c := input.review.object.spec.containers[_]        }---# constraint-require-resources.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequireResourcesmetadata:  name: require-resource-definitionsspec:  enforcementAction: deny  match:    kinds:      - apiGroups: [""]        kinds: ["Pod"]    excludedNamespaces:      - kube-system      - gatekeeper-system

Policy 5: Require Image Digest in Production

# template-require-digest.yamlapiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata:  name: k8srequiredigestspec:  crd:    spec:      names:        kind: K8sRequireDigest  targets:    - target: admission.k8s.gatekeeper.sh      rego: |        package k8srequiredigest         violation[{"msg": msg}] {          container := input_containers[_]          not contains(container.image, "@sha256:")          msg := sprintf(            "Container '%s' uses image '%s' without a digest. In production, use digest references (e.g., registry.cleanstart.com/python3@sha256:abc...). Get digest: crane digest %s",            [container.name, container.image, container.image]          )        }         input_containers[c] {          c := input.review.object.spec.containers[_]        }        input_containers[c] {          c := input.review.object.spec.initContainers[_]        }---# constraint-require-digest-production.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequireDigestmetadata:  name: require-digest-in-productionspec:  enforcementAction: deny  match:    kinds:      - apiGroups: [""]        kinds: ["Pod"]    namespaces:      - production      - prod-*

Auditing and Reporting

View All Violations

# List all constraints and their violation countskubectl get constraints -o wide # Example output:# NAME                            ENFORCEMENT-ACTION   TOTAL-VIOLATIONS# require-cleanstart-registry     deny                 3# enforce-cleanstart-pod-security deny                 12# block-mutable-tags              deny                 1 # Get details on specific violationskubectl get K8sAllowedRegistries require-cleanstart-registry -o yaml # The status section shows every violation:# status:#   violations:#     - enforcementAction: deny#       kind: Pod#       name: legacy-app#       namespace: default#       message: "Container 'legacy' uses image 'nginx:latest'..."

Audit Existing Resources

Gatekeeper automatically audits existing resources against new policies:

# Check audit resultskubectl get constraints -o json | \  jq '.items[] | {name: .metadata.name, violations: (.status.violations // [] | length)}' # Output:# {"name": "require-cleanstart-registry", "violations": 3}# {"name": "enforce-cleanstart-pod-security", "violations": 12}# {"name": "block-mutable-tags", "violations": 1}

Export Violations for Compliance

# Export all violations as JSON (for SIEM, compliance dashboards)kubectl get constraints -o json | \  jq '[.items[] | .status.violations[]? | {    policy: .constraintRef.name,    kind: .kind,    namespace: .namespace,    name: .name,    message: .message  }]' > violations-report.json # Count violations by namespacekubectl get constraints -o json | \  jq '[.items[].status.violations[]?.namespace] | group_by(.) | map({namespace: .[0], count: length})'

Rollout Strategy

Phase 1: Observe (Week 1)

Deploy all policies in dryrun mode:

# All constraints with:spec:  enforcementAction: dryrun # Monitor violations without blocking anythingkubectl get constraints -o wide# Fix what you can, document what needs exceptions

Phase 2: Warn (Week 2)

Switch to warn mode — kubectl shows warnings but allows the request:

spec:  enforcementAction: warn # Users see: Warning: [require-cleanstart-registry] Container uses non-approved registry# But the pod is still created

Phase 3: Enforce (Week 3+)

Switch to deny mode for each policy individually:

# Start with least disruptivespec:  enforcementAction: deny # Order of enforcement:# 1. block-mutable-tags (low disruption — teams should already be tagging)# 2. require-resource-definitions (medium — add defaults to existing manifests)# 3. enforce-cleanstart-pod-security (medium — may need securityContext additions)# 4. require-cleanstart-registry (high — blocks non-CleanStart images entirely)# 5. require-digest-in-production (high — requires workflow changes)

Gatekeeper vs Kyverno: Quick Comparison

Feature

OPA Gatekeeper

Kyverno

Policy language

Rego (custom)

YAML (Kubernetes-native)

Learning curve

Steep (Rego)

Low (YAML)

Image verification

Requires external webhook

Built-in Cosign support

Mutation policies

Limited (assign/modify)

Full strategic merge patch

Audit existing resources

Built-in

Built-in (policy reports)

Community policies

gatekeeper-library (100+)

kyverno-policies (200+)

Cross-stack reuse

Yes (Terraform, APIs, etc.)

Kubernetes only

Performance at scale

Excellent (compiled Rego)

Good

Policy testing

opa test CLI

kyverno test CLI

Use Kyverno if you're Kubernetes-only and want the fastest path to enforcement. Use Gatekeeper if you need Rego across multiple systems or have complex policy logic.

What to Read Next

Kyverno Policies — YAML-native alternative to Gatekeeper. Image Signing with Sigstore — How CleanStart signs images. CIS Hardening — CIS controls that these policies enforce. Network Policies — Network-level enforcement. Runtime Evidence: Falco — Runtime policy enforcement.