Policy enforcement in distributed systems used to mean hand-rolled middleware, feature flags, and tribal knowledge baked into deployment scripts. Open Policy Agent (OPA) changed that by introducing a general-purpose, declarative policy engine that separates what the policy says from how it’s enforced. Gatekeeper brings OPA into Kubernetes as a production-grade admission controller. This post covers everything from Rego fundamentals to running conftest in CI, and then walks out beyond Kubernetes into Envoy, Terraform, and microservice authorization.
1. OPA Fundamentals
What OPA Is
OPA (Open Policy Agent) is a CNCF-graduated general-purpose policy engine. It answers a single question in a general way: “Is this request allowed given these data and these rules?” OPA decouples policy decisions from the services that need to enforce them. Your application calls OPA, passes context as JSON, and gets back a decision — also JSON.
OPA is not Kubernetes-specific. It has been used to govern:
- Kubernetes admission control (via Gatekeeper)
- HTTP API authorization in microservices
- Terraform plan validation
- SSH and sudo access (via PAM)
- CI/CD pipeline gates
- Envoy external authorization
The Data Model
OPA evaluates policies against two document types:
input — the query document. Supplied by the caller at evaluation time. In Gatekeeper it is the admission review object. In an HTTP authorization scenario it might be {"user": "alice", "method": "GET", "path": "/orders/42"}.
data — the base document. A hierarchical JSON store loaded from bundles or pushed via the Data API. In Gatekeeper, the cluster’s current state (Namespaces, existing Deployments, etc.) lives here under data.inventory.
A policy query asks OPA to evaluate a specific rule path, e.g. data.kubernetes.admission.deny, and returns the bound value.
How Evaluation Works
OPA uses a logic-programming engine. Rules are written in Rego. When you query a rule, OPA evaluates every body clause of every rule with that name. If all clauses in a body unify successfully, the head is contributed to the result. Rules with the same name compose via logical OR — multiple bodies produce multiple contributions.
OPA is fully deterministic: given the same input and data, you always get the same result.
Deployment Patterns: Library vs Sidecar vs Standalone
Embedded library (Go only): Import OPA as a Go package. Lowest latency (in-process), no network calls. Suitable for Go services that need policy decisions in the hot path. Import path changed in v1.0: github.com/open-policy-agent/opa/v1/rego.
Sidecar: Run OPA as a container in the same pod as your application. Calls are localhost HTTP, so latency is sub-millisecond. Policy bundles are loaded from a remote bundle server (e.g., an S3 bucket or OCI registry) and updated in the background. The application pods scale with OPA.
Standalone service: Deploy OPA as a shared HTTP/gRPC service. Simpler to manage, but now OPA is a network dependency. Appropriate for services with modest policy call rates or when centralizing policy management outweighs the latency cost.
Gatekeeper: OPA running inside the cluster as an admission webhook controller. Gatekeeper wraps OPA with Kubernetes-native APIs (CRDs), so you express policies and their parameters as Kubernetes resources instead of raw JSON bundles.
OPA v1.0 and Rego v1 Syntax
OPA released v1.0 in early 2025. The big changes that affect existing code:
if and contains are now required keywords. p { true } must become p if { true }. p.a { true } must become p contains "a" if { true }.
in, every, if, and contains are part of the language by default. import future.keywords becomes a no-op and will be ignored.
- The recommended migration is to add
import rego.v1 to each module (available in OPA v0.59+). This makes OPA validate the file as v1-compatible without requiring the full upgrade yet.
- OPA’s server default changed to bind on
localhost instead of 0.0.0.0. Restore v0.x behavior with --addr 0.0.0.0:8181.
- Deprecated builtins removed:
any, all (the old set predicates), re_match, net.cidr_overlap, set_diff, cast_*.
Migration path:
1
2
3
|
opa check --v0-v1 ./policies/ # find syntax errors
opa check --v0-v1 --strict ./policies/ # find strict incompatibilities
opa fmt --write --v0-v1 ./policies/ # auto-reformat to v1 style
|
2. Rego Language Deep Dive
Every policy in OPA is written in Rego. This section covers the constructs you’ll encounter most often.
Complete vs Partial Rules
A complete rule defines a single scalar or composite value:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import rego.v1
# A constant
max_allowed_replicas := 10
# A conditional scalar — "default" provides a fallback when no body matches
default allow := false
allow if {
input.user == "admin"
}
allow if {
input.method == "GET"
"public" in input.resource.tags
}
|
A partial rule builds up a set or object incrementally. Every body that unifies contributes an element:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import rego.v1
# Partial set — every container that is missing resource limits
containers_missing_limits contains name if {
container := input.review.object.spec.containers[_]
name := container.name
not container.resources.limits.memory
}
containers_missing_limits contains name if {
container := input.review.object.spec.initContainers[_]
name := container.name
not container.resources.limits.memory
}
# Partial object — map container name -> image
container_images[name] := image if {
container := input.review.object.spec.containers[_]
name := container.name
image := container.image
}
|
Functions
Functions take arguments and return a single value. They are defined with := in the head and if in the body:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import rego.v1
has_tag(image) if {
contains(image, ":")
not endswith(image, ":latest")
}
# Multi-clause functions allow conditional dispatch
image_registry(image) := registry if {
parts := split(image, "/")
count(parts) >= 2
registry := parts[0]
}
image_registry(image) := "docker.io" if {
not contains(image, "/")
}
|
Comprehensions
Comprehensions are inline expressions that build collections without requiring a helper rule.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import rego.v1
# Array comprehension — collect all image names
all_images := [img |
container := input.review.object.spec.containers[_]
img := container.image
]
# Set comprehension — unique namespaces in use
used_namespaces := {ns |
pod := data.inventory.namespace[ns]["v1"]["Pod"][_]
ns != "kube-system"
}
# Object comprehension — map label key to value
label_map := {k: v |
some k, v
input.review.object.metadata.labels[k] = v
}
|
Negation with not
not checks that a rule or expression is undefined or false. It implements “negation as failure.”
1
2
3
4
5
6
7
8
9
10
11
12
|
import rego.v1
# "not has_security_context" is true when has_security_context is undefined/false
violation contains msg if {
container := input.review.object.spec.containers[_]
not has_security_context(container)
msg := sprintf("Container %v is missing a securityContext", [container.name])
}
has_security_context(container) if {
container.securityContext.runAsNonRoot == true
}
|
Safety rule: a variable inside a not expression must also appear in a non-negated expression within the same rule body. OPA enforces this to prevent unsafe, unbounded negation.
The with Keyword for Testing
with replaces documents (or even built-in functions) during evaluation. It is the primary tool for unit-testing Rego policies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
import rego.v1
test_deny_privileged_container if {
count(violation) > 0 with input as {
"review": {
"object": {
"spec": {
"containers": [{
"name": "shell",
"image": "ubuntu:22.04",
"securityContext": {"privileged": true}
}]
}
}
}
}
}
test_allow_normal_container if {
count(violation) == 0 with input as {
"review": {
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "nginx:1.27.0",
"securityContext": {"privileged": false}
}]
}
}
}
}
}
|
You can also mock built-in functions:
1
2
3
4
5
|
import rego.v1
test_time_based_policy if {
allow with time.now_ns as (1748000000 * 1000000000)
}
|
every for Universal Quantification
Without every you’d need a double-negation pattern to assert that all items satisfy a condition. With it:
1
2
3
4
5
6
7
8
9
10
|
import rego.v1
violation contains msg if {
containers := input.review.object.spec.containers
not every container in containers {
container.resources.limits.cpu != null
container.resources.limits.memory != null
}
msg := "Not all containers have resource limits"
}
|
Iteration and Aggregation Patterns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import rego.v1
# Iteration using _ (wildcard) — classic pattern
violation contains msg if {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Privileged container not allowed: %v", [container.name])
}
# Iteration using some — explicit binding for multiple indices
violation contains msg if {
some i, j
init := input.review.object.spec.initContainers[i]
vol := input.review.object.spec.volumes[j]
init.volumeMounts[_].name == vol.name
vol.hostPath.path == "/"
msg := sprintf("initContainer %v mounts host root via volume %v", [init.name, vol.name])
}
# Aggregation — count missing labels
missing_labels := {label |
label := input.parameters.required[_]
not input.review.object.metadata.labels[label]
}
violation contains msg if {
m := missing_labels
count(m) > 0
msg := sprintf("Missing required labels: %v", [m])
}
|
Helper Rules (Libraries)
Gatekeeper ConstraintTemplate supports a libs field for shared Rego packages. This lets multiple templates share common logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# In libs[0]:
package lib.images
import rego.v1
is_latest(image) if { endswith(image, ":latest") }
is_latest(image) if { not contains(image, ":") }
registry(image) := r if {
parts := split(image, "/")
count(parts) >= 3 # host/org/name
r := parts[0]
}
registry(image) := "docker.io" if {
parts := split(image, "/")
count(parts) < 3
}
|
3. Gatekeeper: Architecture and Setup
What Gatekeeper Adds
Gatekeeper (current stable: v3.22.2, April 2025) wraps OPA with:
- CRD-based policies — ConstraintTemplate and Constraint resources replace raw JSON bundles.
- Parameterized templates — The same template can be instantiated with different parameters for different namespaces or teams.
- Audit controller — Continuously scans the cluster and reports violations in Constraint
.status.violations.
- Mutation support — Assign, AssignMetadata, ModifySet, and AssignImage CRDs.
- Namespace exemptions — The
gatekeeper-system namespace is exempt by default.
- External data providers — Query external services from inside Rego at admission time.
Admission Webhook Architecture
When a resource is created or updated, the Kubernetes API server sends an AdmissionReview object to Gatekeeper’s validating webhook endpoint. Gatekeeper evaluates the request against every applicable Constraint. If any Constraint’s Rego emits a non-empty violation set (and the constraint’s enforcementAction is deny), Gatekeeper returns a rejection. The webhook runs as a ValidatingWebhookConfiguration and — critically — has a timeout (default 3 seconds). Requests that time out are handled per failurePolicy: Gatekeeper defaults to Ignore (fail-open).
Installation
1
2
3
4
5
6
7
8
9
10
11
12
|
# Option 1: direct manifest (pinned version)
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.22.2/deploy/gatekeeper.yaml
# Option 2: Helm (recommended for production)
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system \
--create-namespace \
--set replicas=3 \
--set auditInterval=60 \
--set constraintViolationsLimit=50
|
The deployment creates:
gatekeeper-controller-manager — handles admission webhook requests
gatekeeper-audit — background controller that scans existing resources
- Several CRDs:
ConstraintTemplate, Config, SyncSet, Provider, plus mutation CRDs
ConstraintTemplate: Structure
A ConstraintTemplate has two main sections:
spec.crd — defines the new CRD kind and its parameter schema (OpenAPI v3)
spec.targets — the Rego implementation targeting admission.k8s.gatekeeper.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels # new CRD kind
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
description: "Required label keys"
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
import rego.v1
violation contains {"msg": msg} if {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
|
Constraint: Instantiating a Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: all-namespaces-must-have-team-label
spec:
enforcementAction: deny # deny | dryrun | warn
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
excludedNamespaces:
- kube-system
- gatekeeper-system
- cert-manager
parameters:
labels: ["team", "env"]
|
After a violation is detected by the audit controller, the Constraint’s status section shows it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
status:
auditTimestamp: "2026-05-20T14:30:00Z"
enforced: true
totalViolations: 3
violations:
- enforcementAction: deny
kind: Namespace
name: staging
namespace: ""
message: "Missing required labels: {\"team\"}"
- enforcementAction: deny
kind: Namespace
name: legacy
namespace: ""
message: "Missing required labels: {\"team\", \"env\"}"
|
Violations are capped at --constraint-violations-limit (default 20) per Constraint to keep the object small, but totalViolations reflects the full count.
Enforcement Actions
deny — reject the request at admission time
dryrun — allow but record the violation in audit status (useful for rollout)
warn — allow but return a warning to the client (kubectl displays it)
The standard rollout pattern is: create template and constraint with dryrun, let the audit controller populate violations, fix the violations, then flip to deny.
4. Practical ConstraintTemplate Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblocklatesttag
annotations:
metadata.gatekeeper.sh/title: "Block Latest Image Tag"
metadata.gatekeeper.sh/version: 1.0.0
description: "Requires containers to use explicit, versioned image tags."
spec:
crd:
spec:
names:
kind: K8sBlockLatestTag
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblocklatesttag
import rego.v1
violation contains {"msg": msg} if {
container := input_containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%v' uses ':latest' tag — pin to a specific version.", [container.name])
}
violation contains {"msg": msg} if {
container := input_containers[_]
not contains(container.image, ":")
msg := sprintf("Container '%v' has no image tag (implicitly :latest).", [container.name])
}
input_containers contains c if { c := input.review.object.spec.containers[_] }
input_containers contains c if { c := input.review.object.spec.initContainers[_] }
input_containers contains c if { c := input.review.object.spec.ephemeralContainers[_] }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockLatestTag
metadata:
name: block-latest-tag
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"]
- apiGroups: ["batch"]
kinds: ["Job", "CronJob"]
|
4.2 Require Resource Limits and Requests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8scontainerresources
spec:
crd:
spec:
names:
kind: K8sContainerResources
validation:
openAPIV3Schema:
type: object
properties:
exemptContainers:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8scontainerresources
import rego.v1
violation contains {"msg": msg} if {
container := input_containers[_]
not is_exempt(container.name)
missing := missing_fields(container)
count(missing) > 0
msg := sprintf("Container '%v' is missing resource fields: %v", [container.name, missing])
}
missing_fields(container) := m if {
m := {f |
f := required_fields[_]
not _has_field(container, f)
}
}
required_fields := [
"resources.limits.cpu",
"resources.limits.memory",
"resources.requests.cpu",
"resources.requests.memory",
]
_has_field(container, "resources.limits.cpu") if { container.resources.limits.cpu }
_has_field(container, "resources.limits.memory") if { container.resources.limits.memory }
_has_field(container, "resources.requests.cpu") if { container.resources.requests.cpu }
_has_field(container, "resources.requests.memory") if { container.resources.requests.memory }
is_exempt(name) if {
exempt := input.parameters.exemptContainers[_]
name == exempt
}
input_containers contains c if { c := input.review.object.spec.containers[_] }
input_containers contains c if { c := input.review.object.spec.initContainers[_] }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sContainerResources
metadata:
name: require-resource-limits
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
parameters:
exemptContainers: []
|
4.3 Restrict Privileged Containers
From the official gatekeeper-library, with Rego engine:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spspprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sPSPPrivilegedContainer
validation:
openAPIV3Schema:
type: object
properties:
exemptImages:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spspprivileged
import rego.v1
import data.lib.exclude_update.is_update
import data.lib.exempt_container.is_exempt
violation contains {"msg": msg} if {
not is_update(input.review)
c := input_containers[_]
not is_exempt(c)
c.securityContext.privileged == true
msg := sprintf("Privileged container not allowed: %v", [c.name])
}
input_containers contains c if { c := input.review.object.spec.containers[_] }
input_containers contains c if { c := input.review.object.spec.initContainers[_] }
input_containers contains c if { c := input.review.object.spec.ephemeralContainers[_] }
libs:
- |
package lib.exclude_update
import rego.v1
is_update(review) if { review.operation == "UPDATE" }
- |
package lib.exempt_container
import rego.v1
is_exempt(container) if {
exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
img := container.image
exemption := exempt_images[_]
_matches_exemption(img, exemption)
}
_matches_exemption(img, exemption) if {
not endswith(exemption, "*")
exemption == img
}
_matches_exemption(img, exemption) if {
endswith(exemption, "*")
prefix := trim_suffix(exemption, "*")
startswith(img, prefix)
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
name: psp-privileged-container
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system"]
parameters:
exemptImages: []
|
4.4 Enforce Image Registry Allowlist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
metadata.gatekeeper.sh/title: "Allowed Repositories"
metadata.gatekeeper.sh/version: 1.0.0
description: "Requires container images to come from approved registries."
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
required: [repos]
properties:
repos:
type: array
description: "Image name prefixes that are allowed."
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
import rego.v1
violation contains {"msg": msg} if {
container := input_containers[_]
not image_allowed(container.image)
msg := sprintf(
"Container '%v' uses image '%v' from a disallowed registry. Allowed prefixes: %v",
[container.name, container.image, input.parameters.repos]
)
}
image_allowed(image) if {
repo := input.parameters.repos[_]
startswith(image, repo)
}
input_containers contains c if { c := input.review.object.spec.containers[_] }
input_containers contains c if { c := input.review.object.spec.initContainers[_] }
input_containers contains c if { c := input.review.object.spec.ephemeralContainers[_] }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allow-internal-registry-only
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system", "gatekeeper-system"]
parameters:
repos:
- "registry.internal.corp.com/"
- "gcr.io/your-project/"
|
4.5 Require a PodDisruptionBudget for Deployments
This template accesses data.inventory — you must sync PodDisruptionBudget resources via the Config or SyncSet (see Section 6).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequirepdb
annotations:
description: "Deployments with >= minReplicas must have a matching PodDisruptionBudget."
spec:
crd:
spec:
names:
kind: K8sRequirePDB
validation:
openAPIV3Schema:
type: object
properties:
minReplicas:
type: integer
default: 2
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirepdb
import rego.v1
violation contains {"msg": msg} if {
input.review.object.kind == "Deployment"
replicas := object.get(input.review.object.spec, "replicas", 1)
min_replicas := object.get(input.parameters, "minReplicas", 2)
replicas >= min_replicas
namespace := input.review.object.metadata.namespace
selector := input.review.object.spec.selector.matchLabels
pdbs := matching_pdbs(namespace, selector)
count(pdbs) == 0
msg := sprintf(
"Deployment '%v' has %v replicas but no matching PodDisruptionBudget",
[input.review.object.metadata.name, replicas]
)
}
matching_pdbs(namespace, selector) := pdbs if {
pdbs := [pdb |
pdb := data.inventory.namespace[namespace]["policy/v1"]["PodDisruptionBudget"][_]
selector_matches(pdb.spec.selector.matchLabels, selector)
]
}
selector_matches(pdb_sel, deploy_sel) if {
every k, v in pdb_sel {
deploy_sel[k] == v
}
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequirePDB
metadata:
name: require-pdb-for-deployments
spec:
enforcementAction: warn
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
excludedNamespaces: ["kube-system"]
parameters:
minReplicas: 2
|
5. Data Replication: Config and SyncSet
Policies that need to know about other cluster resources (e.g., existing PDBs, Namespaces) require those resources to be synced into OPA’s data store.
Config CRD
The Config resource must be named config in the gatekeeper-system namespace:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- group: ""
version: "v1"
kind: "Namespace"
- group: ""
version: "v1"
kind: "Pod"
- group: "policy"
version: "v1"
kind: "PodDisruptionBudget"
- group: "apps"
version: "v1"
kind: "Deployment"
|
SyncSet (v3.15+, Preferred)
Multiple SyncSets can coexist; Gatekeeper unions them all:
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: syncset.gatekeeper.sh/v1alpha1
kind: SyncSet
metadata:
name: core-resources
spec:
gvks:
- group: ""
version: "v1"
kind: "Namespace"
- group: "policy"
version: "v1"
kind: "PodDisruptionBudget"
|
Accessing Synced Data in Rego
1
2
3
4
5
6
7
8
|
# Cluster-scoped resources:
ns := data.inventory.cluster["v1"]["Namespace"]["production"]
# Namespace-scoped resources:
pdb := data.inventory.namespace["prod"]["policy/v1"]["PodDisruptionBudget"]["my-pdb"]
# Iterate all pods in a namespace:
pod := data.inventory.namespace["staging"]["v1"]["Pod"][_]
|
6. Mutations
Gatekeeper v3.10+ supports mutating admission webhooks. Mutations are expressed as CRDs rather than raw MutatingWebhookConfiguration patches. Mutations run before validating webhooks, so you can mutate a resource into compliance before validating it.
AssignMetadata can only add labels or annotations (it cannot modify existing values):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata:
name: add-managed-by-label
spec:
match:
scope: Namespaced
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet"]
location: "metadata.labels.managed-by"
parameters:
assign:
value: "gatekeeper"
|
Inject the resource’s own namespace into an annotation (useful for cross-namespace policy auditing):
1
2
3
4
5
6
7
8
9
10
11
12
|
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignMetadata
metadata:
name: annotate-namespace
spec:
match:
scope: Namespaced
location: "metadata.annotations.original-namespace"
parameters:
assign:
fromMetadata:
field: namespace
|
Assign — Mutate Arbitrary Fields
Assign can set any field outside metadata. Use pathTests to apply mutations only when a condition holds (e.g., “set a default only if the field isn’t already set”):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: default-security-context
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
excludedNamespaces: ["kube-system"]
location: "spec.securityContext.runAsNonRoot"
parameters:
assign:
value: true
pathTests:
- subPath: "spec.securityContext"
condition: MustExist
- subPath: "spec.securityContext.runAsNonRoot"
condition: MustNotExist # only set if not already defined
|
ModifySet — Inject or Remove Container Args
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
apiVersion: mutations.gatekeeper.sh/v1
kind: ModifySet
metadata:
name: inject-log-level
spec:
applyTo:
- groups: ["apps"]
kinds: ["Deployment"]
versions: ["v1"]
match:
scope: Namespaced
namespaces: ["production"]
location: "spec.template.spec.containers[name: app].args"
parameters:
operation: merge # merge = insert if missing; prune = remove
values:
fromList:
- "--log-level=info"
|
AssignImage — Pin Image Components
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
apiVersion: mutations.gatekeeper.sh/v1
kind: AssignImage
metadata:
name: pin-image-registry
spec:
applyTo:
- groups: ["apps"]
kinds: ["Deployment"]
versions: ["v1"]
match:
scope: Namespaced
location: "spec.template.spec.containers[name:*].image"
parameters:
assignDomain: "registry.internal.corp.com"
|
7. External Data Providers
External data providers let Gatekeeper call an external HTTP service from inside Rego at admission time. This is how you can validate image signatures, check a CVE database, or query an asset inventory.
Current status: externaldata.gatekeeper.sh/v1beta1 — still beta as of v3.22.x.
Provider CRD
1
2
3
4
5
6
7
8
9
|
apiVersion: externaldata.gatekeeper.sh/v1beta1
kind: Provider
metadata:
name: image-verifier
spec:
url: https://image-verifier.gatekeeper-system.svc:8443/validate
timeout: 2 # seconds; effective timeout is min(this, remaining webhook deadline)
caBundle: |
<base64-encoded-CA-cert>
|
TLS 1.3+ is mandatory. The provider must be an in-cluster service.
Request/Response Contract
The provider receives a POST with:
1
2
3
4
5
6
7
|
{
"apiVersion": "externaldata.gatekeeper.sh/v1beta1",
"kind": "ProviderRequest",
"request": {
"keys": ["registry.internal/app:sha256-abc123", "nginx:1.27.0"]
}
}
|
It must respond with:
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"apiVersion": "externaldata.gatekeeper.sh/v1beta1",
"kind": "ProviderResponse",
"response": {
"idempotent": true,
"items": [
{"key": "registry.internal/app:sha256-abc123", "value": "verified"},
{"key": "nginx:1.27.0", "value": null, "error": "signature not found"}
],
"systemError": ""
}
}
|
Set idempotent: true when using external data with mutations. Cache TTL defaults to 3 minutes (configurable via --external-data-provider-response-cache-ttl).
Rego Integration
Use the external_data built-in in your ConstraintTemplate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package k8simagesigned
import rego.v1
violation contains {"msg": msg} if {
# Batch all image keys into a single provider call
images := [img |
c := input.review.object.spec.containers[_]
img := c.image
]
response := external_data({"provider": "image-verifier", "keys": images})
# response is a list of [key, value, error] triples
result := response[_]
result[2] != "" # non-empty error string means the image failed verification
msg := sprintf("Image '%v' failed signature verification: %v", [result[0], result[2]])
}
|
Key operational note: batch keys into a single external_data call rather than calling per-container to avoid multiplying provider calls per admission request.
Common Provider Patterns
- cosign-gatekeeper-provider — validates Sigstore/cosign image signatures
- ratify — CNCF project for artifact verification (signatures, SBOMs, scan results)
- GitHub artifact attestations OPA provider — verifies GitHub Actions attestations
- JFrog Xray provider — queries JFrog Xray for CVE scores
8. Testing with conftest and opa test
opa test
The opa test command is the primary unit-testing tool for raw Rego policies.
opa test ./policies/ -v # run all tests, verbose
opa test ./policies/ --coverage # with coverage
opa test ./policies/ -r "test_privileged" # filter by regex
Test rule naming convention: prefix with test_. Tests that should be skipped: prefix with todo_.
A complete test file for the allowed-repos policy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
package k8sallowedrepos_test
import rego.v1
import data.k8sallowedrepos
test_allowed_registry_passes if {
count(k8sallowedrepos.violation) == 0 with input as {
"review": {
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "registry.internal.corp.com/team/app:1.2.3"
}],
"initContainers": []
}
}
},
"parameters": {
"repos": ["registry.internal.corp.com/"]
}
}
}
test_disallowed_registry_denied if {
violations := k8sallowedrepos.violation with input as {
"review": {
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "docker.io/nginx:1.27.0"
}],
"initContainers": []
}
}
},
"parameters": {
"repos": ["registry.internal.corp.com/"]
}
}
count(violations) == 1
violations[_].msg == "Container 'app' uses image 'docker.io/nginx:1.27.0' from a disallowed registry. Allowed prefixes: [\"registry.internal.corp.com/\"]"
}
test_init_container_also_checked if {
count(k8sallowedrepos.violation) > 0 with input as {
"review": {
"object": {
"spec": {
"containers": [{
"name": "app",
"image": "registry.internal.corp.com/app:1.0.0"
}],
"initContainers": [{
"name": "init-pull",
"image": "docker.io/busybox:latest"
}]
}
}
},
"parameters": {
"repos": ["registry.internal.corp.com/"]
}
}
}
|
conftest
conftest is a CLI tool that runs OPA policies against structured config files (YAML, JSON, HCL, Dockerfile, etc.). It’s the primary tool for CI gates on Kubernetes manifests, Dockerfiles, and Terraform plans.
Policy rules use deny, warn, or violation (with optional suffixes: deny_no_latest_tag). The default namespace is main.
Policy file (policy/kubernetes.rego):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import rego.v1
deny contains msg if {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%v' uses ':latest' tag", [container.name])
}
deny contains msg if {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Deployment does not set runAsNonRoot: true"
}
warn contains msg if {
input.kind == "Deployment"
not input.metadata.labels["app.kubernetes.io/version"]
msg := "Deployment is missing the 'app.kubernetes.io/version' label"
}
|
Test file (policy/kubernetes_test.rego):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package main
import rego.v1
test_secure_deployment_passes if {
count(deny) == 0 with input as {
"kind": "Deployment",
"metadata": {
"name": "app",
"labels": {"app.kubernetes.io/version": "1.2.3"}
},
"spec": {
"template": {
"spec": {
"securityContext": {"runAsNonRoot": true},
"containers": [{
"name": "app",
"image": "registry.internal.corp.com/app:1.2.3",
"resources": {
"limits": {"cpu": "200m", "memory": "256Mi"},
"requests": {"cpu": "100m", "memory": "128Mi"}
}
}]
}
}
}
}
}
test_latest_tag_denied if {
count(deny) > 0 with input as {
"kind": "Deployment",
"metadata": {"name": "bad-app"},
"spec": {
"template": {
"spec": {
"containers": [{"name": "app", "image": "nginx:latest"}]
}
}
}
}
}
|
Run policy tests:
1
2
3
|
conftest verify --policy ./policy/ # unit tests (test_ prefix rules)
conftest test manifests/*.yaml # apply policy to files
conftest test --update oci://ghcr.io/org/policies:latest ./manifests/ # pull policy from OCI
|
CI/CD Integration
GitHub Actions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
name: Policy Gates
on:
pull_request:
paths: ["kubernetes/**", "terraform/**", "policy/**"]
jobs:
conftest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install conftest
run: |
VERSION=$(curl -s https://api.github.com/repos/open-policy-agent/conftest/releases/latest \
| grep tag_name | cut -d '"' -f4 | sed 's/v//')
wget -qO- "https://github.com/open-policy-agent/conftest/releases/download/v${VERSION}/conftest_${VERSION}_Linux_x86_64.tar.gz" \
| tar xz && sudo mv conftest /usr/local/bin/
- name: Verify policy unit tests
run: conftest verify --policy policy/
- name: Test Kubernetes manifests
run: conftest test kubernetes/**/*.yaml --output github --fail-on-warn
- name: Test Terraform plan
working-directory: terraform/
run: |
terraform init -backend=false
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy ../policy/terraform/
|
GitLab CI:
1
2
3
4
5
6
7
8
9
|
conftest:
image: openpolicyagent/conftest:latest
stage: test
script:
- conftest verify --policy policy/
- conftest test kubernetes/**/*.yaml --output junit > conftest-results.xml
artifacts:
reports:
junit: conftest-results.xml
|
9. OPA Beyond Kubernetes
Envoy External Authorization
OPA integrates with Envoy via the opa-envoy-plugin, which implements the Envoy ext_authz gRPC API. OPA runs as a sidecar alongside Envoy in each pod. When Envoy receives a request, it consults OPA at localhost:9191 before proxying it.
Envoy filter configuration (in Istio’s EnvoyFilter resource):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: opa-authz
namespace: my-app
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: outbound|9191||opa.my-app.svc.cluster.local
failure_mode_allow: false
|
OPA-Envoy sidecar config (opa-config.yaml):
1
2
3
4
5
|
plugins:
envoy_ext_authz_grpc:
addr: :9191
path: envoy/authz/allow
dry-run: false
|
Rego policy for HTTP authorization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package envoy.authz
import rego.v1
default allow := false
allow if {
is_authenticated
is_authorized
}
is_authenticated if {
token := bearer_token
[_, payload, _] := io.jwt.decode(token)
payload.exp > time.now_ns() / 1e9
}
is_authorized if {
input.parsed_path[0] == "public"
}
is_authorized if {
token := bearer_token
[_, payload, _] := io.jwt.decode(token)
required_role := path_roles[concat("/", input.parsed_path)]
required_role in payload.roles
}
bearer_token := t if {
v := input.attributes.request.http.headers.authorization
startswith(v, "Bearer ")
t := substring(v, 7, -1)
}
path_roles := {
"api/orders": "orders-reader",
"api/admin": "admin",
}
|
The allow decision at envoy/authz/allow is either a boolean or a structured response object with {"allowed": true/false, "headers": {...}, "http_status": 403}.
1
2
3
4
5
6
7
8
9
|
# 1. Generate the plan and convert to JSON
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
# 2. Evaluate with OPA directly
opa exec --decision terraform/analysis/authz --bundle policy/ tfplan.json
# 3. Or with conftest
conftest test tfplan.json --policy policy/terraform/
|
The JSON plan’s relevant structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"resource_changes": [
{
"type": "aws_s3_bucket",
"name": "data",
"change": {
"actions": ["create"],
"after": {
"bucket": "my-data-bucket",
"acl": "public-read"
}
}
}
]
}
|
Example policy gate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package terraform.aws.security
import rego.v1
# Deny public S3 buckets
deny contains msg if {
change := input.resource_changes[_]
change.type == "aws_s3_bucket"
"create" in change.change.actions
change.change.after.acl in {"public-read", "public-read-write"}
msg := sprintf("S3 bucket '%v' has public ACL '%v'", [change.name, change.change.after.acl])
}
# Deny unencrypted EBS volumes
deny contains msg if {
change := input.resource_changes[_]
change.type == "aws_ebs_volume"
"create" in change.change.actions
not change.change.after.encrypted
msg := sprintf("EBS volume '%v' is not encrypted", [change.name])
}
# Warn on large instance types
warn contains msg if {
change := input.resource_changes[_]
change.type == "aws_instance"
"create" in change.change.actions
large_instance_types := {"p3.8xlarge", "p3.16xlarge", "x1e.xlarge"}
change.change.after.instance_type in large_instance_types
msg := sprintf("Instance '%v' uses expensive type '%v' — ensure this is intentional",
[change.name, change.change.after.instance_type])
}
|
OPA as a Microservice Authorization Layer
Any service can query OPA over HTTP:
1
2
3
4
5
6
7
8
9
10
|
# Start OPA server
opa run --server --addr :8181 ./policies/ ./data/
# Query a policy decision
curl -X POST http://localhost:8181/v1/data/authz/allow \
-H "Content-Type: application/json" \
-d '{"input": {"user": "alice", "action": "read", "resource": "orders/42"}}'
# Response:
# {"result": true}
|
For high-throughput scenarios, run OPA as a sidecar loading policy bundles from an OCI registry or S3 bucket via the bundle plugin. The bundle is fetched in the background; the sidecar always serves decisions from the in-memory loaded bundle.
10. Operational Considerations
Audit Controller
The audit controller re-evaluates all cached resources against all active Constraints on a configurable interval. Key configuration:
--audit-interval=60 # seconds between audit runs (0 = disabled)
--constraint-violations-limit=50 # max violations stored per constraint (default: 20)
--audit-chunk-size=400 # resources per API server list call (default: 500)
--audit-from-cache=false # true = use OPA cache; false = query API server live
The audit controller populates .status.violations on each Constraint resource. Use Prometheus metrics (gatekeeper_violations) for aggregate dashboards — the per-constraint status cap is purely a Kubernetes object size concern.
Webhook Latency and Timeouts
The admission webhook has a timeoutSeconds (default: 3 seconds). Admission calls that exceed this timeout are handled by failurePolicy:
Ignore (default) — allow the request if Gatekeeper is unreachable or times out
Fail — reject the request if Gatekeeper is unreachable or times out
For production, run at least 3 replicas of gatekeeper-controller-manager spread across failure domains. The audit controller and the webhook controller are separate pods, so audit load does not affect webhook latency.
External data providers have their own timeout configured in the Provider spec. The effective timeout is min(provider.timeout, remaining_webhook_deadline). Keep provider response times under 1 second to leave headroom.
Namespace Exemptions
Gatekeeper exempts gatekeeper-system and kube-system by default. You can add additional exempt namespaces at install time or via the --exempt-namespace flag. At runtime, per-constraint exemptions are configured in spec.match.excludedNamespaces.
Exempt a namespace from the admission webhook entirely using the namespace label:
1
|
kubectl label namespace ci-ephemeral admission.gatekeeper.sh/ignore=no-policy
|
Break-Glass Emergency
If Gatekeeper is blocking legitimate cluster operations:
1
2
3
4
5
6
7
8
|
# Immediately disable ALL Gatekeeper admission checks
kubectl delete validatingwebhookconfigurations gatekeeper-validating-webhook-configuration
# Also disable mutation if needed
kubectl delete mutatingwebhookconfigurations gatekeeper-mutating-webhook-configuration
# Restore after incident — Gatekeeper reconciler will re-create the webhook configurations
kubectl rollout restart deployment/gatekeeper-controller-manager -n gatekeeper-system
|
Because Kubernetes does not subject webhook configuration changes to admission webhooks, deleting the webhook configuration is always possible — you cannot lock yourself out.
Gatekeeper and Kubernetes Validating Admission Policy (VAP)
From Gatekeeper v3.18, Kubernetes native CEL-based ValidatingAdmissionPolicy is supported within ConstraintTemplates via the K8sNativeValidation engine. This runs CEL expressions directly in the API server without a webhook round-trip. The --sync-vap-enforcement-scope flag defaults to true in v3.22, aligning VAP scope with Gatekeeper’s own enforcement boundaries. The Rego engine and CEL engine can coexist in the same template (spec.targets[].code accepts both).
11. Migration from Pod Security Policies
PSP was removed in Kubernetes v1.25. The gatekeeper-library provides drop-in replacements for every PSP field.
PSP-to-Gatekeeper Mapping
| PSP Field |
Gatekeeper Library Template |
privileged |
k8spspprivilegedcontainer |
allowPrivilegeEscalation |
k8spspallowprivilegeescalationcontainer |
hostPID, hostIPC, hostNetwork |
k8spsphostnamespace |
hostPorts |
k8spsphostnetworkingports |
allowedHostPaths |
k8spsphostfilesystem |
allowedCapabilities, requiredDropCapabilities |
k8spspcapabilities |
seLinux |
k8spspselinuxv2 |
runAsUser, runAsGroup, supplementalGroups |
k8spspallowedusers |
fsGroup |
k8spspfsgroup |
readOnlyRootFilesystem |
k8spspreadonlyrootfilesystem |
volumes |
k8spspvolumetypes |
allowedFlexVolumes |
k8spspflexvolumes |
forbiddenSysctls |
k8spspseccomp |
seccomp |
k8spspseccompv2 |
Install the full baseline or restricted profile from the gatekeeper-library:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Clone the library
git clone https://github.com/open-policy-agent/gatekeeper-library
cd gatekeeper-library
# Apply baseline profile templates
kubectl apply -f library/pod-security-policy/privileged-containers/template.yaml
kubectl apply -f library/pod-security-policy/host-namespaces/template.yaml
kubectl apply -f library/pod-security-policy/host-network-ports/template.yaml
kubectl apply -f library/pod-security-policy/capabilities/template.yaml
kubectl apply -f library/pod-security-policy/selinux/template.yaml
# Apply corresponding constraints
kubectl apply -f library/pod-security-policy/privileged-containers/samples/
|
The migration strategy:
- Install gatekeeper-library templates
- Create all constraints with
enforcementAction: dryrun
- Let the audit controller run for at least one full interval
- Inspect violations with
kubectl get <constraintkind> -o yaml | grep -A30 violations
- Fix violations or add exemptions as needed
- Flip to
enforcementAction: deny
- Remove PSP-related RBAC bindings once stable
opa CLI
1
2
3
4
5
6
7
|
opa run --server ./policies/ ./data/ # start OPA server
opa eval -d ./policies/ 'data.mypkg.allow' -i input.json # one-shot eval
opa test ./policies/ -v --coverage # run tests with coverage
opa fmt --write ./policies/ # auto-format
opa check ./policies/ # parse + type check
opa check --v0-v1 ./policies/ # check v1 compatibility
opa build -b ./policies/ -o bundle.tar.gz # compile to bundle
|
conftest CLI
1
2
3
4
5
6
|
conftest test deployment.yaml # test a single file
conftest test manifests/ --all-namespaces # test a directory
conftest verify --policy ./policy/ # run unit tests
conftest pull oci://ghcr.io/org/policy:v1 # pull policy from OCI
conftest push oci://ghcr.io/org/policy:v1 # push policy to OCI
conftest test plan.json --input terraform # explicit parser hint
|
Gatekeeper kubectl operations
1
2
3
4
5
6
7
8
9
10
11
|
# Check constraint violations
kubectl get k8srequiredlabels -o yaml
# List all constraint kinds
kubectl get crds | grep constraints.gatekeeper.sh
# Check template status (validation errors show here)
kubectl get constrainttemplates -o yaml
# Watch audit logs
kubectl logs -n gatekeeper-system -l control-plane=audit-controller -f
|
Known Gotchas
ConstraintTemplate kind must match metadata.name. The spec.crd.spec.names.kind (e.g., K8sRequiredLabels) must be the CamelCase version, and metadata.name must be its lowercase equivalent (k8srequiredlabels). A mismatch causes template creation to fail silently or produce a non-functional CRD.
data.inventory is empty until sync is configured. Templates that call data.inventory without a Config or SyncSet that syncs the relevant resource type will always return empty results — you won’t get an error, just a policy that never matches existing resources.
Gatekeeper does not gate itself during admission deadlock. If all gatekeeper-controller pods are down and failurePolicy: Fail is set, the cluster will not be able to admit any new pods (including the ones needed to restart Gatekeeper). Default failurePolicy: Ignore avoids this but means the cluster is unprotected during an outage.
UPDATE operations skip many validations by design. The is_update helper from the gatekeeper-library is used to skip checks on UPDATE because some fields (like securityContext.privileged) are immutable after pod creation. Be explicit about which operations each template applies to using the operation field in spec.match.
ExternalData provider TLS is non-negotiable. Providers must serve TLS 1.3+. There is no HTTP option. Plan for certificate rotation from the start.
OPA v1.0 changed import rego.v1 from a compatibility shim to the default. Code using import future.keywords and import rego.v1 in the same file will fail to parse in OPA v1.x — remove the future.keywords imports.
conftest v0.64+ defaults to Rego v1 syntax. Older policies using p { ... } style rules without if will fail to parse. Run opa fmt --write --v0-v1 policy/ to auto-migrate.
Comments