Policy-as-Code: Securing Kubernetes and IaC with OPA/Rego and Kyverno
Policy as code with OPA Rego and Kubernetes lets you deny a privileged pod before it ever starts. OPA/Rego vs Kyverno, real examples, testing, and NIS2/DORA proof.
A pod is running in production with privileged: true. Nobody decided that explicitly: a manifest copied from a tutorial, a Dockerfile nobody ever reviewed, an image pulled from a public registry without any signature check. The cluster accepted it because, technically, nothing stopped it. That is exactly the problem policy as code solves: turning a rule written in a security document into a barrier enforced by the platform itself, at object-creation time, not after the fact in an audit report.
Admission control, concretely
Kubernetes exposes a precise anchor point for this control: the admission webhook. Every request to create or modify an object (a pod, a deployment, a network rule) passes through a validation chain before it is persisted to etcd. A policy engine wired into that chain can reject the request before the pod ever starts.
Two concrete cases, common in audits:
- A pod requests
securityContext.privileged: trueor mounts the host's Docker socket: without a policy, it starts normally and holds near-root access on the node. - An image comes from an unverified registry or is not signed (Cosign, Notary): without a policy, Kubernetes makes no distinction between an image signed by the official CI chain and one pushed manually by anyone holding registry credentials.
In both cases, admission control turns a potential incident into a silent, logged rejection, before impact.
OPA/Rego or Kyverno: two philosophies
Two engines dominate this space, with different logics.
OPA (Open Policy Agent) and its Rego language are general-purpose. The same engine can validate a Kubernetes manifest, a Terraform plan before apply, an Envoy routing rule, or a CI/CD pipeline step. Rego is a full logical query language with its own learning curve: you think in terms of facts and rules, not sequential YAML. That is the trade-off for its multi-stack versatility.
Kyverno is Kubernetes-native and written entirely in YAML, with no third-party language to learn. A team already comfortable with Kubernetes manifests can write a Kyverno policy in minutes. The limitation is scope: Kyverno never leaves the Kubernetes ecosystem, it will not validate a Terraform plan or an Envoy rule.
In practice, the hybrid approach is the most common in mature environments: Kyverno for purely Kubernetes policies with high volume and low complexity (mandatory labels, securityContext restrictions, quotas), OPA/Rego for anything that spans multiple stacks (IaC, API gateway, pipelines) or needs finer decision logic. The wrong instinct is picking a single engine on principle rather than on actual scope.
Anatomy of a Rego policy
A Rego policy is organized into a package (the logical namespace) and rules. The deny rule is the most common convention for admission control: every instance of deny that evaluates to true blocks the request and returns the associated message.
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf(
"Container '%v' in pod '%v' is privileged: denied by policy.",
[container.name, input.request.object.metadata.name],
)
}The structure is readable: package scopes the policy, the deny rule decomposes the condition (object type, iteration over containers, field check), and msg builds the message returned to whoever attempted the deployment. That message is what shows up in the kubectl apply output, not a generic error.
The Kyverno equivalent for the same rule fits in declarative YAML, with no query logic to write:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-containers
spec:
validationFailureAction: Enforce
rules:
- name: block-privileged
match:
resources:
kinds: ["Pod"]
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
containers:
- =(securityContext):
=(privileged): "false"Same intent, two syntaxes. The choice depends on the scope to cover, not on the team's stylistic preference.
Testing before you block a critical deployment
A badly written policy is as dangerous as no policy at all: it can block a legitimate deployment in the middle of an on-call shift, or let through a case it thought it covered. Two tools prevent that double failure.
opa test runs unit tests written in Rego against the policy, before any deployment to an admission controller:
opa test policies/ -vpackage kubernetes.admission
test_privileged_pod_denied {
deny["Container 'app' in pod 'demo' is privileged: denied by policy."] with input as {
"request": {
"kind": {"kind": "Pod"},
"object": {
"metadata": {"name": "demo"},
"spec": {"containers": [{"name": "app", "securityContext": {"privileged": true}}]},
},
},
}
}conftest applies the same principle directly to manifest files or Terraform plans exported as JSON, which lets you test a policy in the CI/CD pipeline before it ever reaches the cluster:
conftest test --policy policies/ deployment.yamlThe recommended rollout order stays constant: opa test to validate the rule's own logic, conftest test in CI to validate a broad corpus of real manifests without regressions, and only then enforcement mode inside the cluster. Skipping that step is the classic scenario of an on-call engineer woken at 2 a.m. by a deployment blocked for a reason nobody ever tested.
From technical policy to compliance proof
NIS2 and DORA require documented technical controls and, increasingly, proof of automation rather than manual declarative procedures. Policy as code provides a direct mapping between a regulatory requirement and an enforced rule:
| NIS2/DORA requirement | Corresponding technical policy |
|---|---|
| Encryption at rest and in transit | Rego/Kyverno denying a PVC without encryption, or a service without TLS |
| Network segmentation | Kubernetes NetworkPolicy generated and validated by policy, blocking unauthorized pod-to-pod traffic |
| Access management (least privilege) | RBAC rules audited automatically, denial of overly broad ClusterRoleBindings |
| Software supply chain integrity | Admission denial of unsigned images |
This mapping is not cosmetic: in a NIS2 or DORA audit, proving that a control is enforced automatically and systematically (not merely documented in a procedure) is what separates audited compliance from declarative compliance. The admission controller's denial log becomes a directly usable piece of evidence, timestamped and tied to a specific object, rather than a screenshot taken manually once a quarter.
That distinction matters because auditors increasingly ask not just "do you have a policy" but "show me it firing." A denial log entry referencing a rejected ClusterRoleBinding or an unsigned image is far harder to dispute than a paragraph in a security handbook nobody has re-read since it was written.
In summary
Policy as code moves the security control from the document to the runtime: a Rego or Kyverno policy denies drift before it produces an incident, not weeks later in an audit report. The engine choice follows the scope (Kyverno for pure Kubernetes, OPA/Rego for multi-stack), and rollout to production never skips unit testing beforehand. The NIS2/DORA mapping then turns every technical policy into a piece of automated compliance evidence.
That is exactly what the ThreatClaw policy pack provides: pre-tested Rego and Kyverno rules, ready for admission control, with the compliance mapping already built in, so you are not writing and validating every rule under pressure the week of the audit.
Related articles
Google observes clusters attacked within 18 minutes and escapes via privileged pods. Here is runtime detection with Falco and admission guardrails with OPA.
Every ThreatClaw feed subscription now ships with a per-requirement coverage map for NIS2 and ReCyF, an OSCAL export, and a connector that pre-fills your GRC. Here is exactly how it works, and what it does not claim.
The Commission referred France to the CJEU on July 8, 2026 for failing to transpose NIS2. Penalties loom and the resilience law is delayed: what to anticipate.
OPA Rego permissions for AI agent MCP tool calls: how to interpose a policy-as-code decision before every call, based on role, data sensitivity, and time.