Shayl.Taveras
portfolio / projects / policy-as-code-gate-conftest
← back to portfolio
// Project Walkthrough · 06
Policy as Code Gate with Conftest — AWS

Conftest policy gate that blocks Terraform applies on non-compliant AWS infrastructure by running three Rego policies against plan JSON — fail-closed, exit-code-driven, zero screenshots.

conftest opa rego aws terraform · nist 800-53 · ci · policy-as-code
Problem Statement
01

Wiring the Rego policy library into a real AWS Terraform workflow immediately surfaced the cross-cloud coverage gap: a Rego rule that checks google_storage_bucket returns zero findings against an AWS plan — it passes silently, proves nothing, and gives a false sense of compliance.

The fix is AWS-specific policy variants that check aws_s3_bucket resource types while keeping the same NIST control IDs. The result is a fail-closed, exit-code-driven gate that blocks applies on non-compliant infrastructure and deposits machine-readable JSON evidence — no console screenshots, no manual attestation.

The Cross-Cloud Coverage Problem
02

This is the core insight of the lab and the reason cloud-agnostic policy libraries are a trap. A policy written for GCP resource types will evaluate to undefined against an AWS plan — OPA treats undefined as an empty set, which means no denials, which means the gate passes. From the outside it looks like the infrastructure passed the compliance check. It didn't — it was never checked at all.

// key insight
A policy gate that passes because no rules matched is indistinguishable from a gate that passed because everything was compliant. The fix is not a single multi-cloud policy — it's cloud-specific variants under the same control IDs, each covering the resource types that actually exist in that plan.
Architecture Decision
03
// sc-28 by reference
Match Configuration, Not Values
Bucket names contain a random_id suffix unknown at plan time — planned_values.bucket is null. Matching by configuration.references against aws_s3_bucket.primary.id resolves correctly at plan time.
// ac-3 by planned values
Split the Lookup
The four public-access-block flags are concrete in planned_values even though the bucket ID isn't. Reference matching lives in configuration, flag validation lives in planned_values.
// tags_all not tags
CM-6 Catches Provider Tags
tags_all merges provider default_tags with resource-level tags. Checking tags alone misses tags applied at the provider level — a compliance gap that looks clean at the resource level.
// || true per namespace
All Namespaces Evaluate
A failure in one namespace doesn't abort the script before others run. Every policy evaluates, every result lands in the evidence file, exit code is set at the end.
AWS Policy Variants
04

Three policies, each an AWS-specific variant of the GCP policies built in Lab 3.3. Same control IDs, different resource types and attribute paths.

SC-28 · aws_encryption.rego
S3 Encryption via KMS Reference
Matches by configuration.references rather than planned_values because the bucket name contains a random_id suffix that is null at plan time. Checks that a KMS key reference exists in the bucket's encryption configuration. Deny returns the resource address and SC-28 with exact remediation step.
AC-3 · aws_public_access.rego
All Four Public Access Block Flags
Validates all four AWS S3 public access block flags against planned_values — block_public_acls, block_public_policy, ignore_public_acls, restrict_public_buckets must all be true. Any single flag missing or false triggers a deny. Resource address and AC-3 returned with the specific flag that failed.
CM-6 · aws_tags.rego
Required Tags via tags_all
Checks tags_all rather than tags to catch labels applied at the provider level via default_tags. Uses set subtraction — required_keys minus provided_keys — to identify exactly which tags are missing per resource. Deny returns the resource address, missing tag keys, and CM-6.
The policy-gate.sh Wrapper
05

The wrapper script is the CI entrypoint. It runs all policy namespaces, captures machine-readable JSON evidence, and exits non-zero on any failure. The || true pattern on each conftest call ensures all namespaces evaluate before the exit code is determined. A python3 one-liner parses the JSON output to set the exit code without depending on conftest's text format.

// policy-gate.sh — simplified
#!/bin/bash
PLAN="terraform/plan.json"
EVIDENCE="evidence/policy-results.json"

# Run all namespaces — || true so one failure doesn't abort the rest
conftest test --namespace compliance.sc28 --output json $PLAN >> $EVIDENCE || true
conftest test --namespace compliance.ac3 --output json $PLAN >> $EVIDENCE || true
conftest test --namespace compliance.cm6 --output json $PLAN >> $EVIDENCE || true

# Parse JSON evidence to set exit code
python3 -c "import json,sys; results=json.load(open('$EVIDENCE')); sys.exit(1 if any(r.get('failures') for r in results) else 0)"
// ci entrypoint
The --output=json flag means CI gets a machine-readable artifact it can parse, store, and diff across runs — not a text blob that breaks on conftest version upgrades.
Gate Output Evidence
06

The broken-plan test confirms the gate fires with a deny message naming the resource, the control, and the exact remediation step. The wrapper exits non-zero, blocking the apply.

Conftest gate passing — all three namespaces green on compliant plan
// compliant plan — all three namespaces pass: sc28_aws, ac3_aws, cm6_aws · 1 test, 1 passed, 0 failures each
Conftest gate firing — SC-28 FAIL with full deny message on broken plan
// broken plan — SC-28 fires: aws_s3_bucket.primary has no matching server_side_encryption_configuration · resource address, control ID, and remediation returned
File Structure
07
tools/rego-policies/
  policies/aws/
    aws_encryption.rego # SC-28 — S3 KMS reference matching
    aws_public_access.rego # AC-3 — all four block flags
    aws_tags.rego # CM-6 — tags_all required key check
  tests/aws/
    aws_encryption_test.rego
    aws_public_access_test.rego
    aws_tags_test.rego
  terraform/ # compliant + broken plan fixtures
  policy-gate.sh # CI entrypoint — runs all namespaces, exits non-zero on failure
  evidence/ # policy-results.json — machine-readable gate output
Summary
08
3
aws policies
3
controls enforced
1
gate script
0
screenshots needed
Controls
SC-28
Encryption at Rest
AC-3
Access Enforcement
CM-6
Config Settings
Key Decisions
SC-28 match
configuration.references
AC-3 flags
planned_values
CM-6 tags
tags_all
wrapper
|| true + json parse
Links
github
portfolio