← Back to Blog

Securing Infrastructure as Code: CloudFormation Guardrails and Drift Detection

5 min read

Infrastructure as Code should be your single source of truth for AWS resource configuration. But IaC only provides security guarantees if the deployment pipeline itself is secure. Unrestricted CloudFormation permissions, missing stack policies, and undetected drift can undermine even the most carefully written templates.

Stack Policies

Stack policies prevent accidental modification or deletion of critical resources during updates. Without a stack policy, any aws cloudformation update-stack call can replace your production database.

Protecting Stateful Resources

{
  "Statement": [
    {
      "Effect": "Deny",
      "Action": ["Update:Replace", "Update:Delete"],
      "Principal": "*",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "ResourceType": [
            "AWS::RDS::DBInstance",
            "AWS::RDS::DBCluster",
            "AWS::DynamoDB::Table",
            "AWS::S3::Bucket",
            "AWS::KMS::Key",
            "AWS::Cognito::UserPool"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "Update:*",
      "Principal": "*",
      "Resource": "*"
    }
  ]
}

Apply the policy when creating the stack:

aws cloudformation create-stack \
  --stack-name production-stack \
  --template-body file://template.yaml \
  --stack-policy-body file://stack-policy.json \
  --role-arn arn:aws:iam::123456789012:role/CloudFormationDeployRole

If you need to update a protected resource, temporarily override the policy for that specific update — the override expires automatically after the update completes.

CloudFormation Service Roles

By default, CloudFormation uses the credentials of the user who runs the deployment. This means developers need permissions to create every resource type in the template. A CloudFormation service role breaks this coupling.

import boto3
import json

def create_cloudformation_deploy_role(stack_prefix, allowed_services):
    """Create a least-privilege CloudFormation service role"""

    iam = boto3.client('iam')
    account_id = boto3.client('sts').get_caller_identity()['Account']

    # Trust policy allowing only CloudFormation to assume this role
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "cloudformation.amazonaws.com"},
                "Action": "sts:AssumeRole",
                "Condition": {
                    "StringEquals": {
                        "aws:SourceAccount": account_id
                    }
                }
            }
        ]
    }

    role = iam.create_role(
        RoleName=f"cfn-deploy-{stack_prefix}",
        AssumeRolePolicyDocument=json.dumps(trust_policy),
        Description=f"CloudFormation service role for {stack_prefix} stacks"
    )

    # Build permissions from allowed services
    actions = []
    for service in allowed_services:
        actions.append(f"{service}:*")

    # Always need these for CloudFormation operations
    actions.extend([
        "cloudformation:CreateChangeSet",
        "cloudformation:DescribeChangeSet",
        "cloudformation:ExecuteChangeSet",
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeStackEvents"
    ])

    permissions_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": actions,
                "Resource": "*",
                "Condition": {
                    "StringLike": {
                        "aws:ResourceTag/aws:cloudformation:stack-name": f"{stack_prefix}-*"
                    }
                }
            }
        ]
    }

    iam.put_role_policy(
        RoleName=role['Role']['RoleName'],
        PolicyName='deploy-permissions',
        PolicyDocument=json.dumps(permissions_policy)
    )

    return role['Role']['Arn']

# Example: role for a serverless API stack
api_role = create_cloudformation_deploy_role('api', [
    'lambda', 'apigateway', 'dynamodb', 'sqs', 'logs', 'iam'
])

The developer only needs cloudformation:* and iam:PassRole on the service role — they never need direct permissions to create Lambda functions or DynamoDB tables.

Policy-as-Code with CloudFormation Guard

CloudFormation Guard (cfn-guard) validates templates against rules before deployment. Integrate it into CI/CD to catch security violations early.

Security Rules

# security-rules.guard

# All S3 buckets must have encryption enabled
AWS::S3::Bucket {
  Properties.BucketEncryption.ServerSideEncryptionConfiguration[*] {
    ServerSideEncryptionByDefault.SSEAlgorithm in ["aws:kms", "AES256"]
  }
}

# All S3 buckets must block public access
AWS::S3::Bucket {
  Properties.PublicAccessBlockConfiguration exists
  Properties.PublicAccessBlockConfiguration {
    BlockPublicAcls == true
    BlockPublicPolicy == true
    IgnorePublicAcls == true
    RestrictPublicBuckets == true
  }
}

# All RDS instances must have encryption at rest
AWS::RDS::DBInstance {
  Properties.StorageEncrypted == true
}

# All RDS instances must not be publicly accessible
AWS::RDS::DBInstance {
  Properties.PubliclyAccessible == false
}

# Lambda functions must not use admin policies
AWS::IAM::Role when Properties.AssumeRolePolicyDocument.Statement[*].Principal.Service[*] == "lambda.amazonaws.com" {
  Properties.ManagedPolicyArns[*] != "arn:aws:iam::aws:policy/AdministratorAccess"
}

# All DynamoDB tables must have encryption
AWS::DynamoDB::Table {
  Properties.SSESpecification exists
  Properties.SSESpecification.SSEEnabled == true
}

Run in CI/CD:

# Validate a template against security rules
cfn-guard validate \
  --data template.yaml \
  --rules security-rules.guard \
  --show-summary fail

# Exit code 0 = all rules pass, non-zero = violations found

Drift Detection

Drift occurs when someone modifies a CloudFormation-managed resource directly through the console or CLI. This creates a gap between the template (your intended state) and the actual state.

# Initiate drift detection
DETECTION_ID=$(aws cloudformation detect-stack-drift \
  --stack-name production-stack \
  --query 'StackDriftDetectionId' \
  --output text)

# Wait for detection to complete
aws cloudformation describe-stack-drift-detection-status \
  --stack-drift-detection-id $DETECTION_ID

# Get drifted resources
aws cloudformation describe-stack-resource-drifts \
  --stack-name production-stack \
  --stack-resource-drift-status-filters MODIFIED DELETED \
  --query 'StackResourceDrifts[].{Resource: LogicalResourceId, Type: ResourceType, Status: StackResourceDriftStatus}'

Schedule drift detection daily for production stacks. Drift in security groups, IAM roles, or bucket policies is especially dangerous because it may indicate unauthorized access or misconfiguration that your templates did not intend.

Securing StackSets for Multi-Account Deployment

StackSets deploy CloudFormation templates across multiple accounts and regions. Use service-managed permissions with Organizations:

aws cloudformation create-stack-set \
  --stack-set-name security-baseline \
  --template-body file://security-baseline.yaml \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
  --capabilities CAPABILITY_NAMED_IAM

With SERVICE_MANAGED, StackSets automatically creates execution roles in target accounts. Enable auto-deployment so new accounts added to your Organization automatically receive the security baseline.

Securing IaC Deployments with AccessLens

CloudFormation service roles, StackSet execution roles, and CI/CD pipeline roles all need IAM permissions to create and modify AWS resources. Overpermissive deployment roles are a common escalation path — a compromised CI/CD pipeline with AdministratorAccess can take over the entire account.

AccessLens helps secure your IaC pipeline by providing:

  • Deployment role analysis that identifies overpermissive CloudFormation and CI/CD roles
  • Cross-account StackSet visibility that shows which accounts and roles have deployment access
  • Permission usage tracking that reveals which deployment permissions are actually used vs. granted
  • Drift correlation that connects resource drift to IAM principals who made out-of-band changes

Secure infrastructure starts with secure deployment. AccessLens ensures your IaC pipeline roles follow least privilege.

Secure your IaC pipeline with AccessLens and prevent deployment roles from becoming your biggest attack surface.

Ready to secure your AWS environment?

Get comprehensive IAM visibility across all your AWS accounts in minutes.