BlogKubernetes

OPA Gatekeeper: writing constraint templates from scratch

Enforce org-wide Kubernetes policy with Rego — required labels, blocked images, and audit of existing violations.

Dec 3, 2025·4 min readAdvanced·By the SecOpsLog team · command-tested

Kubernetes will happily accept a Deployment with no owner label, an image from a registry nobody trusts, and a container running as root. OPA Gatekeeper is the admission controller that says no — but only after you translate your org rules into Rego and wire them through Gatekeeper's Kubernetes-native objects. You write a ConstraintTemplate once (the reusable logic), then stamp out Constraints (the parameters and scope). Together they reject bad objects at create time and audit the ones already running.

Gatekeeper sits on the same admission webhook socket as Kyverno and Pod Security. The difference is the policy language: Rego, the same engine behind OPA and Conftest. That means a rule you test against Terraform plans can enforce the same logic against live cluster objects. If Rego still feels foreign, the OPA & Rego track walks through the language before you touch Gatekeeper.

Admission with ConstraintTemplate + Constraint

Template is the Rego. Constraint is the scope. dryrun before deny.

kubectl / CI create / apply kube-apiserver validating webhook ✓ ALLOW ✕ DENY 1 ConstraintTemplate Rego + CRD schema defines the policy language reusable across clusters 2 Constraint parameters + match which namespaces / kinds enforcementAction 3 dryrun first enforcementAction: dryrun audit without blocking then flip to deny Template = logic. Constraint = where it applies. Always dryrun before deny. request → webhook → template+constraint → allow/deny
Template — RegoConstraint — scopedryrun — then deny
From Rego to enforced policy

Never flip straight to deny. Dryrun first, fix violators, then enforce — same discipline as Pod Security audit mode.

1ConstraintTemplateRego + CRD schema2Constraintparams + match scope3dryrunaudit existing objects4Fix violatorslabels, images, caps5denyenforcementAction flip6CI gateconftest on manifests7Review auditweekly violation report

Step 1: write the ConstraintTemplate

The template defines the CRD shape (what parameters a Constraint can pass) and the Rego that evaluates input.review.object. Start small — a required label is the hello-world of Gatekeeper. The violation set must produce a human-readable msg or operators will ignore rejections.

template-required-labels.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names: { kind: K8sRequiredLabels }
validation:
openAPIV3Schema:
properties:
labels:
type: array
items: { type: string }
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
required := input.parameters.labels[_]
not input.review.object.metadata.labels[required]
msg := sprintf("missing required label: %v", [required])
}

Step 2: apply a Constraint with scope

The Constraint binds the template to real clusters: which API groups/kinds it applies to, and the parameter values. Scope matters — a cluster-wide required-label rule on every Pod is different from one on Deployments in production namespaces only. Use match carefully so platform namespaces and system workloads are not caught by surprise.

constraint-owner-label.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: must-have-owner
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
namespaces: ["shop", "payments"]
parameters:
labels: ["owner", "cost-center"]

Step 3: prove it rejects and audits

With enforcementAction: dryrun, non-compliant creates still succeed but show up in Gatekeeper audit results. That is your migration window. Once violators are fixed, flip to deny and confirm the webhook blocks a bad manifest cold.

bash — reject on createlive
kubectl apply -f deploy-no-owner.yaml
admission webhook "validation.gatekeeper.sh" denied the request:
[must-have-owner] missing required label: owner
kubectl get constraint must-have-owner -o yaml | grep totalViolations
audit counts existing objects that would fail in deny mode

Beyond labels: images and capabilities

The same template/constraint split scales to blocked registries, required seccomp profiles, and forbidden capabilities. Gatekeeper ships built-in templates under library.gatekeeper.sh — read them before writing from scratch. For custom rules, test Rego locally with opa eval and sample manifests before you point it at production admission.

constraint-block-latest.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: only-trusted-registries
spec:
match:
kinds: [{ apiGroups: [""], kinds: ["Pod"] }]
parameters:
repos:
- "registry.acme.internal/"
- "ghcr.io/acme/"
Dryrun is not optional
Flipping a new Constraint straight to deny on a Friday afternoon breaks every legacy Deployment in the cluster. Run dryrun for at least one release cycle, export audit violations, give teams a deadline, then enforce. The audit webhook is how you find objects Gatekeeper never saw at admission time.

Where this goes next

Admission policy is one layer. Pair Gatekeeper with Pod Security Standards for baseline hardening, NetworkPolicy for lateral movement, and Conftest in CI so bad manifests never reach the API server. The OPA & Rego path covers writing policies, testing them, and running Gatekeeper at org scale.

Go deeper in a courseOPA & RegoPolicy-as-code with OPA — Rego, Conftest, and Gatekeeper enforcement.View course

Related posts