Policy at the Front Door – Admission Governance (Gatekeeper)

Duration: ~25 minutes

Overview

Admission governance & policy layering. Enforce baseline safety gates before insecure workloads enter the cluster.

Use Gatekeeper admission policies to block unsafe cluster changes at the API and show clear allow versus deny outcomes.

Why it matters

Insecure specs enter the cluster only once—at admission. Gatekeeper (and similar) policies turn “we hope teams follow the guide” into denies at the API before a rootful or hostPath payload ever schedules.

What does it solve

  • Workloads that quietly revive root, privileged, or host mounts

  • Policy drift between wiki docs and what the API accepts

  • Late discovery of unsafe objects already running in production

Your Mission

Install admission gates that block attacker-shaped pod specs at the door and prove clear allow versus deny outcomes.

Prerequisites

  • Gatekeeper / OpenShift operator installed (CRDs: ConstraintTemplate, K8sAllowedRepos, etc.)

  • Cluster admin privileges

Click each step only if you need a hint.

Namespace Setup
oc new-project 201-11-a-govern
No Root UID Constraint

ConstraintTemplate:

oc apply -f - <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8snonrootuid
spec:
  crd:
    spec:
      names:
        kind: K8sNonRootUid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8snonrootuid
        violation[{"msg": msg}] {
          input.review.kind.kind == "Pod"
          c := input.review.object.spec.containers[_]
          not c.securityContext.runAsNonRoot
          msg := sprintf("Container %s must set runAsNonRoot=true", [c.name])
        }
EOF

Constraint:

oc apply -f - <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNonRootUid
metadata:
  name: nonroot-required
spec: {}
EOF

Test (expect denial):

oc apply -n 201-11-a-govern -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: root-pod
spec:
  containers:
  - name: app
    image: registry.access.redhat.com/ubi9/ubi
    securityContext:
      runAsNonRoot: false
    command: ["sleep","5"]
EOF
Mandatory NetworkPolicy Constraint

Template:

oc apply -f - <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8snetpolrequired
spec:
  crd:
    spec:
      names:
        kind: K8sNetPolRequired
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8snetpolrequired
        violation[{"msg": msg}] {
          input.review.kind.kind == "Pod"
          ns := input.review.object.metadata.namespace
          not has_network_policy[ns]
          msg := sprintf("Namespace %s lacks a NetworkPolicy", [ns])
        }
        has_network_policy[ns] {
          p := data.inventory.namespace[ns].networkpolicies[_]
        }
EOF

Constraint:

oc apply -f - <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNetPolRequired
metadata:
  name: netpol-required
spec: {}
EOF

Create a pod (should be denied). Then add a minimal NetworkPolicy and retry:

oc apply -n 201-11-a-govern -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-default
spec:
  podSelector: {}
  policyTypes: [Ingress]
EOF

Retry pod with non-root context:

oc apply -n 201-11-a-govern -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: ok-pod
spec:
  containers:
  - name: app
    image: registry.access.redhat.com/ubi9/ubi@sha256:123fake
    securityContext:
      runAsNonRoot: true
    command: ["sleep","1000"]
EOF
Digest Pinning & (Simulated) Signature Constraint

Simplified example focusing on forbidding mutable tags: Template:

oc apply -f - <<'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdigestonly
spec:
  crd:
    spec:
      names:
        kind: K8sDigestOnly
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdigestonly
        violation[{"msg": msg}] {
          input.review.kind.kind == "Pod"
          c := input.review.object.spec.containers[_]
          startswith(c.image, "registry.access.redhat.com/")
          not contains(c.image, "@sha256:")
          msg := sprintf("Image %s must use digest", [c.image])
        }
EOF

Constraint:

oc apply -f - <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDigestOnly
metadata:
  name: digest-only
spec: {}
EOF

Test denial with tag:

oc apply -n 201-11-a-govern -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: tag-pod
spec:
  containers:
  - name: app
    image: registry.access.redhat.com/ubi9/ubi:latest
    securityContext:
      runAsNonRoot: true
    command: ["sleep","1"]
EOF
Namespace Exemption
oc label namespace 201-11-a-govern policy-tier=dev
# Example: refine rego to exempt tier=dev; left as exercise

Debrief

Admission governance blocks drift before runtime: constraints for non-root, required NetworkPolicy, digest/signing—and exemptions only via explicit, auditable labels.

What breaks without this:

  • Policy-only-at-runtime → bad configs already scheduled

  • Silent exceptions → untracked risk across teams

Controls that matter: Gatekeeper/Kyverno (or equivalent) constraint sets, progressive enforce, and labeled exemptions with owners and expiry.

Quick facts: admission is the last gate before the API persists the object—pair it with CI scanning so developers fail earlier.

giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module.

cd ~/openshift-security-roadshow
bash setup/lab-cleanup.sh --module 201-11