AWS Well-Architected Security Pillar Best Practices: Complete Guide 2024
Master the AWS Well-Architected Security Pillar with this comprehensive guide covering IAM, detection, infrastructure protection, data encryption, and incident response with real code examples.
The AWS Well-Architected Framework provides a consistent approach for evaluating cloud architectures and implementing designs that scale securely over time. Among the six pillars of the framework, the Security Pillar stands as the foundation upon which all other pillars depend. Without robust security measures, operational excellence, reliability, performance efficiency, cost optimization, and sustainability efforts can be undermined by a single breach.
This comprehensive guide dives deep into the AWS Well-Architected Security Pillar, providing actionable best practices, real-world implementation examples, and production-ready code samples using AWS CLI, CloudFormation, Python boto3, and Terraform. Whether you're a cloud architect, security engineer, or DevOps professional, this guide will help you build and maintain a secure AWS environment.
Understanding the AWS Security Pillar Foundation
The Security Pillar encompasses the ability to protect data, systems, and assets while taking advantage of cloud technologies to improve your overall security posture. AWS defines security through five interconnected areas that work together to create defense in depth:
The Five Security Focus Areas
- Identity and Access Management (IAM): Controlling who can do what in your AWS environment
- Detection Capabilities: Identifying security events and potential threats
- Infrastructure Protection: Securing network and compute resources
- Data Protection: Safeguarding data at rest and in transit
- Incident Response: Preparing for and responding to security events
Each area requires specific controls, monitoring, and automation to achieve a comprehensive security posture. Let's explore each area with detailed implementation guidance and code examples.
Identity and Access Management Deep Dive
Identity and Access Management forms the cornerstone of AWS security. Every API call, every resource access, and every action in AWS is governed by IAM. Implementing IAM correctly is not just about security—it's about enabling your organization to operate efficiently while maintaining the principle of least privilege.
Implementing the Principle of Least Privilege
The principle of least privilege states that every identity should have only the minimum permissions required to perform its intended function. This reduces the blast radius of compromised credentials and simplifies security auditing.
Creating a Least-Privilege IAM Policy
Here's a production-ready example of a least-privilege policy for an application that needs to read from S3 and write to DynamoDB:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadS3ApplicationBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-application-data-bucket",
"arn:aws:s3:::my-application-data-bucket/*"
],
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
},
{
"Sid": "WriteDynamoDBTable",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/ApplicationTable",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"${aws:PrincipalTag/tenant-id}"
]
}
}
}
]
}
This policy demonstrates several best practices:
- Specific resource ARNs instead of wildcards
- Regional conditions to prevent cross-region access
- Attribute-based access control (ABAC) using principal tags for multi-tenant isolation
Enforcing MFA for Sensitive Operations
Multi-Factor Authentication (MFA) is critical for protecting privileged operations. Here's how to create an IAM policy that requires MFA for sensitive actions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowViewAccountInfo",
"Effect": "Allow",
"Action": [
"iam:GetAccountPasswordPolicy",
"iam:ListVirtualMFADevices"
],
"Resource": "*"
},
{
"Sid": "AllowManageOwnMFA",
"Effect": "Allow",
"Action": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:ResyncMFADevice"
],
"Resource": [
"arn:aws:iam::*:mfa/${aws:username}",
"arn:aws:iam::*:user/${aws:username}"
]
},
{
"Sid": "DenyAllExceptListedIfNoMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Setting Up IAM Roles for EC2 Instances
Never embed long-term credentials in EC2 instances. Instead, use IAM instance profiles. Here's a CloudFormation template:
AWSTemplateFormatVersion: '2010-09-09'
Description: Secure EC2 Instance with IAM Role
Resources:
ApplicationRole:
Type: AWS::IAM::Role
Properties:
RoleName: SecureApplicationRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: ApplicationPermissions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub 'arn:aws:s3:::${ApplicationBucket}/*'
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /
Roles:
- !Ref ApplicationRole
ApplicationBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
SecureInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref LatestAmiId
InstanceType: t3.medium
IamInstanceProfile: !Ref InstanceProfile
MetadataOptions:
HttpTokens: required
HttpPutResponseHopLimit: 1
HttpEndpoint: enabled
Tags:
- Key: Name
Value: SecureApplicationInstance
Parameters:
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
This template enforces IMDSv2 (Instance Metadata Service Version 2) which protects against SSRF attacks that could steal instance credentials.
Detection Capabilities: Building Your Security Monitoring Stack
Detection is about identifying when something goes wrong before it becomes a major incident. AWS provides several services that work together to create a comprehensive detection and monitoring capability.
Enabling AWS CloudTrail Organization-Wide
CloudTrail is the foundation of AWS security monitoring. Every API call made in your account is logged, providing an audit trail for security investigations and compliance requirements.
Here's a Terraform configuration to set up organization-wide CloudTrail with encryption:
# Terraform configuration for Organization CloudTrail
resource "aws_cloudtrail" "organization_trail" {
name = "organization-security-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id
s3_key_prefix = "cloudtrail"
include_global_service_events = true
is_multi_region_trail = true
is_organization_trail = true
enable_log_file_validation = true
kms_key_id = aws_kms_key.cloudtrail_key.arn
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3"]
}
data_resource {
type = "AWS::Lambda::Function"
values = ["arn:aws:lambda"]
}
}
insight_selector {
insight_type = "ApiCallRateInsight"
}
insight_selector {
insight_type = "ApiErrorRateInsight"
}
tags = {
Environment = "production"
Purpose = "security-audit"
}
}
resource "aws_s3_bucket" "cloudtrail_logs" {
bucket = "organization-cloudtrail-logs-${data.aws_caller_identity.current.account_id}"
}
resource "aws_s3_bucket_versioning" "cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail_lifecycle" {
bucket = aws_s3_bucket.cloudtrail_logs.id
rule {
id = "archive-old-logs"
status = "Enabled"
transition {
days = 90
storage_class = "STANDARD_IA"
}
transition {
days = 365
storage_class = "GLACIER"
}
expiration {
days = 2555 # 7 years for compliance
}
}
}
resource "aws_kms_key" "cloudtrail_key" {
description = "KMS key for CloudTrail encryption"
deletion_window_in_days = 30
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM User Permissions"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow CloudTrail to encrypt logs"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = [
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
}
data "aws_caller_identity" "current" {}
Deploying Amazon GuardDuty for Threat Detection
GuardDuty uses machine learning to analyze VPC Flow Logs, CloudTrail events, and DNS logs to detect threats. Here's how to enable it across your organization using Python boto3:
import boto3
from botocore.exceptions import ClientError
def enable_guardduty_organization():
"""
Enable GuardDuty across an AWS Organization with all protection plans.
Run this from the delegated administrator account.
"""
guardduty = boto3.client('guardduty')
organizations = boto3.client('organizations')
# Create or get the detector
try:
response = guardduty.create_detector(
Enable=True,
FindingPublishingFrequency='FIFTEEN_MINUTES',
DataSources={
'S3Logs': {'Enable': True},
'Kubernetes': {
'AuditLogs': {'Enable': True}
},
'MalwareProtection': {
'ScanEc2InstanceWithFindings': {
'EbsVolumes': True
}
}
},
Features=[
{
'Name': 'S3_DATA_EVENTS',
'Status': 'ENABLED'
},
{
'Name': 'EKS_AUDIT_LOGS',
'Status': 'ENABLED'
},
{
'Name': 'EBS_MALWARE_PROTECTION',
'Status': 'ENABLED'
},
{
'Name': 'RDS_LOGIN_EVENTS',
'Status': 'ENABLED'
},
{
'Name': 'LAMBDA_NETWORK_LOGS',
'Status': 'ENABLED'
},
{
'Name': 'RUNTIME_MONITORING',
'Status': 'ENABLED',
'AdditionalConfiguration': [
{
'Name': 'EKS_ADDON_MANAGEMENT',
'Status': 'ENABLED'
}
]
}
],
Tags={
'Environment': 'production',
'ManagedBy': 'security-team'
}
)
detector_id = response['DetectorId']
print(f"Created GuardDuty detector: {detector_id}")
except ClientError as e:
if e.response['Error']['Code'] == 'BadRequestException':
# Detector already exists, get it
detectors = guardduty.list_detectors()
detector_id = detectors['DetectorIds'][0]
print(f"Using existing detector: {detector_id}")
else:
raise
# Enable organization configuration
guardduty.update_organization_configuration(
DetectorId=detector_id,
AutoEnable=True,
DataSources={
'S3Logs': {'AutoEnable': True},
'Kubernetes': {
'AuditLogs': {'AutoEnable': True}
},
'MalwareProtection': {
'ScanEc2InstanceWithFindings': {
'EbsVolumes': {'AutoEnable': True}
}
}
},
AutoEnableOrganizationMembers='ALL'
)
print("GuardDuty organization configuration updated successfully")
# List all accounts in the organization and add as members
paginator = organizations.get_paginator('list_accounts')
for page in paginator.paginate():
for account in page['Accounts']:
if account['Status'] == 'ACTIVE':
try:
guardduty.create_members(
DetectorId=detector_id,
AccountDetails=[{
'AccountId': account['Id'],
'Email': account['Email']
}]
)
print(f"Added member account: {account['Id']}")
except ClientError as e:
print(f"Could not add account {account['Id']}: {e}")
return detector_id
if __name__ == "__main__":
enable_guardduty_organization()
Configuring AWS Config for Compliance Monitoring
AWS Config continuously monitors and records your AWS resource configurations, enabling you to automate compliance assessments:
# CloudFormation template for AWS Config setup
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Config Configuration with Security Rules
Resources:
ConfigRecorder:
Type: AWS::Config::ConfigurationRecorder
Properties:
Name: SecurityConfigRecorder
RoleARN: !GetAtt ConfigRole.Arn
RecordingGroup:
AllSupported: true
IncludeGlobalResourceTypes: true
ConfigDeliveryChannel:
Type: AWS::Config::DeliveryChannel
Properties:
S3BucketName: !Ref ConfigBucket
SnsTopicARN: !Ref ConfigNotificationTopic
ConfigSnapshotDeliveryProperties:
DeliveryFrequency: Six_Hours
# Security-focused Config Rules
RootAccountMFAEnabled:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: root-account-mfa-enabled
Description: Checks whether MFA is enabled for the root account
Source:
Owner: AWS
SourceIdentifier: ROOT_ACCOUNT_MFA_ENABLED
MaximumExecutionFrequency: TwentyFour_Hours
IAMPasswordPolicy:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: iam-password-policy
Description: Checks IAM password policy requirements
Source:
Owner: AWS
SourceIdentifier: IAM_PASSWORD_POLICY
InputParameters:
RequireUppercaseCharacters: 'true'
RequireLowercaseCharacters: 'true'
RequireSymbols: 'true'
RequireNumbers: 'true'
MinimumPasswordLength: '14'
PasswordReusePrevention: '24'
MaxPasswordAge: '90'
S3BucketPublicReadProhibited:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: s3-bucket-public-read-prohibited
Description: Checks that S3 buckets do not allow public read access
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_PUBLIC_READ_PROHIBITED
S3BucketSSLRequestsOnly:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: s3-bucket-ssl-requests-only
Description: Checks that S3 buckets require SSL for requests
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_SSL_REQUESTS_ONLY
EncryptedVolumes:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: encrypted-volumes
Description: Checks that EBS volumes are encrypted
Source:
Owner: AWS
SourceIdentifier: ENCRYPTED_VOLUMES
RDSEncryptionEnabled:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: rds-storage-encrypted
Description: Checks that RDS instances have encryption enabled
Source:
Owner: AWS
SourceIdentifier: RDS_STORAGE_ENCRYPTED
VPCFlowLogsEnabled:
Type: AWS::Config::ConfigRule
DependsOn: ConfigRecorder
Properties:
ConfigRuleName: vpc-flow-logs-enabled
Description: Checks that VPC Flow Logs are enabled
Source:
Owner: AWS
SourceIdentifier: VPC_FLOW_LOGS_ENABLED
InputParameters:
trafficType: ALL
ConfigRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: config.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWS_ConfigRole
Policies:
- PolicyName: ConfigS3DeliveryPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:PutObjectAcl
Resource: !Sub '${ConfigBucket.Arn}/*'
- Effect: Allow
Action:
- s3:GetBucketAcl
Resource: !GetAtt ConfigBucket.Arn
ConfigBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
ConfigNotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: config-notifications
KmsMasterKeyId: alias/aws/sns
Outputs:
ConfigBucketName:
Description: S3 bucket for AWS Config
Value: !Ref ConfigBucket
ConfigTopicArn:
Description: SNS topic for Config notifications
Value: !Ref ConfigNotificationTopic
Aggregating Findings with AWS Security Hub
Security Hub provides a comprehensive view of your security posture by aggregating findings from GuardDuty, Inspector, Macie, and third-party tools. Enable it with this AWS CLI command:
#!/bin/bash
# Enable Security Hub with all security standards
REGION="us-east-1"
# Enable Security Hub
aws securityhub enable-security-hub \
--enable-default-standards \
--control-finding-generator SECURITY_CONTROL \
--region $REGION
# Enable additional security standards
aws securityhub batch-enable-standards \
--standards-subscription-requests '[
{"StandardsArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0"},
{"StandardsArn": "arn:aws:securityhub:us-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"},
{"StandardsArn": "arn:aws:securityhub:us-east-1::standards/pci-dss/v/3.2.1"}
]' \
--region $REGION
# Enable integrations
aws securityhub enable-import-findings-for-product \
--product-arn "arn:aws:securityhub:$REGION::product/aws/guardduty" \
--region $REGION
aws securityhub enable-import-findings-for-product \
--product-arn "arn:aws:securityhub:$REGION::product/aws/inspector" \
--region $REGION
echo "Security Hub enabled with CIS, AWS Best Practices, and PCI DSS standards"
Infrastructure Protection: Securing Your Network Layer
Infrastructure protection encompasses the controls and practices that protect your network and compute resources from unauthorized access and attacks.
Designing a Secure VPC Architecture
A well-designed VPC provides network isolation and traffic control. Here's a comprehensive Terraform configuration for a production VPC:
# Secure VPC with public and private subnets, NAT Gateway, and VPC Flow Logs
variable "environment" {
default = "production"
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
# Public subnets for load balancers
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = false
tags = {
Name = "${var.environment}-public-${count.index + 1}"
Type = "public"
}
}
# Private subnets for application tier
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-private-${count.index + 1}"
Type = "private"
}
}
# Isolated subnets for databases
resource "aws_subnet" "database" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 20)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-database-${count.index + 1}"
Type = "database"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
}
}
# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.environment}-nat-eip"
}
}
# NAT Gateway
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.environment}-nat"
}
}
# VPC Flow Logs
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
log_destination_type = "cloud-watch-logs"
log_destination = aws_cloudwatch_log_group.flow_logs.arn
iam_role_arn = aws_iam_role.flow_logs.arn
max_aggregation_interval = 60
tags = {
Name = "${var.environment}-flow-logs"
}
}
resource "aws_cloudwatch_log_group" "flow_logs" {
name = "/aws/vpc/${var.environment}-flow-logs"
retention_in_days = 365
kms_key_id = aws_kms_key.logs.arn
}
# Network ACLs for database subnets
resource "aws_network_acl" "database" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.database[*].id
# Allow inbound from private subnets only
ingress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "10.0.10.0/24"
from_port = 5432
to_port = 5432
}
ingress {
protocol = "tcp"
rule_no = 101
action = "allow"
cidr_block = "10.0.11.0/24"
from_port = 5432
to_port = 5432
}
ingress {
protocol = "tcp"
rule_no = 102
action = "allow"
cidr_block = "10.0.12.0/24"
from_port = 5432
to_port = 5432
}
# Allow ephemeral ports for responses
ingress {
protocol = "tcp"
rule_no = 200
action = "allow"
cidr_block = var.vpc_cidr
from_port = 1024
to_port = 65535
}
egress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = var.vpc_cidr
from_port = 1024
to_port = 65535
}
tags = {
Name = "${var.environment}-database-nacl"
}
}
data "aws_availability_zones" "available" {
state = "available"
}
Security Groups Best Practices
Security groups act as virtual firewalls for your instances. Here's an example of a properly configured security group for a web application:
# CloudFormation Security Groups
Resources:
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Application Load Balancer
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS from internet
Tags:
- Key: Name
Value: alb-security-group
ApplicationSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for application instances
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref ALBSecurityGroup
Description: Traffic from ALB only
Tags:
- Key: Name
Value: application-security-group
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for RDS database
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref ApplicationSecurityGroup
Description: PostgreSQL from application tier only
Tags:
- Key: Name
Value: database-security-group
Data Protection: Encryption at Rest and in Transit
Data protection ensures that your sensitive information remains confidential and maintains integrity both when stored and when transmitted.
Implementing KMS for Centralized Key Management
AWS Key Management Service (KMS) provides centralized control over encryption keys. Here's a Python boto3 script to create and manage KMS keys:
import boto3
import json
def create_secure_kms_key(alias_name: str, description: str,
admin_role_arn: str, user_role_arns: list):
"""
Create a KMS key with proper key policy for encryption operations.
"""
kms = boto3.client('kms')
sts = boto3.client('sts')
account_id = sts.get_caller_identity()['Account']
# Define key policy with separation of admin and usage
key_policy = {
"Version": "2012-10-17",
"Id": "key-policy",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": f"arn:aws:iam::{account_id}:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow Key Administrators",
"Effect": "Allow",
"Principal": {
"AWS": admin_role_arn
},
"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",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "Allow Key Usage",
"Effect": "Allow",
"Principal": {
"AWS": user_role_arns
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "Allow AWS Services",
"Effect": "Allow",
"Principal": {
"Service": [
"s3.amazonaws.com",
"rds.amazonaws.com",
"ebs.amazonaws.com",
"secretsmanager.amazonaws.com"
]
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:CallerAccount": account_id,
"kms:ViaService": [
f"s3.{boto3.session.Session().region_name}.amazonaws.com",
f"rds.{boto3.session.Session().region_name}.amazonaws.com"
]
}
}
}
]
}
# Create the key
response = kms.create_key(
Description=description,
KeyUsage='ENCRYPT_DECRYPT',
KeySpec='SYMMETRIC_DEFAULT',
Policy=json.dumps(key_policy),
Tags=[
{'TagKey': 'Environment', 'TagValue': 'production'},
{'TagKey': 'Purpose', 'TagValue': 'data-encryption'},
{'TagKey': 'ManagedBy', 'TagValue': 'security-team'}
],
MultiRegion=False
)
key_id = response['KeyMetadata']['KeyId']
# Enable automatic key rotation
kms.enable_key_rotation(KeyId=key_id)
# Create alias
kms.create_alias(
AliasName=f"alias/{alias_name}",
TargetKeyId=key_id
)
print(f"Created KMS key: {key_id}")
print(f"Alias: alias/{alias_name}")
print("Key rotation: Enabled (annual)")
return key_id
if __name__ == "__main__":
key_id = create_secure_kms_key(
alias_name="application-data-key",
description="KMS key for encrypting application data",
admin_role_arn="arn:aws:iam::123456789012:role/SecurityAdmin",
user_role_arns=[
"arn:aws:iam::123456789012:role/ApplicationRole",
"arn:aws:iam::123456789012:role/BackupRole"
]
)
Enforcing Encryption for S3 Buckets
Use bucket policies to enforce encryption and secure access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnencryptedUploads",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
},
{
"Sid": "DenyNonKMSEncryption",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-secure-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
}
}
},
{
"Sid": "DenyNonHTTPS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Sid": "EnforceTLSVersion",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-secure-bucket",
"arn:aws:s3:::my-secure-bucket/*"
],
"Condition": {
"NumericLessThan": {
"s3:TlsVersion": "1.2"
}
}
}
]
}
Incident Response: Automated Security Remediation
Incident response is about being prepared for security events and having the ability to respond quickly and effectively. Automation is key to reducing response time.
Building an Automated Incident Response System
Here's a Lambda function that automatically responds to security findings:
import json
import boto3
from datetime import datetime
ec2 = boto3.client('ec2')
sns = boto3.client('sns')
ssm = boto3.client('ssm')
SECURITY_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:security-alerts'
QUARANTINE_SG_ID = 'sg-quarantine123'
def lambda_handler(event, context):
"""
Automated incident response for GuardDuty and Security Hub findings.
This function isolates compromised instances and notifies the security team.
"""
# Parse the finding
if 'detail' in event:
finding = event['detail']
else:
finding = event
finding_type = finding.get('type', finding.get('Types', ['Unknown'])[0])
severity = finding.get('severity', finding.get('Severity', {}).get('Normalized', 0))
response_actions = []
# High severity instance compromise
if severity >= 7 and 'Instance' in str(finding):
instance_id = extract_instance_id(finding)
if instance_id:
# Isolate the instance
isolate_result = isolate_instance(instance_id)
response_actions.append(isolate_result)
# Capture forensic snapshot
snapshot_result = capture_forensic_snapshot(instance_id)
response_actions.append(snapshot_result)
# Compromised credentials
if 'UnauthorizedAccess' in finding_type or 'CredentialAccess' in finding_type:
access_key_id = extract_access_key(finding)
if access_key_id:
# Disable the access key
disable_result = disable_access_key(access_key_id)
response_actions.append(disable_result)
# S3 bucket exposure
if 'S3' in finding_type and 'Public' in finding_type:
bucket_name = extract_bucket_name(finding)
if bucket_name:
# Block public access
block_result = block_public_access(bucket_name)
response_actions.append(block_result)
# Send notification
notification = {
'finding_type': finding_type,
'severity': severity,
'timestamp': datetime.utcnow().isoformat(),
'actions_taken': response_actions,
'finding_id': finding.get('Id', 'Unknown'),
'account_id': finding.get('AwsAccountId', 'Unknown'),
'region': finding.get('Region', 'Unknown')
}
sns.publish(
TopicArn=SECURITY_TOPIC_ARN,
Subject=f"Security Incident Response: {finding_type}",
Message=json.dumps(notification, indent=2)
)
return {
'statusCode': 200,
'body': json.dumps(notification)
}
def isolate_instance(instance_id: str) -> dict:
"""Isolate instance by moving to quarantine security group."""
try:
# Get current security groups
instance = ec2.describe_instances(InstanceIds=[instance_id])
current_sgs = [sg['GroupId'] for sg in
instance['Reservations'][0]['Instances'][0]['SecurityGroups']]
# Store original SGs as tags
ec2.create_tags(
Resources=[instance_id],
Tags=[
{'Key': 'OriginalSecurityGroups', 'Value': ','.join(current_sgs)},
{'Key': 'IsolationTimestamp', 'Value': datetime.utcnow().isoformat()},
{'Key': 'SecurityIncident', 'Value': 'Isolated'}
]
)
# Apply quarantine security group
ec2.modify_instance_attribute(
InstanceId=instance_id,
Groups=[QUARANTINE_SG_ID]
)
return {
'action': 'isolate_instance',
'instance_id': instance_id,
'status': 'success',
'original_security_groups': current_sgs
}
except Exception as e:
return {
'action': 'isolate_instance',
'instance_id': instance_id,
'status': 'failed',
'error': str(e)
}
def capture_forensic_snapshot(instance_id: str) -> dict:
"""Create snapshots of all volumes for forensic analysis."""
try:
instance = ec2.describe_instances(InstanceIds=[instance_id])
volumes = instance['Reservations'][0]['Instances'][0].get('BlockDeviceMappings', [])
snapshot_ids = []
for volume in volumes:
volume_id = volume['Ebs']['VolumeId']
snapshot = ec2.create_snapshot(
VolumeId=volume_id,
Description=f"Forensic snapshot for incident on {instance_id}",
TagSpecifications=[{
'ResourceType': 'snapshot',
'Tags': [
{'Key': 'Purpose', 'Value': 'ForensicAnalysis'},
{'Key': 'SourceInstance', 'Value': instance_id},
{'Key': 'CreatedAt', 'Value': datetime.utcnow().isoformat()}
]
}]
)
snapshot_ids.append(snapshot['SnapshotId'])
return {
'action': 'forensic_snapshot',
'instance_id': instance_id,
'status': 'success',
'snapshot_ids': snapshot_ids
}
except Exception as e:
return {
'action': 'forensic_snapshot',
'instance_id': instance_id,
'status': 'failed',
'error': str(e)
}
def disable_access_key(access_key_id: str) -> dict:
"""Disable a compromised access key."""
iam = boto3.client('iam')
try:
# Find the user associated with the key
paginator = iam.get_paginator('list_users')
for page in paginator.paginate():
for user in page['Users']:
keys = iam.list_access_keys(UserName=user['UserName'])
for key in keys['AccessKeyMetadata']:
if key['AccessKeyId'] == access_key_id:
iam.update_access_key(
UserName=user['UserName'],
AccessKeyId=access_key_id,
Status='Inactive'
)
return {
'action': 'disable_access_key',
'access_key_id': access_key_id,
'user_name': user['UserName'],
'status': 'success'
}
return {
'action': 'disable_access_key',
'access_key_id': access_key_id,
'status': 'not_found'
}
except Exception as e:
return {
'action': 'disable_access_key',
'access_key_id': access_key_id,
'status': 'failed',
'error': str(e)
}
def block_public_access(bucket_name: str) -> dict:
"""Block public access to an S3 bucket."""
s3 = boto3.client('s3')
try:
s3.put_public_access_block(
Bucket=bucket_name,
PublicAccessBlockConfiguration={
'BlockPublicAcls': True,
'IgnorePublicAcls': True,
'BlockPublicPolicy': True,
'RestrictPublicBuckets': True
}
)
return {
'action': 'block_public_access',
'bucket_name': bucket_name,
'status': 'success'
}
except Exception as e:
return {
'action': 'block_public_access',
'bucket_name': bucket_name,
'status': 'failed',
'error': str(e)
}
def extract_instance_id(finding: dict) -> str:
"""Extract EC2 instance ID from finding."""
resources = finding.get('Resources', finding.get('resources', []))
for resource in resources:
if resource.get('Type') == 'AwsEc2Instance':
details = resource.get('Details', {}).get('AwsEc2Instance', {})
return details.get('InstanceId')
if 'instanceId' in str(resource):
# GuardDuty format
return resource.get('instanceId')
return None
def extract_access_key(finding: dict) -> str:
"""Extract access key ID from finding."""
resources = finding.get('Resources', finding.get('resources', []))
for resource in resources:
if 'accessKeyDetails' in resource:
return resource['accessKeyDetails'].get('accessKeyId')
return None
def extract_bucket_name(finding: dict) -> str:
"""Extract S3 bucket name from finding."""
resources = finding.get('Resources', finding.get('resources', []))
for resource in resources:
if resource.get('Type') == 'AwsS3Bucket':
return resource.get('Details', {}).get('AwsS3Bucket', {}).get('Name')
return None
Working with Warqline
We are a cloud engineering consultancy and an official AWS and Google Cloud partner. If you are running this in production and want a second pair of eyes, we scope work in a free 45-minute technical call: you describe what you are running and what worries you, and we tell you what we would look at first.
Conclusion: Building a Culture of Security
Implementing the AWS Well-Architected Security Pillar is not a one-time project—it's an ongoing commitment to protecting your organization's assets and data. The practices, configurations, and code examples in this guide provide a solid foundation for building secure AWS environments.
Key Takeaways
- Identity is the new perimeter: Implement strong IAM controls with least privilege, MFA, and role-based access
- Detection enables response: Deploy comprehensive monitoring with CloudTrail, GuardDuty, Config, and Security Hub
- Defense in depth: Layer security controls at the network, compute, and application levels
- Encrypt everything: Protect data at rest and in transit using KMS and TLS
- Automate incident response: Build runbooks and Lambda functions that respond to threats in real-time
- Continuously assess: Use tools like Warqline to maintain visibility and compliance over time
Security is a shared responsibility between AWS and its customers. While AWS secures the cloud infrastructure, you are responsible for securing your configurations, data, and applications in the cloud. By following the best practices in this guide and leveraging automation, you can significantly reduce your organization's risk exposure and build customer trust.
Start your security journey today by booking a technical review to identify gaps in your current security posture and receive actionable remediation guidance.