AWS Policy as Code: SCPs, CloudFormation Guard, OPA, and AWS Config
Policy as code means writing your security and compliance rules in a machine-readable format and evaluating them automatically: in your CI pipeline, at deploy time, or continuously after deployment. Instead of hoping a reviewer catches every misconfigured S3 bucket, a policy check blocks the deployment before it happens. AWS offers four tools for this: Service Control Policies (SCPs), CloudFormation Guard, Open Policy Agent (OPA), and AWS Config. Each runs at a different stage and catches a different class of problem.
Simple explanation
Imagine your team has a rule: “No S3 bucket can ever be public.” Without policy as code, someone has to check every infrastructure change manually. With policy as code, you write that rule once and it runs automatically on every pull request, every deployment, and continuously across every existing resource.
A type checker in a programming language does not replace code review. It automates a specific class of checks so reviewers can focus on things that require human judgment. Policy as code does exactly the same thing for infrastructure security rules: consistent, automatic, and not dependent on anyone remembering to look.
The four AWS tools each cover a different moment in the infrastructure lifecycle:
- SCPs: block AWS API calls at the account level, before anything is created
- CloudFormation Guard: validate CloudFormation templates (or Terraform plan JSON) in CI before deployment
- OPA + Conftest: validate Terraform plans in CI before
terraform applyruns - AWS Config: detect drift and misconfigurations in already-deployed resources
AWS policy-as-code tools at a glance
Pick the right tool from this table before reading any further.
| Tool | Best for | Where it runs | What it checks | Best when | Not ideal when |
|---|---|---|---|---|---|
| SCPs | Hard account-level limits | AWS Organizations (runtime) | AWS API calls | Multi-account, non-bypassable rules | Single-account or per-template rules |
| CloudFormation Guard | Template validation before deploy | CI pipeline | CloudFormation or Terraform plan JSON | CloudFormation-heavy workflows | Post-deployment checks |
| OPA + Conftest | Terraform plan validation | CI pipeline | Terraform plan JSON | Terraform-heavy workflows, complex conditions | Simple template checks (cfn-guard is simpler) |
| AWS Config | Continuous drift detection | AWS account (always on) | Deployed resources | Post-deployment monitoring, compliance reporting | Blocking deployments (it detects, not prevents) |
Why teams use policy as code
A security team reviewing infrastructure changes manually faces a scaling problem. As a company grows, the volume of Terraform PRs and CloudFormation stacks increases while the security team size does not keep pace. Reviewers miss things. The same mistake gets made repeatedly because there is no automated check to catch it.
Policy as code turns compliance rules into executable checks that run on every change. When a developer opens a PR that creates a public S3 bucket, the check fails immediately with a clear error, before the security team ever sees the PR. This is especially valuable in teams using Infrastructure as Code in AWS, where every resource change goes through version control.
Three concrete benefits:
- Speed. Automated checks give developers instant feedback without waiting for a manual review cycle.
- Consistency. The same rule runs on every change, every time. Manual review is inconsistent by nature.
- Reduced bottleneck. Security teams can focus on policy design instead of reviewing individual PRs.
How it works in a CI/CD pipeline
Policy checks slot into different stages of a secure CI/CD pipeline. The goal is to catch problems as early as possible, ideally before a PR merges.
- PR opened. OPA/Conftest and cfn-guard run against the Terraform plan or CloudFormation template. If they fail, the CI job fails and the PR cannot merge.
- Deploy triggered. SCPs act as a runtime safety net. Even if a misconfiguration slips through CI, an SCP blocks the AWS API call that would create the non-compliant resource.
- After deployment. AWS Config continuously evaluates running resources. It catches drift from manual console changes or configurations that slipped through earlier checks.
This layered approach is why teams use multiple tools. Each layer catches a different class of problem. See how automated testing in CodeBuild fits alongside policy checks in the same build pipeline.
Service Control Policies (SCPs)
Service Control Policies are AWS Organizations policies that restrict what AWS API calls can be made within an account. An SCP applies to every principal in the account: IAM users, IAM roles, even the root user. No one can bypass an SCP, not even an account administrator.
SCPs do not grant permissions. They set the maximum permissions available in an account. If an SCP denies an action, it is denied regardless of what IAM policies say. This makes SCPs ideal for hard limits: preventing resource creation outside approved regions, blocking IAM policies that allow unrestricted access, or preventing VPC internet gateway creation in sensitive accounts.
This SCP prevents any resource from being created outside us-east-1 and us-west-2:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyNonApprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"support:*",
"sts:*",
"cloudfront:*",
"route53:*",
"waf:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"us-east-1",
"us-west-2"
]
}
}
}
]
}An SCP that denies all regions will silently break IAM, Route 53, and CloudFront if you forget the NotAction list. Global services do not operate in any specific region, so a blanket region restriction blocks them too. Always use NotAction for region-based SCPs and test in a non-production account first. See IAM policy structure for how NotAction works.
SCPs only work with AWS Organizations. If you are using a single-account setup, you cannot use SCPs. For single-account enforcement, rely on IAM permission boundaries and CloudFormation Guard instead. See AWS Organizations overview for multi-account setup.
When this is the right choice: Use SCPs when you need non-bypassable account-level limits in a multi-account AWS environment. Rules like “this account can never create resources outside the EU” or “no one can disable CloudTrail.” For per-template or per-resource validation, use CloudFormation Guard or OPA instead. For deeper coverage, see the Service Control Policies guide.
CloudFormation Guard
CloudFormation Guard (cfn-guard) is an open-source tool from AWS that evaluates CloudFormation templates and Terraform plan JSON against rules you define. It runs as a step in your CI pipeline and fails the build if any rule is violated.
cfn-guard rules use a domain-specific language designed for evaluating JSON/YAML documents. Here is a rule that requires S3 buckets to have server-side encryption enabled:
# s3-encryption-required.guard
rule s3_bucket_encryption_required {
AWS::S3::Bucket {
Properties {
BucketEncryption {
ServerSideEncryptionConfiguration[*] {
ServerSideEncryptionByDefault {
SSEAlgorithm exists
SSEAlgorithm in ["AES256", "aws:kms"]
}
}
}
}
}
}Install cfn-guard from the official releases page, then validate a template in CI:
# Validate a CloudFormation template against your rules
cfn-guard validate \
--data template.yaml \
--rules s3-encryption-required.guard \
--show-summary allFor Terraform workflows, convert the plan to JSON and run cfn-guard against it. This catches misconfigurations before terraform apply runs:
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
cfn-guard validate --data tfplan.json --rules rules/AWS maintains a Guard Rules Registry with pre-built rules for CIS, PCI DSS, and HIPAA compliance. Start there before writing your own. The registry gives you a working rule set in minutes, and you can layer your own custom rules on top. This integrates naturally with Terraform for AWS workflows where plan JSON is generated as a standard CI step.
When this is the right choice: Use cfn-guard when your team primarily uses CloudFormation, or when you want a simpler rule syntax than Rego. cfn-guard is also the natural pick if you want to start with AWS-maintained compliance rule sets instead of writing policies from scratch.
Open Policy Agent (OPA) + Conftest
Open Policy Agent (OPA) is a general-purpose policy engine used widely in Kubernetes admission control and infrastructure validation. With Conftest (a tool that wraps OPA for CI use), you can evaluate Terraform plans against Rego policies before applying them.
Rego is OPA’s policy language. It evaluates a JSON input document against a set of rules and produces a decision. Here is a Rego policy that blocks public S3 buckets:
# policies/s3-no-public-access.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf(
"S3 bucket '%s' must have block_public_acls enabled",
[resource.address]
)
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.ignore_public_acls == false
msg := sprintf(
"S3 bucket '%s' must have ignore_public_acls enabled",
[resource.address]
)
}Run this in CI against the Terraform plan JSON:
# Generate Terraform plan JSON
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# Run Conftest against your policies directory
conftest test tfplan.json --policy policies/If any deny rule matches, Conftest exits non-zero and the CI build fails. The error message includes the specific resource and rule that was violated, so developers know exactly what to fix. This integrates cleanly into a GitHub Actions workflow or a CodeBuild step. Keeping policies in a consistent directory across repositories is easier when you have a well-structured Terraform project layout.
Always run OPA against terraform show -json tfplan, not against Terraform source files. The plan JSON contains resolved values after variable substitution and data source lookups. Running against raw source files means checking unresolved templates, which produces unreliable results and false passes.
When this is the right choice: Use OPA when your team uses Terraform heavily and you want expressive, testable policies. Rego is more flexible than cfn-guard’s DSL for complex conditions: cross-resource rules or policies that depend on the relationship between multiple resources. OPA also works outside AWS, so teams using a mix of clouds or Kubernetes benefit from one policy language across all of them.
AWS Config
SCPs, cfn-guard, and OPA all operate before or at deployment time. AWS Config operates after. It continuously evaluates deployed resources against rules and flags any that fall out of compliance, whether through a misconfigured deployment or a manual change made in the AWS console outside the pipeline.
AWS Config solves a specific problem: drift. Someone with console access changes a security group rule. A Terraform run creates a resource with a default that does not match your policy. A managed rule gets disabled manually. None of these are caught by pre-deployment checks. AWS Config sees them all.
Enable managed Config rules to get continuous drift detection:
# Enable AWS Config rule to check S3 bucket encryption
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "s3-bucket-server-side-encryption-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
}
}'
# Enable rule to check that MFA is required for the root account
aws configservice put-config-rule \
--config-rule '{
"ConfigRuleName": "root-account-mfa-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "ROOT_ACCOUNT_MFA_ENABLED"
}
}'You can configure AWS Config to automatically remediate non-compliant resources using SSM Automation documents, or to send notifications to an SNS topic when a resource falls out of compliance. AWS Config also produces a compliance history that is useful for audit reports.
AWS Config detects problems after resources exist. By the time it fires an alert, the misconfigured resource is already running. Use it as your always-on monitoring layer, not your first line of defense. Pair it with solid least privilege practices to reduce the blast radius when something does slip through.
When this is the right choice: Use AWS Config for post-deployment drift detection, continuous compliance monitoring, and audit-ready reports. It catches what SCPs, cfn-guard, and OPA cannot: misconfigurations that happen after deployment, or resources changed outside the pipeline entirely.
When to use each tool
The four tools are not alternatives; they cover different layers. The question is which layers your environment needs now.
- Start with SCPs if you run multiple AWS accounts and need hard limits that cannot be bypassed by any IAM principal. This is the highest-leverage first step in a multi-account setup.
- Add cfn-guard if your team uses CloudFormation and wants to validate templates in CI without learning Rego. The Guard Rules Registry gives you pre-built rules for CIS, PCI DSS, and HIPAA.
- Add OPA if your team uses Terraform and needs flexible, testable policies. Rego handles complex cross-resource conditions that cfn-guard’s DSL cannot express cleanly.
- Add AWS Config when you need post-deployment drift detection, continuous compliance monitoring, or audit-ready reports.
- Use all four in mature environments. Each layer catches a different class of problem, and no single tool covers the full lifecycle.
If your team is just getting started, pick one tool and implement it consistently before adding others. SCPs are the right first step for multi-account environments. OPA or cfn-guard are the right first step if you want to add compliance checks to an existing Terraform or CloudFormation pipeline.
How the tools fit into a CI pipeline
Here is how the four tools map to pipeline stages in a typical CodeBuild-based workflow:
| Stage | Tool | What it catches |
|---|---|---|
| PR / CI | OPA + Conftest | Terraform plan violations before apply |
| PR / CI | cfn-guard | CloudFormation template misconfigurations |
| Runtime (always on) | SCPs | Blocked API calls regardless of how they are triggered |
| Post-deployment (continuous) | AWS Config | Resource drift and manual changes outside the pipeline |
SCPs, cfn-guard, and OPA are preventive: they stop misconfigurations before or during deployment. AWS Config is detective: it finds problems after resources exist. Both are necessary because no pre-deployment check is perfect. Manage your Terraform state carefully alongside these checks, since state drift can cause policy checks to produce false results.
SCP vs CloudFormation Guard vs OPA vs AWS Config
| Dimension | SCP | CloudFormation Guard | OPA + Conftest | AWS Config |
|---|---|---|---|---|
| When it runs | At runtime (every API call) | Before deployment (CI) | Before deployment (CI) | After deployment (continuous) |
| What it evaluates | AWS API calls | CloudFormation / Terraform plan JSON | Terraform plan JSON | Deployed AWS resources |
| Can it prevent deployment? | Yes (blocks API calls) | Yes (fails CI build) | Yes (fails CI build) | No (detects after the fact) |
| Requires AWS Organizations? | Yes | No | No | No |
| Policy language | IAM JSON | cfn-guard DSL | Rego | Managed rules or Lambda |
| Best for | Hard non-bypassable limits | CloudFormation teams | Terraform teams, complex rules | Drift detection, audit reporting |
The key distinction: SCPs and AWS Config are always-on AWS-managed services. cfn-guard and OPA are tools you run in your pipeline. The pipeline tools catch misconfigurations before they deploy; the always-on tools catch what slips through or changes afterward.
Common beginner mistakes
- Using SCPs in Deny-all mode without exception carve-outs. An SCP that denies all regions except us-east-1 will break IAM, CloudFront, and Route 53 if you do not include them in a
NotActionlist. Test SCPs in a non-production account before applying to production. - Writing overly broad cfn-guard rules. A rule that applies to every resource type when it only needs to apply to S3 buckets will generate false positives and frustrate developers. Always scope rules to the specific resource type being validated.
- Running OPA against the Terraform source, not the plan. The Terraform plan contains resolved values after variable substitution and data source lookups. Running OPA against source files means checking unresolved templates. Always run against
terraform show -json tfplan. - Treating AWS Config as a prevention tool. AWS Config detects non-compliance after a resource is created or modified. By the time it fires an alert, the misconfigured resource already exists. Use it for detection and alerting, not as your first line of defense.
- Not testing policies themselves. Policy files are code and can have bugs. OPA has a built-in test framework (
opa test), and cfn-guard also supports unit testing. Write tests for your policies to verify they catch violations and do not produce false positives.
Summary
- Policy as code replaces manual security review with automated checks that run on every infrastructure change, consistently and without human error.
- Service Control Policies (SCPs) set hard account-level limits that cannot be bypassed by any IAM principal, including administrators. Use them for things that must never happen: wrong-region deployments, unrestricted IAM policies, or disabling CloudTrail.
- CloudFormation Guard validates CloudFormation templates and Terraform plan JSON against rules you define. It runs in CI and fails the build before infrastructure is created.
- Open Policy Agent with Conftest evaluates Terraform plans against Rego policies. Conftest exits non-zero on violations, blocking the pipeline and reporting the specific resource and rule that failed.
- AWS Config continuously evaluates deployed resources after they exist. It catches drift from manual changes and pipeline bugs that slipped through pre-deployment checks.
- Use all four in combination: SCPs for hard limits, cfn-guard and OPA for pre-deployment validation, and AWS Config for continuous post-deployment monitoring.
Frequently asked questions
What is the difference between policy as code and manual security review?
Manual review depends on a human reading every infrastructure change before it deploys. Policy as code runs the same checks automatically on every commit and blocks non-compliant changes without human involvement. Manual review scales poorly and misses things; automated policy checks run consistently every time.
When should I use an SCP versus CloudFormation Guard?
SCPs are account-level guardrails that prevent AWS API calls entirely. They work at runtime and cannot be bypassed by any user in the account, including the root user. CloudFormation Guard validates infrastructure templates before deployment. Use SCPs for hard limits that must never be broken; use cfn-guard to catch misconfigurations in templates before they are applied.
Does OPA replace AWS Config?
No. OPA with Conftest validates Terraform plans before deployment, catching problems before infrastructure is created. AWS Config evaluates deployed resources after they exist, catching drift and misconfigurations that slipped through or were made outside of the pipeline. They are complementary.
Can I use all four tools together?
Yes, and most mature AWS environments do. SCPs set hard account-level limits. CloudFormation Guard and OPA catch misconfigurations before deployment. AWS Config monitors what is already running. Each layer catches a different class of problem, so using all four gives you defense in depth.
Does cfn-guard work with Terraform, or only CloudFormation?
cfn-guard works with any JSON or YAML document, including Terraform plan output. Convert the Terraform plan to JSON using terraform show -json, then run cfn-guard against that file. This lets you use cfn-guard rules even if your team uses Terraform instead of CloudFormation.