Runtime Zero
ESC
Browse by topic
Articles  /  Kubernetes

Securing Kubernetes with OPA Gatekeeper

OPA Gatekeeper enforces policy at admission time, rejecting non-compliant workloads before they ever reach your cluster. This guide covers ConstraintTemplates, audit mode, and the policies every production cluster needs.

ER

Kubernetes RBAC controls who can do what. OPA Gatekeeper controls what resources look like when they're created. The two are complementary, and production clusters need both. This guide focuses on Gatekeeper.

How Gatekeeper Works

Gatekeeper runs as a validating admission webhook. When you kubectl apply a resource, the API server sends a copy to Gatekeeper before writing it to etcd. Gatekeeper evaluates the resource against all applicable Constraint objects and rejects it if any policy fails.

The policy language is Rego (from OPA). You define a ConstraintTemplate (the Rego logic) and a Constraint (the parameters and scope):

ConstraintTemplate: "Containers must not run as root"
    ↓
Constraint: "Apply this rule to all pods in the production namespace"
    ↓
Admission request: rejected if runAsNonRoot is false

Essential Policies for Every Cluster

1. Require Non-Root Containers

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequirenonroot
spec:
  crd:
    spec:
      names:
        kind: K8sRequireNonRoot
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequirenonroot
        violation[{"msg": msg}] {
          c := input.review.object.spec.containers[_]
          not c.securityContext.runAsNonRoot
          msg := sprintf("Container %v must set runAsNonRoot: true", [c.name])
        }

2. Block Privileged Containers

# Rego snippet
violation[{"msg": msg}] {
  c := input.review.object.spec.containers[_]
  c.securityContext.privileged
  msg := sprintf("Container %v must not be privileged", [c.name])
}

3. Require Resource Limits

Pods without CPU/memory limits can consume unbounded resources and impact neighbors. Enforce limits at admission:

violation[{"msg": msg}] {
  c := input.review.object.spec.containers[_]
  not c.resources.limits.memory
  msg := sprintf("Container %v must set resources.limits.memory", [c.name])
}

4. Restrict Allowed Registries

Only permit images from your internal registry:

violation[{"msg": msg}] {
  c := input.review.object.spec.containers[_]
  not startswith(c.image, "registry.company.com/")
  msg := sprintf("Image %v must be from registry.company.com", [c.image])
}

Audit Mode: Discover Before Enforcing

Deploy policies in dryrun mode to discover violations without blocking anything:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
  name: require-non-root
spec:
  enforcementAction: dryrun   # Change to "deny" when ready
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["production"]

Query existing violations:

kubectl get k8srequirenonroot require-non-root -o jsonpath='{.status.violations}' | jq .

Run audit mode for at least one sprint before switching to deny. The violations output will surprise you — every cluster has old workloads that don't meet modern security standards.

Exemptions Without Compromising Security

Some system workloads (CoreDNS, CNI plugins) legitimately need elevated permissions. Exempt them by namespace rather than disabling policies:

spec:
  match:
    excludedNamespaces:
      - kube-system
      - kube-public
      - cert-manager

Define a policy for which namespaces can be excluded and enforce it — otherwise exemptions accumulate until the policy is meaningless.