Skip to main content
INS // Insights

Compliance as Code: OPA and Rego for Federal CI/CD

Updated April 2026 · 8 min read

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.