AWS CloudFormation Guard Policies Complete Guide
Write and deploy CloudFormation Guard rules for infrastructure validation, security enforcement, and compliance checking in your AWS deployments.
๐ Prerequisites
- Expert knowledge of Infrastructure as Code (CloudFormation or Terraform).
- Advanced proficiency with YAML/JSON syntax and structure.
- Strong experience with CI/CD concepts and tooling (e.g., GitHub Actions, AWS CodePipeline).
- Familiarity with the command line and installing open-source tools.
๐ก From Detective to Preventive: The "Shift-Left" Revolution
While detective controls like AWS Config are essential for monitoring deployed resources, **preventive controls** stop non-compliant infrastructure from ever being created. CloudFormation Guard is a powerful open-source tool that allows you to validate IaC templates against codified policies, embedding security and compliance directly into your development workflow and CI/CD pipeline. This is the essence of "shifting left."
What You'll Learn
๐ท๏ธ Topics Covered
Writing Advanced Guard Rules
CloudFormation Guard uses a declarative, domain-specific language that is easy to read but powerful enough to express complex compliance logic. Let's explore some advanced rule patterns.
๐ก๏ธ Guard: Comprehensive S3 Bucket Security Rule
This rule ensures that all S3 buckets follow security best practices: encryption, blocked public access, and versioning must all be enabled.
#
# Rule: S3_BUCKET_SECURE_CONFIGURATION
#
let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
rule S3_BUCKET_SECURE_CONFIGURATION when %s3_buckets !empty {
%s3_buckets.Properties {
# Check 1: Server-Side Encryption must be enabled
BucketEncryption.ServerSideEncryptionConfiguration[*] {
ServerSideEncryptionByDefault.SSEAlgorithm == "AES256" or
ServerSideEncryptionByDefault.SSEAlgorithm == "aws:kms"
} << Must have AES256 or KMS encryption enabled.
# Check 2: Block Public Access configuration must be set
PublicAccessBlockConfiguration {
BlockPublicAcls == true
BlockPublicPolicy == true
IgnorePublicAcls == true
RestrictPublicBuckets == true
} << Public Access must be blocked on all four settings.
# Check 3: Versioning must be enabled
VersioningConfiguration.Status == "Enabled" << Versioning must be enabled for data protection.
}
} ๐ Guard: Secure Security Group Ingress Rule
A common enterprise requirement is to deny unrestricted ingress (`0.0.0.0/0`) and ensure that SSH (port 22) is only open to a specific bastion host IP. This rule uses variables (`let`) and complex queries.
let security_groups = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
# Define an allowed CIDR for SSH access
let allowed_ssh_cidr = '10.10.0.5/32' # Bastion Host IP
rule SECURE_INGRESS_RULES when %security_groups !empty {
%security_groups.Properties.SecurityGroupIngress[*] {
# Check 1: Deny unrestricted ingress for any protocol
CidrIp != "0.0.0.0/0" << Unrestricted ingress (0.0.0.0/0) is not allowed.
# Check 2: Handle specific rules for port 22 (SSH)
when ToPort == 22 and FromPort == 22 {
# SSH can only be from the allowed bastion host CIDR
CidrIp == %allowed_ssh_cidr << SSH access is only permitted from the bastion host.
}
}
} Test-Driven Development (TDD) for Rules
How do you know your rules work correctly without deploying infrastructure? CFN Guard includes a powerful test framework. You write test cases with sample IaC snippets and expected outcomes (`PASS` or `FAIL`).
๐งช YAML: CFN Guard Test Definition
This test file validates our `SECURE_INGRESS_RULES` rule with both a compliant and a non-compliant Security Group template.
rule-file: ./rules/security_group_rules.guard
tests:
- name: Test_Compliant_SG
input:
Resources:
MyWebAppSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Allow SSH from bastion and HTTPS"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 10.10.0.5/32 # Compliant SSH
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 10.20.0.0/16 # Compliant HTTPS
expectations:
rules:
SECURE_INGRESS_RULES: PASS
- name: Test_Non_Compliant_SG_Unrestricted_Ingress
input:
Resources:
BadSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: "Non-compliant unrestricted ingress"
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0 # VIOLATION: Unrestricted ingress
expectations:
rules:
SECURE_INGRESS_RULES: FAIL
Integrating Guard into a CI/CD Pipeline
The ultimate goal of preventive controls is to fail a build or deployment *before* insecure infrastructure is created. This is accomplished by adding a CFN Guard validation step to your CI/CD pipeline.
๐ YAML: GitHub Actions CI/CD Pipeline with CFN Guard
name: IaC Validation Pipeline
on:
pull_request:
branches: [ main ]
paths:
- 'templates/**'
- 'rules/**'
jobs:
validate-iac:
name: Validate Infrastructure as Code
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install AWS CloudFormation Guard
run: |
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh
echo "/home/runner/.guard/bin" >> $GITHUB_PATH
- name: Run CFN Guard Validation
run: |
cfn-guard validate --data templates/ --rules rules/ --show-summary fail
Managing Rule Sets at Enterprise Scale
For a large organization, managing rules requires a structured approach. A single, monolithic rule file is not scalable or maintainable.
๐ฆ Central Git Repository
Store all Guard rules in a dedicated, version-controlled repository, making it the single source of truth for policies.
๐ Logical Grouping
Organize rules into directories by compliance domain (e.g., `/security`, `/cost`, `/tagging`, `/networking`).
๐ Rule Layering
Apply a base set of mandatory rules to all pipelines, and allow teams to layer on additional, more specific rules for their applications.
๐ท๏ธ Versioning
Use Git tags or branches to version your rule sets. CI/CD pipelines can then pull specific, stable versions of the rules for validation.
Troubleshooting Advanced Guard Rules
As rules become more complex, debugging them requires understanding how Guard queries and evaluates data.
โ Rule Fails to Trigger on Nested Properties
- Symptom: A rule that checks a deeply nested property (e.g., a specific container definition in an ECS Task) doesn't seem to evaluate correctly.
- Cause: The query path to the property is incorrect, or the property may not exist in all resources, causing the rule to fail evaluation.
- Solution: Use the `cfn-guard parse-tree` command on your template (`cfn-guard parse-tree -t my-template.yaml`). This will print the exact, queryable structure of your template as Guard sees it. You can then use this to verify your query paths. Also, use `when` clauses to ensure your rule only applies when the property you are checking actually exists.
โ Ambiguous `FAIL` without a Clear Reason
- Symptom: A rule fails, but the default error message isn't helpful.
- Cause: The rule lacks custom violation messages.
- Solution: Add custom messages using the `<< ... >>` syntax after every clause. You can use variables within the message to provide context. For example: `CidrIp != "0.0.0.0/0" << The CIDR for rule '%{this.Description}' cannot be 0.0.0.0/0`.
๐ Expert-Level CFN Guard Best Practices
- Integrate as Early as Possible: Add CFN Guard to pre-commit hooks and Pull Request checks to give developers the fastest possible feedback.
- Test Your Rules, Not Just Your Templates: Use `cfn-guard test` extensively to validate that your rules behave exactly as you expect before enforcing them.
- Provide Meaningful Error Messages: Use custom messages ('<< ... >>') in your rules to give developers clear, actionable feedback on why their template failed validation.