Policy-as-Code Architecture (OPA Bundles, OCI, Gatekeeper, Kyverno)¶
This document specifies how to add, package, publish, and use RuleHub policies as OPA bundles in OCI registries, in Kubernetes (Gatekeeper, Kyverno), and in CI pipelines.
End-to-end Flow (author -> test -> bundle -> publish -> consume)¶
Figure (text alternative for the following mermaid diagram, for screen readers): The lifecycle proceeds linearly: Author policy (create Rego, metadata, and tests) -> Validate (format, schema) -> Unit tests (opa / kyverno / gatekeeper) -> Build OPA bundle (tar artifact) -> Publish to OCI registry -> Two parallel consumption paths: (a) CI pipelines (conftest / opa eval) and (b) Kubernetes admission controllers (Gatekeeper / Kyverno). This describes how authored policies become consumable artifacts.
flowchart LR
A[Author policy\n- Rego module(s)\n- metadata.yaml\n- tests] --> B[Validate\n- opa fmt/regal\n- metadata schema]
B --> C[Unit tests\n- opa test\n- kyverno test\n- conftest (if applicable)]
C --> D[Build OPA bundle\n- opa build -b policies\n- dist/opa-bundle.tar.gz]
D --> E[Publish to OCI\n- ORAS push to GHCR\n- mediaType: application/vnd.opa.bundle.layer.v1+tar]
E --> F[Consume in CI\n- conftest / opa eval\n- IaC checks]
E --> G[Consume in K8s\n- Gatekeeper (ConstraintTemplate/Constraint)\n- Kyverno (Policy/ClusterPolicy)\n- Optional: OPA sidecar loads bundle]
Repository structure and naming¶
- Policies live under
policies/<domain>/<policy_id>/ policy.rego- Rego module(s)policy_test.rego- unit tests for rulesmetadata.yaml- id, title, description, references to standards (PCI/GDPR/etc.), coverage, paths- Metadata schema under
tools/schemas/policy-metadata.schema.json(already present) - K8s integration examples under
addons/: - Gatekeeper ConstraintTemplates and Constraints:
addons/k8s-gatekeeper/{templates,constraints}/*.yaml - Kyverno policies:
addons/kyverno/policies/*.yaml - Helm chart for policy sets: external repository (
rulehub/rulehub-charts) - Docs:
docs/(this page, coverage, metadata docs) - CI workflows:
.github/workflows/**
Naming conventions:
- Rego package:
package rulehub.<domain>.<policy_id> - Policy ID:
<domain>.<slug>; do not mix kebab/underscore inside the same identifier. Prefer dot-separated package path and snake_case rule names if needed. - Versioning:
- Record standard versions (e.g., PCI DSS 4.0) in
metadata.yamlunderreferences. - OPA bundle tag: semantic version or a short SHA label. OCI reference example:
oci://ghcr.io/rulehub/rulehub-bundle:<tag>
Minimal file scaffolds¶
policies/<domain>/<policy_id>/policy.rego
- Contract:
- Input: depends on use case (e.g., Kubernetes object for admission, or IaC document for CI)
- Output: decision rules with boolean/objects
Example skeleton:
package rulehub.k8s.no_run_as_root
default deny := false
deny[msg] {
input.kind == "Pod"
some c
c := input.spec.containers[_]
not c.securityContext.runAsNonRoot
msg := "Containers must set securityContext.runAsNonRoot=true"
}
policies/<domain>/<policy_id>/policy_test.rego
package rulehub.k8s.no_run_as_root
import data.rulehub.k8s.no_run_as_root as pol
# happy path
test_all_containers_run_as_non_root {
input := {
"kind": "Pod",
"spec": {"containers": [{"securityContext": {"runAsNonRoot": true}}]}
}
not pol.deny[_]
}
# violation
test_violation_when_missing_flag {
input := {
"kind": "Pod",
"spec": {"containers": [{"name": "c1"}]}
}
count(pol.deny) == 1
}
policies/<domain>/<policy_id>/metadata.yaml (fields per existing schema; example):
id: k8s.no_run_as_root
name: 'K8s: Disallow root user'
version: 1.0.0
owner: platform-security
path:
- policies/k8s/no_run_as_root/policy.rego
- policies/k8s/no_run_as_root/policy_test.rego
references:
- standard: Kubernetes
section: SecurityContext
- standard: GDPR
section: Art. 5(1)(f)
- standard: PCI DSS
version: '4.0'
section: 2.2
Contributor path: Add a policy (step-by-step)¶
- Create a directory:
policies/<domain>/<policy_id>/ - Add
policy.regofollowing Styra Rego Style Guide (packages, rule naming, comments) - Add
policy_test.regowith unit tests (opa test) - Add
metadata.yamlvalid pertools/schemas/policy-metadata.schema.json - Run local checks:
- Format:
opa fmt -w policies/<domain>/<policy_id>/policy.rego - Validate metadata:
make validate - Unit tests:
opa test -v policies - If applicable, add K8s examples in
addons/; external chart repo manages packaging. - Update compliance maps in
compliance/maps/*.ymlif this policy contributes to coverage - Submit PR; CI runs validate -> test -> bundle (if Rego present) -> optionally publish on main
Consumer path: Use a policy¶
In CI (IaC validation):
-
Pull the bundle from OCI and evaluate against files using Conftest or OPA:
-
Conftest example:
- Fetch bundle artifact (e.g., using
oras pulltodist/opa-bundle.tar.gz) - Run:
conftest test -p dist/opa-bundle.tar.gz path/to/manifests/
- Fetch bundle artifact (e.g., using
-
OPA example:
opa eval -b dist/opa-bundle.tar.gz -i input.json "data.rulehub.k8s.no_run_as_root.deny"
In Kubernetes:
- Gatekeeper
- Apply ConstraintTemplate with embedded Rego, then create Constraint targeting resources
- Example ConstraintTemplate (excerpt):
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srunasnonroot
spec:
crd:
spec:
names:
kind: K8sRunAsNonRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srunasnonroot
violation[{"msg": msg}] {
some c
c := input.review.object.spec.containers[_]
not c.securityContext.runAsNonRoot
msg := "Containers must set securityContext.runAsNonRoot=true"
}
- Example Constraint:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRunAsNonRoot
metadata:
name: disallow-root-users
spec:
match:
kinds:
- apiGroups: ['']
kinds: ['Pod']
- Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-root-users
spec:
validationFailureAction: enforce
rules:
- name: disallow-root
match:
any:
- resources:
kinds: [Pod]
validate:
message: 'Containers must set securityContext.runAsNonRoot=true'
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
Optional runtime OPA: configure OPA sidecar to pull bundles from OCI registry as a bundle source (see OPA docs) and evaluate decisions through its API.
Make/CI steps (validate -> test -> bundle -> publish)¶
Make targets (see repo Makefile):
make validate- validate metadata against JSON Schemamake test- run Kyverno and Gatekeeper testsmake opa-bundle- builddist/opa-bundle.tar.gzfrompolicies/(if.regofiles exist)make oras-publish IMAGE=ghcr.io/rulehub/rulehub-bundle TAG=<tag>- publish the bundle to OCI with correct mediaType
GitHub Actions workflow .github/workflows/opa-bundle-publish.yml publishes on pushes to main and releases.
Rego Style Guide (Repository Conventions)¶
This repository locks in a subset of the Styra / community style plus additional safety constraints aligned with Rego v1 parsing rules enforced by our current OPA version.
Goals:
- Deterministic parsing across OPA minor upgrades
- Readable, uniform violation structure
- Early surfacing of logic errors (parse/type) before bundle build
Mandatory Patterns¶
- Atomic deny rules
Prefer multiple small deny["<id>"] or deny[msg] (fixed message form) rules over one large rule with boolean chains.
- Control (allow) rule
Pattern for policies using an allow decision:
package rulehub.<domain>.<policy_id>
deny["<short_reason>"] { <atomic condition 1> }
deny["<other_reason>"] { <atomic condition 2> }
# Allow when nothing denied
allow if count(deny) == 0
Alternative boolean default (default allow := true then set allow := false) is discouraged for consistency.
- Formatting
All Rego sources must pass opa fmt (pre-commit opa-fmt, CI quick check).
- Package naming
package rulehub.<domain>.<policy_id> (lowercase, dotted). No hyphens inside segments.
- Tests colocated
policy_test.rego MUST use same package (and may alias: import data.rulehub.<domain>.<policy_id> as pol).
Prohibited constructs (auto-scanned)¶
The quick check (make opa-quick-check and workflow opa-quick-check.yml) fails on these textual patterns:
(not- Parenthesized negation grouping inside composite boolean expressions.and not- Inline conjunction + negation; expand to separate rule or explicit equality lines.not (- Leadingnotapplied to grouped expression containingand/or.not in {- Negated membership; rewrite as chained inequalities or explicit whitelist set.
Rationale: Older / stricter Rego parser modes and future optimizations reject or mis-handle certain parenthesized negations; splitting conditions keeps each violation predicate simple and avoids hidden precedence errors.
Rewrite examples (violation[...] -> deny[...])¶
Anti-pattern:
violation[msg] { (not input.enabled) or (not input.flag); msg := "..." } # example
Preferred:
deny["feature_disabled"] { input.enabled == false }
deny["flag_missing"] { input.flag == false }
Anti-pattern:
deny { input.a and not input.b }
Preferred:
deny["b_missing_when_a"] { input.a == true; input.b == false }
Anti-pattern:
violation { not (input.x and input.y) }
Preferred: separate violations (one per missing predicate) or explicit positive checks then count(violation)==0 for allow.
Anti-pattern:
input.status not in {"A","B"}
Preferred:
input.status != "A"
input.status != "B"
or
allowed := {"A","B"}
not allowed[input.status]
Test style¶
Each test case should:
- Create a self-contained
input := {...}object (no reliance on global state) - Assert absence/presence of denies explicitly (
not pol.deny[_]orcount(pol.deny) == N) - Cover at least one happy path and one failing path per policy
Deny rule normalization patterns¶
- Split disjunctions (OR) across separate
deny[...]rules. - Convert
A and not Binto a single deny block listing both atomic lines (A == trueandB == false). - Replace negated group
not (A and B and C)with multiple atomic deny rules (one per missing/false predicate) unless semantic meaning differs. - Replace
X not in { ... }with either explicit inequalities or a positive membership check plusnoton the set (allowed[X]pattern) - but avoid negative set membership in a single expression.
Tooling enforcement¶
| Check | Mechanism | Invocation |
|---|---|---|
| Formatting | opa fmt -l via pre-commit opa-fmt |
pre-commit run opa-fmt |
| Forbidden patterns | grep scan (see Makefile) | make opa-quick-check |
| Parse/type errors | opa check policies |
make opa-quick-check |
| deny[] rule usage scan | tools/deny_usage_scan.py |
(add to CI before bundle) |
| Unit tests | opa test / make test-gatekeeper |
CI & local |
Violations of style should be fixed in-place rather than waived.
Quality checklists¶
Rego
- Formatted (
opa fmt), optionalregallint - Clear package names:
rulehub.<domain>.<policy_id> - Tests cover happy path and violations; no magic constants without context
Metadata
- Valid per schema;
pathincludes all relevant files - References to standards include version/section where possible
Bundle
- Built via
opa build -b policies -o dist/opa-bundle.tar.gz - Contents verified with
tar tzf dist/opa-bundle.tar.gz - Media type used for OCI:
application/vnd.opa.bundle.layer.v1+tar
Publication
- ORAS push uses semantic or SHA tags
- Artifact annotated with repository and commit information
Kubernetes
- Gatekeeper: provide
ConstraintTemplateandConstraintexamples with parameter schema where needed (usedeny[msg]; avoidviolation[...]) - Kyverno:
validationFailureActionset appropriately; include test fixtures if applicable
References (primary sources)¶
- OPA Bundles & distribution
- OPA Bundles
- ORAS CLI (ORAS CLI)
- Rego Style Guide
- Styra Rego Style Guide
- Gatekeeper
- Gatekeeper
- Kyverno
- Kyverno
- Conftest
- Conftest