Write Once, Enforce Everywhere: Reusing Rego Policies Across Build and Runtime
Stop writing the same policy twice. With the use of Rego, you can enforce the same exact rules, both in CI/CD and in production.
Join the DZone community and get the full member experience.
Join For FreeIn most organizations, security and compliance are enforced twice — once during build-time checks and again at runtime through admission controllers and monitoring systems. Often, the policies written at build-time are not reused at runtime, leading to drift, redundancy, and gaps in enforcement. With the rise of Open Policy Agent (OPA) and Rego, teams now have the opportunity to unify policy logic and reuse it seamlessly across both phases.
This article discusses the principles, design patterns, and practical techniques for reusing Rego policies at build-time and runtime, helping teams reduce duplication, improve compliance confidence, and accelerate software delivery.
The Policy Fragmentation Problem
Organizations have traditionally split policy enforcement into two silos:
- Build-time policies: Applied during CI/CD pipelines to validate container images, infrastructure-as-code templates, or dependency versions. Example: “No container image may run as root.”
- Runtime policies: Enforced when applications are deployed and running — often through Kubernetes admission controllers. Example: “Pods cannot be scheduled on production nodes unless labeled.”
These two worlds often use different languages, frameworks, and enforcement mechanisms, resulting in:
- Duplication of the same policy logic across multiple systems.
- Leads to drift where build-time and runtime checks diverge.
- Creates blind spots where a policy is only partially enforced.
This results in an inconsistent compliance posture and wasted engineering effort.
Writing a Reusable Policy
Here’s a simple Rego policy that blocks containers from running as root:
package policies.security
deny[msg] {
input.spec.containers[_].securityContext.runAsUser == 0
msg := "Containers must not run as root user"
}
- Build-time: Use conftest in the CI/CD pipeline to validate Kubernetes manifests or Helm charts before deployment.
-
Runtime: Deploy the same policy using OPA Gatekeeper to enforce rules on live clusters.
This makes it possible to express a rule once and reuse it across the software lifecycle with zero drift.
Case Study: Kubernetes + Terraform
Consider an enterprise adopting Kubernetes and multi-cloud infrastructure. They face two common challenges:
- Kubernetes Pods must not run with privileged escalation.
- Terraform-managed AWS S3 buckets must enforce encryption.
Traditionally, these were checked separately — Kubernetes admission controllers for pods and static analysis tools for Terraform. With Rego, the organization defined policies once and reused them everywhere.
Kubernetes Policy (Privileged Escalation)
package policies.k8s
deny[msg] {
input.kind == "Pod"
input.spec.containers[_].securityContext.allowPrivilegeEscalation == true
msg := "Privilege escalation not allowed in Pods"
}
- CI/CD: Run conftest test pod.yaml to block bad manifests before merge.
-
Runtime: Enforce via Gatekeeper so developers can’t sneak in privileged pods.
Terraform Policy (S3 Encryption)
package policies.terraform
deny[msg] {
input.resource_type == "aws_s3_bucket"
not input.encryption.enabled
msg := sprintf("S3 bucket %v must enable encryption", [input.name])
}
- CI/CD: Run OPA eval against Terraform plan JSON.
-
Runtime: Hook into cloud policy engines that leverage OPA bundles.
Results
- One Git repo for all policies
- Same rules applied across dev, CI, and production
- Developers got faster feedback, and security got consistent enforcement
Best Practices for Policy Reuse
1. Policy as Code Repository
Store all Rego policies in a central Git repository. Reference them both in CI/CD and runtime controllers. Version control ensures auditability.
2. Abstract Inputs, Normalize Data
Build-time inputs (YAML manifests, Terraform templates) and runtime inputs (Kubernetes AdmissionReview objects) often differ. Define a normalization layer to convert inputs into a consistent schema.
package normalize.kubernetes
pod(cont) = {
"name": cont.name,
"user": cont.securityContext.runAsUser,
} {
cont := input.spec.containers[_]
}
3. Bundle Policies
Package policies into OPA bundles that can be distributed and reused across build and runtime. CI/CD pipelines can fetch the same bundle that Gatekeeper enforces in production.
4. Test Once, Deploy Everywhere
Write unit tests for Rego policies and run them in CI. This guarantees correctness before promoting policies to runtime enforcement.
Challenges and How to Address Them
Reusing policies across build-time and runtime introduces practical challenges. While the vision is straightforward — write once, enforce everywhere — the reality requires careful engineering.
1. Different Data Shapes
- Problem: At build-time, policies evaluate static manifests (e.g., YAML, Terraform plan files). At runtime, inputs are dynamic objects (e.g., Kubernetes AdmissionReview requests). Their schemas differ significantly. Without harmonization, the same Rego rule cannot be directly reused.
- Solution: Introduce a normalization layer. Write helper Rego modules that translate input data into a consistent schema before applying policies. Example: Map both Pod specs in YAML and AdmissionReview payloads into a normalized pod object with fields like user, privileged, and image. Policies remain environment-agnostic and easier to reuse.
2. Performance at Scale
- Problem: Build-time checks are relatively cheap (they run once per pipeline), but runtime enforcement may need to handle thousands of admission requests per second in Kubernetes or API gateway decisions. Evaluating raw Rego on every request can create latency. Runtime bottlenecks can degrade cluster performance or increase request latency.
- Solution: Using data filtering minimizes the input passed into OPA (e.g., only the relevant fields instead of the full AdmissionReview payload). Splitting heavy, complex rules into smaller, focused policies reduces evaluation cost.
3. Change Management and Policy Drift
- Problem: Policies evolve over time. A rule that is acceptable today may become too restrictive tomorrow (or vice versa). Updating policies across environments (build vs. runtime) introduces the risk of breaking developer workflows or blocking production deployments. Developers lose trust in the policy system if changes cause friction.
- Solution: Implement a progressive rollout strategy by introducing new rules in “warn” or “audit” mode first, capturing violations without blocking. Once confidence is built, move to enforce.
4. Debugging and Observability
- Problem: When policies block a deployment at runtime, developers often see a generic “denied” message. Without context, debugging is frustrating. Teams may bypass policy enforcement altogether to “unblock themselves.”
- Solution: Always return human-readable reasons from Rego (msg := "Containers must not run as root"). Enable OPA decision logging and integrate with logging platforms (e.g., ELK, Datadog).
5. Policy Testing and Regression Risks
- Problem: A single Rego change can inadvertently block valid workloads across multiple pipelines and clusters. It can lead to high regression risk — especially when policies are reused across environments.
- Solution: Write Rego unit tests (OPA test) to validate expected outcomes. Run automated tests in CI before publishing a new policy bundle.
Conclusion
Reusing policies across build-time and runtime is not just a best practice — it’s becoming essential for modern DevSecOps. By embracing OPA and Rego, organizations can unify policy enforcement, reduce redundancy, and ensure compliance without slowing down developers. The key lies in treating policies as code, centralizing them, and reusing them seamlessly across the software lifecycle.
In the world of cloud-native security, shift-left doesn’t mean abandoning runtime — it means reuse.
Opinions expressed by DZone contributors are their own.
Comments