AWS CloudFormation Security
Overview
Create secure AWS infrastructure using CloudFormation templates with security best practices. This skill covers encryption with AWS KMS, secrets management with Secrets Manager, secure parameters, IAM least privilege, security groups, TLS/SSL certificates, and defense-in-depth strategies.
When to Use
Use this skill when:
-
Creating CloudFormation templates with encryption at-rest and in-transit
-
Managing secrets and credentials with AWS Secrets Manager
-
Configuring AWS KMS for encryption keys
-
Implementing secure parameters with SSM Parameter Store
-
Creating IAM policies with least privilege
-
Configuring security groups and network security
-
Implementing secure cross-stack references
-
Configuring TLS/SSL for AWS services
-
Applying defense-in-depth for infrastructure
Instructions
Follow these steps to create secure CloudFormation infrastructure:
-
Define Encryption Keys: Create KMS keys for data encryption
-
Set Up Secrets: Use Secrets Manager for credentials and API keys
-
Configure Secure Parameters: Use SSM Parameter Store with encryption
-
Implement IAM Policies: Apply least privilege principles
-
Create Security Groups: Configure network access controls
-
Set Up TLS Certificates: Use ACM for SSL/TLS certificates
-
Enable Encryption: Configure encryption for storage and transit
-
Implement Monitoring: Enable CloudTrail and security logging
For complete examples, see the EXAMPLES.md file.
Examples
The following examples demonstrate common security patterns:
Example 1: KMS Key for Encryption
KmsKey: Type: AWS::KMS::Key Properties: KeyPolicy: Version: "2012-10-17" Statement: - Effect: Allow Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: kms:* Resource: "" - Effect: Allow Principal: Service: s3.amazonaws.com Action: - kms:Encrypt - kms:Decrypt Resource: ""
KmsAlias: Type: AWS::KMS::Alias Properties: AliasName: !Sub "alias/${AWS::StackName}-key" TargetKeyId: !Ref KmsKey
Example 2: Secrets Manager Secret
DatabaseSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub "${AWS::StackName}/database-credentials" Description: Database credentials for application SecretString: !Sub | { "username": "${DBUsername}", "password": "${DBPassword}", "host": "${DBInstance.Endpoint.Address}", "port": "${DBInstance.Endpoint.Port}" }
Example 3: Secure Parameter
SecureParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub "/${AWS::StackName}/api-key" Type: SecureString Value: !Ref ApiKeyValue Description: Secure API key for external service
For complete production-ready examples, see EXAMPLES.md.
CloudFormation Template Structure
Base Template with Security Section
AWSTemplateFormatVersion: 2010-09-09 Description: Secure infrastructure template with encryption and secrets management
Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Encryption Settings Parameters: - EncryptionKeyArn - SecretsKmsKeyId - Label: default: Security Configuration Parameters: - SecurityLevel - EnableVPCPeering
Parameters: Environment: Type: String Default: dev AllowedValues: - dev - staging - production
EncryptionKeyArn: Type: AWS::KMS::Key::Arn Description: KMS key ARN for encryption
SecretsKmsKeyId: Type: String Description: KMS key ID for secrets encryption
Mappings: SecurityConfig: dev: EnableDetailedMonitoring: false RequireMultiAZ: false staging: EnableDetailedMonitoring: true RequireMultiAZ: false production: EnableDetailedMonitoring: true RequireMultiAZ: true
Conditions: IsProduction: !Equals [!Ref Environment, production] EnableEnhancedMonitoring: !Equals [!Ref Environment, production]
Resources:
Resources will be defined here
Outputs: SecurityConfigurationOutput: Description: Security configuration applied Value: !Ref Environment
AWS KMS - Encryption
Complete KMS Key with Full Policy
Resources:
Master KMS Key for application
ApplicationKmsKey: Type: AWS::KMS::Key Properties: Description: "KMS Key for application encryption" KeyPolicy: Version: "2012-10-17" Id: "application-key-policy" Statement: # Allow key management to administrators - Sid: "EnableIAMPolicies" Effect: Allow Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:role/AdminRole" Action: - kms:Create* - kms:Describe* - kms:Enable* - kms:List* - kms:Put* - kms:Update* - kms:Revoke* - kms:Disable* - kms:Get* - kms:Delete* - kms:TagResource - kms:UntagResource Resource: "*" Condition: StringEquals: aws:PrincipalOrgID: !Ref OrganizationId
# Allow encryption/decryption for application roles
- Sid: "AllowCryptographicOperations"
Effect: Allow
Principal:
AWS:
- !Sub "arn:aws:iam::${AWS::AccountId}:role/LambdaExecutionRole"
- !Sub "arn:aws:iam::${AWS::AccountId}:role/ECSTaskRole"
Action:
- kms:Encrypt
- kms:Decrypt
- kms:GenerateDataKey*
- kms:ReEncrypt*
Resource: "*"
# Allow key usage for specific services
- Sid: "AllowKeyUsageForSpecificServices"
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- ecs.amazonaws.com
- rds.amazonaws.com
Action:
- kms:Encrypt
- kms:Decrypt
- kms:GenerateDataKey*
Resource: "*"
KeyUsage: ENCRYPT_DECRYPT
EnableKeyRotation: true
PendingWindowInDays: 30
Alias for the key
ApplicationKmsKeyAlias: Type: AWS::KMS::Alias Properties: AliasName: !Sub "alias/application-${Environment}" TargetKeyId: !Ref ApplicationKmsKey
KMS Key for S3 bucket encryption
S3KmsKey: Type: AWS::KMS::Key Properties: Description: "KMS Key for S3 bucket encryption" KeyPolicy: Version: "2012-10-17" Statement: - Sid: "AllowS3Encryption" Effect: Allow Principal: Service: s3.amazonaws.com Action: - kms:Encrypt - kms:Decrypt - kms:GenerateDataKey* Resource: "*" Condition: StringEquals: aws:SourceAccount: !Ref AWS::AccountId
KMS Key for RDS encryption
RdsKmsKey: Type: AWS::KMS::Key Properties: Description: "KMS Key for RDS database encryption" KeyPolicy: Version: "2012-10-17" Statement: - Sid: "AllowRDSEncryption" Effect: Allow Principal: Service: rds.amazonaws.com Action: - kms:Encrypt - kms:Decrypt - kms:GenerateDataKey* Resource: "*"
S3 Bucket with KMS Encryption
Resources: EncryptedS3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "secure-bucket-${AWS::AccountId}-${AWS::Region}" PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !Ref S3KmsKey
BucketKeyEnabled: true
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: ArchiveOldVersions
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 90
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Encrypted
Value: "true"
AWS Secrets Manager
Secrets Manager with Automatic Rotation
Resources:
Database credentials secret
DatabaseSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub "${AWS::StackName}/database/credentials" Description: "Database credentials with automatic rotation" SecretString: !Sub | { "username": "${DBUsername}", "password": "${DBPassword}", "host": "${DBHost}", "port": "${DBPort}", "dbname": "${DBName}", "engine": "postgresql" } KmsKeyId: !Ref SecretsKmsKeyId
# Enable automatic rotation
RotationRules:
AutomaticallyAfterDays: 30
# Rotation Lambda configuration
RotationLambdaARN: !GetAtt SecretRotationFunction.Arn
Tags:
- Key: Environment
Value: !Ref Environment
- Key: ManagedBy
Value: CloudFormation
- Key: RotationEnabled
Value: "true"
Secret with resource-based policy
ApiSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub "${AWS::StackName}/api/keys" Description: "API keys for external service authentication" SecretString: !Sub | { "api_key": "${ExternalApiKey}", "api_secret": "${ExternalApiSecret}", "endpoint": "https://api.example.com" } KmsKeyId: !Ref SecretsKmsKeyId
# Resource-based policy for access control
ResourcePolicy:
Version: "2012-10-17"
Statement:
- Sid: "AllowLambdaAccess"
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:role/LambdaExecutionRole"
Action:
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
Resource: "*"
Condition:
StringEquals:
aws:ResourceTag/Environment: !Ref Environment
- Sid: "DenyUnencryptedAccess"
Effect: Deny
Principal: "*"
Action:
- secretsmanager:GetSecretValue
Resource: "*"
Condition:
StringEquals:
kms:ViaService: !Sub "secretsmanager.${AWS::Region}.amazonaws.com"
StringNotEquals:
kms:EncryptContext: !Sub "secretsmanager:${AWS::StackName}"
Secret with cross-account access
SharedSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub "${AWS::StackName}/shared/credentials" Description: "Secret shared across accounts" SecretString: !Sub | { "shared_key": "${SharedKey}", "shared_value": "${SharedValue}" } KmsKeyId: !Ref SecretsKmsKeyId
# Cross-account access policy
ResourcePolicy:
Version: "2012-10-17"
Statement:
- Sid: "AllowCrossAccountRead"
Effect: Allow
Principal:
AWS:
- !Sub "arn:aws:iam::${ProductionAccountId}:role/SharedSecretReader"
Action:
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
Resource: "*"
SSM Parameter Store with SecureString
Parameters:
SSM Parameter for database connection
DBCredentialsParam: Type: AWS::SSM::Parameter::Value<SecureString> NoEcho: true Description: Database credentials from SSM Parameter Store Value: !Sub "/${Environment}/database/credentials"
SSM Parameter with specific path
ApiKeyParam: Type: AWS::SSM::Parameter::Value<SecureString> NoEcho: true Description: API key for external service Value: !Sub "/${Environment}/external-api/key"
Resources:
Lambda function using SSM parameters
SecureLambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub "${AWS::StackName}-secure-function" Runtime: python3.11 Handler: handler.handler Code: S3Bucket: !Ref CodeBucket S3Key: lambda/secure-function.zip Role: !GetAtt LambdaExecutionRole.Arn Environment: Variables: DB_CREDENTIALS_SSM_PATH: !Sub "/${Environment}/database/credentials" API_KEY_SSM_PATH: !Sub "/${Environment}/external-api/key"
IAM Security - Least Privilege
IAM Role with Granular Policies
Resources:
Lambda Execution Role with minimal permissions
LambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-lambda-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Ref AWS::AccountId lambda:SourceFunctionArn: !Ref SecureLambdaFunctionArn
# Permissions boundary for enhanced security
PermissionsBoundary: !Ref PermissionsBoundaryPolicy
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
# Policy for specific secrets access
- PolicyName: SecretsAccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
Resource: !Ref DatabaseSecretArn
Condition:
StringEquals:
secretsmanager:SecretTarget: !Sub "${DatabaseSecretArn}:${DatabaseSecret}"
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref ApiSecretArn
# Policy for specific S3 access
- PolicyName: S3AccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- !Sub "${DataBucket.Arn}/*"
- !Sub "${DataBucket.Arn}"
Condition:
StringEquals:
s3:ResourceAccount: !Ref AWS::AccountId
- Effect: Deny
Action:
- s3:DeleteObject*
Resource:
- !Sub "${DataBucket.Arn}/*"
Condition:
Bool:
aws:MultiFactorAuthPresent: true
# Policy for CloudWatch Logs
- PolicyName: CloudWatchLogsPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub "${LogGroup.Arn}:*"
Tags:
- Key: Environment
Value: !Ref Environment
- Key: LeastPrivilege
Value: "true"
Permissions Boundary Policy
PermissionsBoundaryPolicy: Type: AWS::IAM::ManagedPolicy Properties: Description: "Permissions boundary for Lambda execution role" PolicyDocument: Version: "2012-10-17" Statement: - Sid: "DenyAccessToAllExceptSpecified" Effect: Deny Action: - "" Resource: "" Condition: StringNotEqualsIfExists: aws:RequestedRegion: - !Ref AWS::Region ArnNotEqualsIfExists: aws:SourceArn: !Ref AllowedResourceArns
IAM Policy for Cross-Account Access
Resources:
Role for cross-account access
CrossAccountRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-cross-account-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: AWS: - !Sub "arn:aws:iam::${ProductionAccountId}:root" - !Sub "arn:aws:iam::${StagingAccountId}:role/CrossAccountAccessRole" Action: sts:AssumeRole Condition: StringEquals: aws:PrincipalAccount: !Ref ProductionAccountId Bool: aws:MultiFactorAuthPresent: true
Policies:
- PolicyName: CrossAccountReadOnlyPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject*
- s3:List*
Resource:
- !Sub "${SharedBucket.Arn}"
- !Sub "${SharedBucket.Arn}/*"
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
Resource:
- !Sub "${SharedTable.Arn}"
- !Sub "${SharedTable.Arn}/index/*"
- Effect: Deny
Action:
- s3:DeleteObject*
- s3:PutObject*
Resource:
- !Sub "${SharedBucket.Arn}/*"
VPC Security
Security Groups with Restrictive Rules
Resources:
Security Group for application
ApplicationSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${AWS::StackName}-app-sg" GroupDescription: "Security group for application tier" VpcId: !Ref VPCId Tags: - Key: Name Value: !Sub "${AWS::StackName}-app-sg" - Key: Environment Value: !Ref Environment
# Inbound rules - only necessary traffic
SecurityGroupIngress:
# HTTP from ALB
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
Description: "HTTP from ALB"
# HTTPS from ALB
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref ALBSecurityGroup
Description: "HTTPS from ALB"
# SSH from bastion only (if needed)
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: !Ref BastionSecurityGroup
Description: "SSH access from bastion"
# Custom TCP for internal services
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref InternalSecurityGroup
Description: "Internal service communication"
# Outbound rules - limited
SecurityGroupEgress:
# HTTPS outbound for API calls
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: "HTTPS outbound"
# DNS outbound
- IpProtocol: udp
FromPort: 53
ToPort: 53
CidrIp: 10.0.0.0/16
Description: "DNS outbound for VPC"
Security Group for database
DatabaseSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${AWS::StackName}-db-sg" GroupDescription: "Security group for database tier" VpcId: !Ref VPCId Tags: - Key: Name Value: !Sub "${AWS::StackName}-db-sg"
# Inbound - only from application security group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref ApplicationSecurityGroup
Description: "PostgreSQL from application tier"
# Outbound - minimum required
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: "HTTPS for updates and patches"
Security Group for ALB
ALBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${AWS::StackName}-alb-sg" GroupDescription: "Security group for ALB" VpcId: !Ref VPCId
SecurityGroupIngress:
# HTTP from internet
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: "HTTP from internet"
# HTTPS from internet
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: "HTTPS from internet"
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ApplicationSecurityGroup
Description: "Forward to application"
VPC Endpoint for Secrets Manager
SecretsManagerVPCEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcId: !Ref VPCId ServiceName: !Sub "com.amazonaws.${AWS::Region}.secretsmanager" VpcEndpointType: Interface Subnets: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 SecurityGroups: - !Ref ApplicationSecurityGroup PrivateDnsEnabled: true
TLS/SSL Certificates with ACM
Certificate Manager for API Gateway
Resources:
SSL Certificate for domain
SSLCertificate: Type: AWS::CertificateManager::Certificate Properties: DomainName: !Ref DomainName SubjectAlternativeNames: - !Sub "*.${DomainName}" - !Ref AdditionalDomainName
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: !Ref DomainName
Route53HostedZoneId: !Ref HostedZoneId
Options:
CertificateTransparencyLoggingPreference: ENABLED
Tags:
- Key: Environment
Value: !Ref Environment
- Key: ManagedBy
Value: CloudFormation
Certificate for regional API Gateway
RegionalCertificate: Type: AWS::CertificateManager::Certificate Properties: DomainName: !Sub "${Environment}.${DomainName}" ValidationMethod: DNS DomainValidationOptions: - DomainName: !Sub "${Environment}.${DomainName}" Route53HostedZoneId: !Ref HostedZoneId
API Gateway with TLS 1.2+
SecureApiGateway: Type: AWS::ApiGateway::RestApi Properties: Name: !Sub "${AWS::StackName}-secure-api" Description: "Secure REST API with TLS enforcement" EndpointConfiguration: Types: - REGIONAL MinimumCompressionSize: 1024
# Policy to enforce HTTPS
Policy:
Version: "2012-10-17"
Statement:
- Effect: Deny
Principal: "*"
Action: execute-api:Invoke
Resource: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${SecureApiGateway}/*"
Condition:
Bool:
aws:SecureTransport: "false"
Custom Domain per API Gateway
ApiGatewayDomain: Type: AWS::ApiGateway::DomainName Properties: DomainName: !Sub "api.${DomainName}" RegionalCertificateArn: !Ref RegionalCertificate EndpointConfiguration: Types: - REGIONAL
Route 53 record per dominio API
ApiGatewayDNSRecord: Type: AWS::Route53::RecordSet Properties: Name: !Sub "api.${DomainName}." Type: A AliasTarget: DNSName: !GetAtt ApiGatewayRegionalHostname.RegionalHostname HostedZoneId: !GetAtt ApiGatewayRegionalHostname.RegionalHostedZoneId EvaluateTargetHealth: false HostedZoneId: !Ref HostedZoneId
Lambda Function URL con AuthType AWS_IAM
SecureLambdaUrl: Type: AWS::Lambda::Url Properties: AuthType: AWS_IAM TargetFunctionArn: !GetAtt SecureLambdaFunction.Arn Cors: AllowCredentials: true AllowHeaders: - Authorization - Content-Type AllowMethods: - GET - POST AllowOrigins: - !Ref AllowedOrigin MaxAge: 86400 InvokeMode: BUFFERED
Parameter Security Best Practices
AWS-Specific Parameter Types with Validation
Parameters:
AWS-specific types for automatic validation
VPCId: Type: AWS::EC2::VPC::Id Description: VPC ID for deployment
SubnetIds: Type: List<AWS::EC2::Subnet::Id> Description: Subnet IDs for private subnets
SecurityGroupIds: Type: List<AWS::EC2::SecurityGroup::Id> Description: Security group IDs
DatabaseInstanceIdentifier: Type: AWS::RDS::DBInstance::Identifier Description: RDS instance identifier
KMSKeyArn: Type: AWS::KMS::Key::Arn Description: KMS key ARN for encryption
SecretArn: Type: AWS::SecretsManager::Secret::Arn Description: Secrets Manager secret ARN
LambdaFunctionArn: Type: AWS::Lambda::Function::Arn Description: Lambda function ARN
SSM Parameter with secure string
DatabasePassword: Type: AWS::SSM::Parameter::Value<SecureString> NoEcho: true Description: Database password from SSM
Custom parameters with constraints
DBUsername: Type: String Description: Database username Default: appuser MinLength: 1 MaxLength: 63 AllowedPattern: "[a-zA-Z][a-zA-Z0-9_]*" ConstraintDescription: Must start with letter, alphanumeric and underscores only
DBPort: Type: Number Description: Database port Default: 5432 MinValue: 1024 MaxValue: 65535
MaxConnections: Type: Number Description: Maximum database connections Default: 100 MinValue: 10 MaxValue: 65535
EnvironmentName: Type: String Description: Deployment environment Default: dev AllowedValues: - dev - staging - production ConstraintDescription: Must be dev, staging, or production
Outputs and Secure Cross-Stack References
Export with Naming Convention
Outputs:
Export for cross-stack references
VPCIdExport: Description: VPC ID for network stack Value: !Ref VPC Export: Name: !Sub "${AWS::StackName}-VPCId"
ApplicationSecurityGroupIdExport: Description: Application security group ID Value: !Ref ApplicationSecurityGroup Export: Name: !Sub "${AWS::StackName}-AppSecurityGroupId"
DatabaseSecurityGroupIdExport: Description: Database security group ID Value: !Ref DatabaseSecurityGroup Export: Name: !Sub "${AWS::StackName}-DBSecurityGroupId"
KMSKeyArnExport: Description: KMS key ARN for encryption Value: !GetAtt ApplicationKmsKey.Arn Export: Name: !Sub "${AWS::StackName}-KMSKeyArn"
DatabaseSecretArnExport: Description: Database secret ARN Value: !Ref DatabaseSecret Export: Name: !Sub "${AWS::StackName}-DatabaseSecretArn"
SSLCertificateArnExport: Description: SSL certificate ARN Value: !Ref SSLCertificate Export: Name: !Sub "${AWS::StackName}-SSLCertificateArn"
Import from Network Stack
Parameters: NetworkStackName: Type: String Description: Name of the network stack
Resources:
Import values from network stack
VPCId: Type: AWS::EC2::VPC Properties: CidrBlock: !Select [0, !Split [",", !ImportValue !Sub "${NetworkStackName}-VPCcidrs"]]
ApplicationSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${AWS::StackName}-app-sg" VpcId: !ImportValue !Sub "${NetworkStackName}-VPCId" SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !ImportValue !Sub "${NetworkStackName}-ALBSecurityGroupId"
CloudWatch Logs Encryption
Log Group with KMS Encryption
Resources:
Encrypted CloudWatch Log Group
EncryptedLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${AWS::StackName}-function" RetentionInDays: 30 KmsKeyId: !Ref ApplicationKmsKey
# Data protection policy
LogGroupClass: STANDARD
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Encrypted
Value: "true"
Metric Filter for security events
SecurityEventMetricFilter: Type: AWS::Logs::MetricFilter Properties: LogGroupName: !Ref EncryptedLogGroup FilterPattern: '[ERROR, WARNING, "Access Denied", "Unauthorized"]' MetricTransformations: - MetricValue: "1" MetricNamespace: !Sub "${AWS::StackName}/Security" MetricName: SecurityEvents
Alarm for security errors
SecurityAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub "${AWS::StackName}-security-errors" AlarmDescription: Alert on security-related errors MetricName: SecurityEvents Namespace: !Sub "${AWS::StackName}/Security" Statistic: Sum Period: 60 EvaluationPeriods: 5 Threshold: 1 ComparisonOperator: GreaterThanThreshold AlarmActions: - !Ref SecurityAlertTopic
SNS Topic for security alerts
SecurityAlertTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub "${AWS::StackName}-security-alerts"
Defense in Depth
Stack with Multiple Security Layers
AWSTemplateFormatVersion: 2010-09-09 Description: Defense in depth security architecture
Resources:
Layer 1: Network Security - Security Groups
WebTierSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "Web tier security group" VpcId: !Ref VPCId SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 Description: "HTTPS from internet"
AppTierSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "App tier security group" VpcId: !Ref VPCId SecurityGroupIngress: - IpProtocol: tcp FromPort: 8080 ToPort: 8080 SourceSecurityGroupId: !Ref WebTierSecurityGroup
Layer 2: Encryption - KMS
DataEncryptionKey: Type: AWS::KMS::Key Properties: Description: "Data encryption key" KeyPolicy: Version: "2012-10-17" Statement: - Sid: "EnableIAMPoliciesForKeyManagement" Effect: Allow Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:role/AdminRole" Action: kms:* Resource: "" - Sid: "AllowEncryptionOperations" Effect: Allow Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:role/AppRole" Action: - kms:Encrypt - kms:Decrypt Resource: ""
Layer 3: Secrets Management
ApplicationSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub "${AWS::StackName}/application/credentials" SecretString: "{}" KmsKeyId: !Ref DataEncryptionKey
Layer 4: IAM Least Privilege
ApplicationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${AWS::StackName}-app-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: ecs.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: MinimalSecretsAccess PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: !Ref ApplicationSecret
Layer 5: Logging and Monitoring
AuditLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/${AWS::StackName}/audit" RetentionInDays: 365 KmsKeyId: !Ref DataEncryptionKey
Layer 6: WAF for API protection
WebACL: Type: AWS::WAFv2::WebACL Properties: Name: !Sub "${AWS::StackName}-waf" Scope: REGIONAL DefaultAction: Allow: CustomRequestHandling: InsertHeaders: - Name: X-Frame-Options Value: DENY Rules: - Name: BlockSQLInjection Priority: 1 Statement: SqliMatchStatement: FieldToMatch: Body: OversizeHandling: CONTINUE SensitivityLevel: HIGH Action: Block: CustomResponse: ResponseCode: 403 ResponseBody: "Request blocked due to SQL injection" VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: BlockSQLInjection
- Name: BlockXSS
Priority: 2
Statement:
XssMatchStatement:
FieldToMatch:
QueryString:
OversizeHandling: CONTINUE
Action:
Block:
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: BlockXSS
- Name: RateLimit
Priority: 3
Statement:
RateBasedStatement:
Limit: 2000
EvaluationWindowSec: 60
Action:
Block:
CustomResponse:
ResponseCode: 429
ResponseBody: "Too many requests"
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: RateLimit
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub "${AWS::StackName}-WebACL"
SampledRequestsEnabled: true
Best Practices
Encryption
-
Always use KMS with customer-managed keys for sensitive data
-
Enable automatic key rotation (max 365 days)
-
Use S3 bucket keys to reduce KMS costs
-
Encrypt CloudWatch Logs with KMS
-
Implement envelope encryption for large data
Secrets Management
-
Use Secrets Manager for automatic rotation
-
Reference secrets via ARN, not hard-coded
-
Use resource-based policies for granular access
-
Implement encryption context for auditing
-
Limit access with IAM conditions
IAM Security
-
Apply least privilege in all policies
-
Use permissions boundaries to limit escalation
-
Enable MFA for administrative roles
-
Implement condition keys for region/endpoint
-
Regular audit with IAM Access Analyzer
Network Security
-
Security groups with minimal rules
-
Deny default outbound where possible
-
Use VPC endpoints for AWS services
-
Implement private subnets for backend tiers
-
Use Network ACLs as additional layer
TLS/SSL
-
Use ACM for managed certificates
-
Enforce HTTPS with resource policies
-
Configure minimum TLS 1.2
-
Use HSTS headers
-
Renew certificates before expiration
Monitoring
-
Enable CloudTrail for audit trail
-
Create metrics for security events
-
Configure alarms for suspicious activity
-
Appropriate log retention (min 90 days)
-
Use GuardDuty for threat detection
Related Resources
-
AWS KMS Documentation
-
AWS Secrets Manager
-
IAM Best Practices
-
Security Groups
-
AWS WAF
Additional Files
For complete details on resources and their properties, see:
-
REFERENCE.md - Detailed reference guide for all CloudFormation security resources
-
EXAMPLES.md - Complete production-ready examples for security scenarios
CloudFormation Stack Management Best Practices
Stack Policies
Stack Policies prevent accidental updates to critical infrastructure resources. Use them to protect production resources from unintended modifications.
Resources: ProductionStackPolicy: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/production-stack.yaml" StackPolicyBody: Version: "2012-10-17" Statement: - Effect: Allow Action: Update:* Principal: "" Resource: "" - Effect: Deny Action: - Update:Replace - Update:Delete Principal: "*" Resource: - LogicalResourceId/ProductionDatabase - LogicalResourceId/ProductionKmsKey Condition: StringEquals: aws:RequestedRegion: - us-east-1 - us-west-2
Inline stack policy for sensitive resources
SensitiveResourcesPolicy: Type: AWS::CloudFormation::StackPolicy Properties: PolicyDocument: Version: "2012-10-17" Statement: - Effect: Deny Action: Update:* Principal: "" Resource: "" Condition: StringEquals: aws:ResourceTag/Environment: production Not: StringEquals: aws:username: security-admin
Termination Protection
Enable termination protection to prevent accidental deletion of production stacks. This adds a safety layer for critical infrastructure.
Resources:
Production stack with termination protection
ProductionDatabaseStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/database.yaml" TerminationProtection: true Parameters: Environment: production InstanceClass: db.r6g.xlarge MultiAZ: true
Stack with conditional termination protection
ApplicationStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/application.yaml" TerminationProtection: !If [IsProduction, true, false] Parameters: Environment: !Ref Environment
Drift Detection
Detect configuration drift in your CloudFormation stacks to identify unauthorized or unexpected changes.
Resources:
Custom resource for drift detection
DriftDetectionFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub "${AWS::StackName}-drift-detector" Runtime: python3.11 Handler: drift_detector.handler Role: !GetAtt DriftDetectionRole.Arn Code: S3Bucket: !Ref CodeBucket S3Key: lambda/drift-detector.zip Environment: Variables: STACK_NAME: !Ref StackName SNS_TOPIC_ARN: !Ref DriftAlertTopic Timeout: 300
Scheduled drift detection
DriftDetectionSchedule: Type: AWS::Events::Rule Properties: Name: !Sub "${AWS::StackName}-drift-schedule" ScheduleExpression: rate(1 day) State: ENABLED Targets: - Arn: !GetAtt DriftDetectionFunction.Arn Id: DriftDetectionFunction
Permission for EventBridge to invoke Lambda
DriftDetectionPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref DriftDetectionFunction Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt DriftDetectionSchedule.Arn
SNS topic for drift alerts
DriftAlertTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub "${AWS::StackName}-drift-alerts"
Drift Detection Python Handler
import boto3 import json
def handler(event, context): cloudformation = boto3.client('cloudformation') sns = boto3.client('sns')
stack_name = event.get('STACK_NAME', 'my-production-stack')
topic_arn = event.get('SNS_TOPIC_ARN')
# Detect drift
response = cloudformation.detect_stack_drift(StackName=stack_name)
# Wait for drift detection to complete
import time
time.sleep(60)
# Get drift status
drift_status = cloudformation.describe_stack-drift-detection-status(
StackName=stack_name,
DetectionId=response['StackDriftDetectionId']
)
# Get resources with drift
resources = []
paginator = cloudformation.get_paginator('list_stack_resources')
for page in paginator.paginate(StackName=stack_name):
for resource in page['StackResourceSummaries']:
if resource['DriftStatus'] != 'IN_SYNC':
resources.append({
'LogicalId': resource['LogicalResourceId'],
'PhysicalId': resource['PhysicalResourceId'],
'DriftStatus': resource['DriftStatus'],
'Expected': resource.get('ExpectedResourceType'),
'Actual': resource.get('ActualResourceType')
})
# Send alert if drift detected
if resources:
message = f"Drift detected on stack {stack_name}:\n"
for r in resources:
message += f"- {r['LogicalId']}: {r['DriftStatus']}\n"
sns.publish(
TopicArn=topic_arn,
Subject=f"CloudFormation Drift Alert: {stack_name}",
Message=message
)
return {
'statusCode': 200,
'body': json.dumps({
'drift_status': drift_status['StackDriftStatus'],
'resources_with_drift': len(resources)
})
}
Change Sets Usage
Use Change Sets to preview and review changes before applying them to production stacks.
Resources:
Change set for stack update
ChangeSet: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/updated-template.yaml" ChangeSetName: !Sub "${AWS::StackName}-update-changeset" ChangeSetType: UPDATE Parameters: Environment: !Ref Environment InstanceType: !Ref NewInstanceType Capabilities: - CAPABILITY_IAM - CAPABILITY_NAMED_IAM
Nested change set for review
ReviewChangeSet: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/review-template.yaml" ChangeSetName: !Sub "${AWS::StackName}-review-changeset" ChangeSetType: UPDATE Parameters: Environment: !Ref Environment Tags: - Key: ChangeSetType Value: review - Key: CreatedBy Value: CloudFormation
Change Set Generation Script
#!/bin/bash
Create a change set for review
aws cloudformation create-change-set
--stack-name my-production-stack
--change-set-name production-update-changeset
--template-url https://my-bucket.s3.amazonaws.com/updated-template.yaml
--parameters ParameterKey=Environment,ParameterValue=production
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
Wait for change set creation
aws cloudformation wait change-set-create-complete
--stack-name my-production-stack
--change-set-name production-update-changeset
Describe change set to see what will change
aws cloudformation describe-change-set
--stack-name my-production-stack
--change-set-name production-update-changeset
Execute change set if changes look good
aws cloudformation execute-change-set
--stack-name my-production-stack
--change-set-name production-update-changeset
Stack Update with Rollback Triggers
Resources:
Production stack with rollback configuration
ProductionStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://${BucketName}.s3.amazonaws.com/production.yaml" TimeoutInMinutes: 60 RollbackConfiguration: RollbackTriggers: - Arn: !Sub "arn:aws:cloudwatch:${AWS::Region}:${AWS::AccountId}:alarm:ProductionCPUHigh" Type: AWS::CloudWatch::Alarm - Arn: !Sub "arn:aws:cloudwatch:${AWS::Region}:${AWS::AccountId}:alarm:ProductionLatencyHigh" Type: AWS::CloudWatch::Alarm MonitoringTimeInMinutes: 15 NotificationARNs: - !Ref UpdateNotificationTopic
CloudWatch alarms for rollback
ProductionCPUHigh: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub "${AWS::StackName}-CPU-High" AlarmDescription: Trigger rollback if CPU exceeds 80% MetricName: CPUUtilization Namespace: AWS/EC2 Statistic: Average Period: 60 EvaluationPeriods: 5 Threshold: 80 ComparisonOperator: GreaterThanThreshold AlarmActions: - !Ref UpdateNotificationTopic
SNS topic for notifications
UpdateNotificationTopic: Type: AWS::SNS::Topic Properties: TopicName: !Sub "${AWS::StackName}-update-notifications"
Best Practices for Stack Management
Enable Termination Protection
-
Always enable for production stacks
-
Use as a safety mechanism against accidental deletion
-
Requires manual disabling before deletion
Use Stack Policies
-
Protect critical resources from unintended updates
-
Use Deny statements for production databases, KMS keys, and IAM roles
-
Apply conditions based on region, user, or tags
Implement Drift Detection
-
Run drift detection regularly (daily for production)
-
Alert on any drift detection
-
Investigate and remediate drift immediately
Use Change Sets
-
Always use Change Sets for production updates
-
Review changes before execution
-
Use descriptive change set names
Configure Rollback Triggers
-
Set up CloudWatch alarms for critical metrics
-
Configure monitoring time to allow stabilization
-
Test rollback triggers in non-production first
Implement Change Management
-
Require approval for production changes
-
Use CodePipeline with manual approval gates
-
Document all changes in change log
Use Stack Sets for Multi-Account
-
Deploy infrastructure consistently across accounts
-
Use StackSets for organization-wide policies
-
Implement drift detection at organization level
Constraints and Warnings
Resource Limits
-
Security Group Rules: Maximum 60 inbound and 60 outbound rules per security group
-
Security Groups: Maximum 500 security groups per VPC
-
NACL Rules: Maximum 20 inbound and 20 outbound rules per NACL per subnet
-
VPC Limits: Maximum 5 VPCs per region (soft limit, can be increased)
Security Constraints
-
Default Security Groups: Default security groups cannot be deleted
-
Security Group References: Security group references cannot span VPC peering in some cases
-
NACL Stateless: NACLs are stateless; return traffic must be explicitly allowed
-
Flow Logs: VPC Flow Logs generate significant CloudWatch Logs costs
Operational Constraints
-
CIDR Overlap: VPC CIDR blocks cannot overlap with peered VPCs or on-prem networks
-
ENI Limits: Each instance type has maximum ENI limits; affects scaling
-
Elastic IP Limits: Each account has limited number of Elastic IPs
-
NAT Gateway Limits: Each AZ can have only one NAT gateway per subnet
Network Constraints
-
Transit Gateway: Transit Gateway attachments have per-AZ and per-account limits
-
VPN Connections: VPN connections have bandwidth limitations
-
Direct Connect: Direct Connect requires physical infrastructure and lead time
-
PrivateLink: VPC Endpoint services have availability constraints
Cost Considerations
-
NAT Gateway: NAT gateways incur hourly costs plus data processing costs
-
Traffic Mirroring: Traffic mirroring doubles data transfer costs
-
Flow Logs: Flow logs storage and analysis add significant costs
-
PrivateLink: Interface VPC endpoints incur hourly and data processing costs
Access Control Constraints
-
IAM vs Resource Policies: Some services require both IAM and resource-based policies
-
SCP Limits: Service Control Policies have character limits and complexity constraints
-
Permission Boundaries: Permission boundaries do not limit service actions
-
Session Policies: Session policies cannot grant more permissions than IAM policies
Additional Files