Manual compliance checking is the bottleneck in federal software delivery. A security engineer reviewing Terraform plans for NIST control compliance can assess a few dozen resources per day. An OPA policy engine evaluates thousands of resources per second, consistently, without fatigue or interpretation variance.
Compliance as code — expressing NIST 800-53 control requirements as executable policy that runs in CI/CD — turns compliance from a periodic gate into a continuous property of the codebase. Rutagon implements OPA/Rego-based compliance gates across Terraform infrastructure provisioning, Kubernetes admission control, and API authorization.
Open Policy Agent Architecture
OPA is a general-purpose policy engine that evaluates Rego queries against structured input (JSON/YAML). It runs as a sidecar, standalone daemon, or library — the deployment model adapts to where policy decisions need to be made:
Input (JSON) OPA Engine Output (JSON)
───────────── ────────────────── ─────────────────
Terraform plan → Rego policies → allow: true/false
K8s AdmissionReq (data + rules) violations: [...]
API request compliance: {...} For federal programs, OPA policies encode NIST 800-53 controls directly — a failing Rego evaluation means a control is violated, and the build stops.
Terraform Plan Validation with OPA
Every terraform plan generates a JSON representation of the proposed infrastructure changes. OPA evaluates this plan against compliance policies before any resources are created:
# nist_encryption.rego — SC-28 encryption at rest controls
package federal.nist.sc28
import future.keywords.if
import future.keywords.in
# DENY: S3 bucket without server-side encryption
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.actions[_] in {"create", "update"}
# Check that SSE configuration exists
not resource.change.after.server_side_encryption_configuration
msg := sprintf(
"SC-28 violation: S3 bucket '%s' must have server-side encryption enabled",
[resource.address]
)
}
# DENY: KMS key without rotation enabled
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_kms_key"
resource.change.actions[_] in {"create", "update"}
not resource.change.after.enable_key_rotation == true
msg := sprintf(
"SC-12 violation: KMS key '%s' must have key rotation enabled (annual rotation required)",
[resource.address]
)
}
# DENY: RDS instance without encryption
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
resource.change.actions[_] in {"create", "update"}
not resource.change.after.storage_encrypted == true
msg := sprintf(
"SC-28 violation: RDS instance '%s' must have storage_encrypted = true",
[resource.address]
)
}
# ALLOW if no violations
allow if {
count(deny) == 0
} # nist_access_control.rego — AC-3, AC-6 least privilege controls
package federal.nist.ac
import future.keywords.if
import future.keywords.in
# DENY: IAM role with AdministratorAccess
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_iam_role_policy_attachment"
resource.change.actions[_] in {"create"}
contains(resource.change.after.policy_arn, "AdministratorAccess")
msg := sprintf(
"AC-6 violation: Role '%s' has AdministratorAccess — violates least privilege principle",
[resource.address]
)
}
# DENY: Security group with 0.0.0.0/0 ingress on sensitive ports
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.type == "ingress"
resource.change.actions[_] in {"create", "update"}
cidr := resource.change.after.cidr_blocks[_]
cidr == "0.0.0.0/0"
port := resource.change.after.from_port
port in {22, 3389, 5432, 3306, 27017, 6379} # SSH, RDP, DB ports
msg := sprintf(
"SC-7 violation: Security group rule '%s' exposes port %d to 0.0.0.0/0",
[resource.address, port]
)
} CI/CD Integration
# GitLab CI — OPA Terraform validation
terraform-compliance:
stage: validate
image: openpolicyagent/opa:latest-envoy
before_script:
- terraform init -backend=false
- terraform plan -out=plan.tfplan
- terraform show -json plan.tfplan > plan.json
script:
- |
# Evaluate all compliance policies
opa eval \
--data policies/ \
--input plan.json \
--format pretty \
"data.federal.nist.sc28.deny | data.federal.nist.ac.deny" \
> violations.json
VIOLATION_COUNT=$(cat violations.json | python3 -c "
import json, sys
v = json.load(sys.stdin)
total = sum(len(x) for x in v)
print(total)
")
if [ "$VIOLATION_COUNT" -gt "0" ]; then
echo "COMPLIANCE GATE FAILED: $VIOLATION_COUNT violations found"
cat violations.json
exit 1
fi
echo "Compliance gate passed — 0 violations"
artifacts:
when: always
paths:
- violations.json
expire_in: 90 days Kubernetes Admission Control with OPA Gatekeeper
OPA Gatekeeper enforces compliance policies at the Kubernetes API layer — before any resource is created in the cluster:
# ConstraintTemplate — enforce non-root container requirement
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequirenonroot
spec:
crd:
spec:
names:
kind: K8sRequireNonRoot
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequirenonroot
# NIST CM-6: Configuration settings
# DISA STIG V-242415: Containers must not run as root
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf(
"Container '%s' must set securityContext.runAsNonRoot = true",
[container.name]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.runAsUser == 0
msg := sprintf(
"Container '%s' must not run as root user (UID 0)",
[container.name]
)
} # Constraint — apply the template to production namespace
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
name: require-non-root-production
spec:
enforcementAction: deny # Reject — not just warn
match:
namespaces:
- production
- staging
kinds:
- apiGroups: [""]
kinds: ["Pod"] Gatekeeper constraints cover the full DoD container STIG requirements: non-root execution, read-only root filesystems, resource limits, seccomp profiles, and prohibited privilege escalation.
Policy Organization and Versioning
Federal compliance policies are versioned like application code:
policies/
├── nist/
│ ├── ac/ — Access Control family
│ │ ├── ac_3.rego
│ │ ├── ac_6.rego
│ │ └── ac_17.rego
│ ├── sc/ — System and Communications Protection
│ │ ├── sc_7.rego
│ │ ├── sc_12.rego
│ │ └── sc_28.rego
│ ├── si/ — System and Information Integrity
│ │ ├── si_3.rego
│ │ └── si_7.rego
│ └── au/ — Audit and Accountability
│ ├── au_2.rego
│ └── au_12.rego
├── disa/
│ ├── aws_cloud_stig.rego
│ └── k8s_stig.rego
└── program/
└── custom_requirements.rego Policy changes go through the same code review and CI pipeline as application code — a policy weakening (allowing something previously denied) requires explicit justification in the pull request and security engineer approval.
ATO Evidence from Policy Evaluations
Every OPA evaluation generates evidence directly relevant to the ATO package:
# evidence_mapper.py — maps OPA results to NIST controls
POLICY_CONTROL_MAP = {
"federal.nist.sc28.deny": ["SC-28"],
"federal.nist.sc12.deny": ["SC-12"],
"federal.nist.ac.deny": ["AC-3", "AC-6", "SC-7"],
"federal.nist.au.deny": ["AU-2", "AU-12"],
"k8srequirenonroot": ["CM-6", "CM-7"],
}
def generate_control_evidence(opa_results: dict, pipeline_id: str) -> dict:
"""Map OPA evaluation results to NIST control evidence."""
evidence = {}
for policy_package, controls in POLICY_CONTROL_MAP.items():
result = opa_results.get(policy_package, {})
violations = result.get("violations", [])
for control in controls:
evidence[control] = {
"policyEvaluated": True,
"violationsFound": len(violations),
"compliant": len(violations) == 0,
"pipelineId": pipeline_id,
"details": violations
}
return evidence This evidence feeds directly into the ConMon dashboard and SSP — turning every pipeline run into a compliance attestation.
Compliance as code transforms NIST controls from documentation commitments to executable requirements. Programs operating with OPA/Rego compliance gates can demonstrate continuous control satisfaction to authorizing officials with machine-verifiable evidence rather than manual spot-checks. Rutagon delivers this capability as part of the integrated DevSecOps pipeline — policies maintained alongside application code, evidence generated on every commit.
Discuss compliance automation for your program →
Frequently Asked Questions
What is the difference between OPA Gatekeeper and Kyverno for Kubernetes policy?
Both Gatekeeper (OPA-based) and Kyverno enforce Kubernetes admission policies, but they differ in design philosophy. Gatekeeper uses Rego — a purpose-built policy language with strong expressiveness for complex logic. Kyverno uses YAML-based policy definitions that are more accessible to operators without programming backgrounds. Rutagon uses Gatekeeper for programs requiring complex cross-resource policy logic and Kyverno for programs prioritizing policy readability and simpler governance. Both achieve the same DoD STIG compliance outcomes.
Can OPA policies be shared across programs?
Yes — and this is one of the strongest arguments for compliance-as-code. Rutagon maintains a library of NIST 800-53 Rego policies that are reusable across programs. Program-specific requirements extend the base library without modifying it, using Rego packages and policy composition. This means each new program starts with a compliant baseline rather than building policies from scratch.
How do OPA policies handle NIST control inheritance from AWS GovCloud?
Cloud Service Provider (CSP) controls are inherited — AWS GovCloud handles physical security, hypervisor security, and foundational network controls. OPA policies focus on the customer-responsibility controls: encryption configuration, access permissions, network segmentation, and audit logging configuration. The inheritance documentation in the SSP maps inherited controls to AWS FedRAMP package documentation, and OPA policies document the customer-implemented controls.
What happens when OPA finds a violation in CI — does the build stop?
Yes — by design. OPA policy violations in CI are hard gates: the pipeline fails, the commit cannot progress to production, and the developer receives a specific violation message identifying which control was violated and which resource caused it. This is intentional. The alternative — soft gates that warn but allow progression — leads to accumulated violations that never get fixed. Production hardness of security gates is what makes compliance-as-code meaningful.
How often should Rego policies be updated?
Policies should be updated when: NIST publishes a new revision (800-53 Rev 6 is anticipated), DISA releases new STIGs for cloud services, AWS adds new GovCloud services that need coverage, or program requirements change. Rutagon's policy library is versioned with semantic versioning and programs pin to a policy version in their CI configuration — updates are deliberate, not automatic. A quarterly policy review is standard practice.