Advanced 45 min read Policy as Code Updated: 2025-07-25

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

  • Strong experience with Infrastructure as Code (CloudFormation or Terraform).
  • Proficiency with YAML syntax and structure.
  • Solid understanding of CI/CD concepts and tooling (e.g., GitHub Actions, Jenkins).
  • Familiarity with the command line and installing open-source tools.

๐Ÿ’ก From Detective to Preventive: The "Shift-Left" Revolution

Traditional compliance tools often act as detective controls, finding issues after they've been deployed. CloudFormation Guard enables preventive controls by validating your IaC templates before deployment. This "shift-left" approach embeds policy enforcement directly into your development workflow, catching violations early and preventing non-compliant infrastructure from ever being created.

๐Ÿท๏ธ Topics Covered

CloudFormation GuardPolicy as CodeShift-Left SecurityCI/CDInfrastructure as CodeComplianceStatic AnalysisDevSecOps

The Power of Preventive Controls

Understanding the difference between preventive and detective controls is key to building a mature cloud governance strategy. While both are essential, preventive controls offer significant advantages in speed and security.

Detective Controls (e.g., AWS Config)

  • Scans deployed resources.
  • Finds non-compliance post-deployment.
  • Requires remediation actions to fix issues.
  • The security gap exists until remediation occurs.
  • Answers: "Is my deployed environment compliant?"

Preventive Controls (e.g., CFN Guard)

  • Scans Infrastructure as Code templates.
  • Blocks non-compliance pre-deployment.
  • Prevents issues from ever being created.
  • No security gap; the build/deployment fails.
  • Answers: "Is my planned environment compliant?"

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.

Example 1: Comprehensive S3 Bucket Security

This rule ensures that all S3 buckets follow security best practices: encryption, blocked public access, and versioning must all be enabled.

๐Ÿ›ก๏ธ Guard: S3 Security Best Practices Rule

#
# 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.
    }
}

Example 2: Enforce Strict Security Group Ingress Rules

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.

๐ŸŒ Guard: Secure Security Group Ingress Rule

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`).

Example: Testing the Security Group Rule

This test file validates our `SECURE_INGRESS_RULES` rule with both a compliant and a non-compliant Security Group template.

๐Ÿงช YAML: CFN Guard Test Definition

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
  
  - name: Test_Non_Compliant_SG_Unrestricted_SSH
    input:
      Resources:
        AnotherBadSG:
          Type: AWS::EC2::SecurityGroup
          Properties:
            GroupDescription: "Non-compliant unrestricted SSH"
            SecurityGroupIngress:
              - IpProtocol: tcp
                FromPort: 22
                ToPort: 22
                CidrIp: 172.16.0.0/16 # VIOLATION: SSH from wrong CIDR
    expectations:
      rules:
        SECURE_INGRESS_RULES: FAIL

To run these tests, you simply execute: `cfn-guard test --test-data my_test_file.yaml`

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
        # This step will fail the pipeline if any rule check fails.
        # -d points to the data templates, -r to the rule files.
        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.

๐Ÿ”‘ CFN Guard Advanced Best Practices

  • Integrate as Early as Possible: Add CFN Guard to pre-commit hooks and Pull Request checks to give developers the fastest feedback.
  • Write Granular Rules: Avoid creating large, monolithic rules. Break down complex policies into smaller, single-purpose rules for better clarity and easier testing.
  • 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.
  • Start with High-Impact Rules: Begin by implementing rules that address your most critical security risks, such as public access and encryption.
  • Version Control Everything: Treat your rules as code. They should be versioned, reviewed, and managed in a central repository just like your application code.