August 2026
GRC Engineering 6 Week Challenge
An in-progress build turning NIST 800-53 controls into running code. Two weeks in: a Terraform baseline that is compliant on creation, and three of its controls rewritten as Open Policy Agent rules that reject a non-compliant plan before anything is deployed.
A six-week build in the GRC Engineering community, one week per deliverable. The through-line is a single idea: a control that lives in a document is a control somebody has to remember, and a control that lives in code is a control that enforces itself.
Status: 2 of 6 weeks complete. Week 1 built the compliant thing. Week 2 made the rules that judge it executable. What follows is what is actually in the repo today, not the plan for what goes in it later.
Week 1 - Your first compliant resource
The premise is that compliance is cheapest at creation. Rather than deploy an S3 bucket and then remediate it, the Terraform describes a bucket that has never been non-compliant, with four controls satisfied in the same apply that creates it.
Two buckets: a primary and a dedicated log bucket.
- SC-28 (Protection of Information at Rest) - server-side encryption on both buckets, AES256 by default.
- AC-3 (Access Enforcement) - a public access block on both buckets with all four flags true. Three of four is not a partial pass; it is an open bucket.
- CM-6 (Configuration Settings) - versioning on the primary, plus a provider
default_tagsblock so every taggable resource inheritsProject,Environment,ManagedBy, andComplianceScopewithout anyone having to remember to tag it. - AU-3 (Content of Audit Records) - ownership controls, then a
log-delivery-writeACL on the log bucket, then access logging pointed from the primary at it. The ordering matters: the ACL cannot be applied until object ownership is set, so the dependency is explicit.
# CM-6 - every taggable resource inherits these, no exceptions to police.
provider "aws" {
region = var.region
default_tags {
tags = {
Project = "grc-challenge"
Environment = "test"
ManagedBy = "GRC"
ComplianceScope = "NIST 800-53"
}
}
}
The audit habit that carried over intact was the evidence step. The plan is exported as JSON and kept:
terraform plan -out=tfplan
terraform show -json tfplan > evidence/plan.json
That file is not a build artifact. It is the population - a complete,
machine-readable record of exactly what was going to be created, captured before
it was created. A verify.sh then re-checks encryption, versioning, and the
public access block against the live bucket after apply, which is the difference
between design effectiveness and operating effectiveness stated in shell.
Week 2 - Make the rules executable
Week 1 produced a compliant bucket. It did not produce anything that would notice if the next bucket was not.
Week 2 rewrites three of those controls as Open Policy Agent policies that run against the plan JSON. Same controls, opposite direction: instead of describing what compliant looks like, they describe what gets rejected.
Each policy carries a metadata header binding it back to its source control:
# METADATA
# title: SC-28 - Encryption at Rest (AWS S3)
# custom:
# control_id: SC-28
# framework: nist-800-53
# severity: high
# remediation: Add aws_s3_bucket_server_side_encryption_configuration referencing the bucket.
package compliance.sc28_aws
That header is what makes a denial auditable rather than merely correct. Every failure traces back to a control ID, a framework, a severity, and the specific remediation - which is most of a finding write-up, generated automatically.
The technique that makes it work
At plan time, resource names do not exist yet. The bucket name carries a random suffix that is not generated until apply, so matching on values fails.
The answer is to match on references. Terraform’s plan JSON records the
dependency graph in configuration.root_module.resources[], where an encryption
resource points at its bucket via expressions.bucket.references - strings like
"aws_s3_bucket.primary.id".
deny contains msg if {
some bucket in input.configuration.root_module.resources
bucket.type == "aws_s3_bucket"
addr := sprintf("aws_s3_bucket.%s", [bucket.name])
not bucket_has_encryption(addr)
msg := sprintf("SC-28: %s has no server-side encryption configuration", [addr])
}
bucket_has_encryption(addr) if {
some enc in input.configuration.root_module.resources
enc.type == "aws_s3_bucket_server_side_encryption_configuration"
some ref in enc.expressions.bucket.references
startswith(ref, addr)
}
AC-3 needs both halves of the plan file, and that turned out to be the more
interesting one. The relationship - which public access block belongs to which
bucket - only exists in configuration, while the four flag values only exist
in planned_values. So the rule resolves the block by reference, reconstructs
its address, then reads the flags from planned values and requires all four to
be true. CM-6 is simpler: read values.tags_all, which is the post-merge tag
set including provider defaults, and deny once per missing tag so the message
names the exact one.
Tests are the specification
Every policy ships with unit tests covering both directions - a compliant plan must produce no denial, and a non-compliant plan must be denied.
opa test policies/ -v # 6/6 passing
This is the part that translates most directly from audit work. The starter begins at 3 passing, 3 failing, and the three that pass are the happy path ones - they pass against a stub that denies nothing at all. A control test that only proves the compliant case is not evidence the control operates. Proving the negative case is.
Running against the real week 1 plan is then one command per control:
conftest test --policy policies --namespace compliance.sc28_aws plan.json
All three pass against the compliant plan. Delete the encryption block from a copy, regenerate, and SC-28 fails with a message naming the resource and the fix.
Where this is going
Weeks 3 through 6 are still ahead. The obvious next move is wiring conftest
into CI so it runs on every pull request - at which point the control stops
being a quarterly sample of what already shipped and becomes a gate on what
ships at all, with the pipeline log as its evidence. I will write those up as
they are actually built.