AWS Cloud Security: Fundamental Best Practices and Configurations
Introduction
Cloud security is a shared responsibility between AWS and customers. While AWS secures the infrastructure, you’re responsible for securing your workloads, data, and access controls. Misconfigurations in IAM policies, S3 buckets, or security groups can expose sensitive data and create attack vectors. This guide covers essential AWS security practices.
Common AWS Security Issues
- Overly permissive IAM policies
- Public S3 buckets with sensitive data
- Open security groups (0.0.0.0/0)
- Unencrypted data at rest and in transit
- No MFA on root account
- Missing CloudTrail logging
- Exposed API keys and credentials
- Unrestricted cross-account access
IAM Security Best Practices
Vulnerable IAM Policy
json// VULNERABLE: Overly permissive policy { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "*", // All actions allowed "Resource": "*" // On all resources } ] }
Secure IAM Policy with Least Privilege
json// SECURE: Least privilege principle { "Version": "2012-10-17", "Statement": [ { "Sid": "ListBuckets", "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": "arn:aws:s3:::my-app-bucket" }, { "Sid": "ReadWriteObjects", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::my-app-bucket/uploads/*", "Condition": { "StringEquals": { "s3:x-amz-server-side-encryption": "AES256" } } } ] }
IAM Role with Conditions
json{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:StartInstances", "ec2:StopInstances" ], "Resource": "arn:aws:ec2:*:*:instance/*", "Condition": { "StringEquals": { "ec2:ResourceTag/Environment": "Development" }, "IpAddress": { "aws:SourceIp": "203.0.113.0/24" // Only from office IP }, "DateGreaterThan": { "aws:CurrentTime": "2025-01-01T00:00:00Z" }, "DateLessThan": { "aws:CurrentTime": "2025-12-31T23:59:59Z" } } } ] }
MFA Enforcement Policy
json{ "Version": "2012-10-17", "Statement": [ { "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" } } } ] }
S3 Bucket Security
Secure S3 Bucket Configuration
json// Block public access { "BlockPublicAcls": true, "IgnorePublicAcls": true, "BlockPublicPolicy": true, "RestrictPublicBuckets": true }
json// Bucket policy with encryption enforcement { "Version": "2012-10-17", "Statement": [ { "Sid": "DenyUnencryptedObjectUploads", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-secure-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "AES256" } } }, { "Sid": "DenyInsecureTransport", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::my-secure-bucket", "arn:aws:s3:::my-secure-bucket/*" ], "Condition": { "Bool": { "aws:SecureTransport": "false" } } }, { "Sid": "AllowSSLRequestsOnly", "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::my-secure-bucket", "arn:aws:s3:::my-secure-bucket/*" ], "Condition": { "NumericLessThan": { "s3:TlsVersion": "1.2" } } } ] }
S3 with Terraform (Secure Configuration)
hclresource "aws_s3_bucket" "secure_bucket" { bucket = "my-secure-bucket" tags = { Environment = "Production" Security = "High" } } resource "aws_s3_bucket_public_access_block" "secure_bucket" { bucket = aws_s3_bucket.secure_bucket.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_versioning" "secure_bucket" { bucket = aws_s3_bucket.secure_bucket.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "secure_bucket" { bucket = aws_s3_bucket.secure_bucket.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" kms_master_key_id = aws_kms_key.s3_key.arn } bucket_key_enabled = true } } resource "aws_s3_bucket_logging" "secure_bucket" { bucket = aws_s3_bucket.secure_bucket.id target_bucket = aws_s3_bucket.log_bucket.id target_prefix = "s3-access-logs/" } resource "aws_s3_bucket_lifecycle_configuration" "secure_bucket" { bucket = aws_s3_bucket.secure_bucket.id rule { id = "delete-old-versions" status = "Enabled" noncurrent_version_expiration { noncurrent_days = 90 } } }
VPC Security
Secure VPC Configuration
hclresource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "production-vpc" } } # Public subnet (for load balancers only) resource "aws_subnet" "public" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = false # Don't auto-assign public IPs tags = { Name = "public-subnet-${count.index + 1}" Tier = "Public" } } # Private subnet (for applications) resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index + 10}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "private-subnet-${count.index + 1}" Tier = "Private" } } # NAT Gateway for private subnet internet access resource "aws_eip" "nat" { count = 2 domain = "vpc" } resource "aws_nat_gateway" "main" { count = 2 allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id tags = { Name = "nat-gateway-${count.index + 1}" } }
Security Groups (Restrictive)
hcl# Application security group resource "aws_security_group" "app" { name_description = "Application security group" vpc_id = aws_vpc.main.id # Allow inbound only from load balancer ingress { description = "HTTP from ALB" from_port = 8080 to_port = 8080 protocol = "tcp" security_groups = [aws_security_group.alb.id] } # Allow outbound to database only egress { description = "PostgreSQL to database" from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.database.id] } # Allow outbound HTTPS for updates egress { description = "HTTPS for external APIs" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "app-security-group" } } # Database security group resource "aws_security_group" "database" { name_description = "Database security group" vpc_id = aws_vpc.main.id # Only allow from application ingress { description = "PostgreSQL from app" from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.app.id] } # No outbound internet access egress { description = "No internet access" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = [] } tags = { Name = "database-security-group" } }
KMS Encryption
hclresource "aws_kms_key" "main" { description = "Main encryption key" 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 services to use the key" Effect = "Allow" Principal = { Service = [ "s3.amazonaws.com", "logs.amazonaws.com", "rds.amazonaws.com" ] } Action = [ "kms:Decrypt", "kms:GenerateDataKey" ] Resource = "*" } ] }) tags = { Name = "main-kms-key" } } resource "aws_kms_alias" "main" { name = "alias/main-key" target_key_id = aws_kms_key.main.key_id }
CloudTrail Logging
hclresource "aws_cloudtrail" "main" { name = "main-trail" s3_bucket_name = aws_s3_bucket.cloudtrail.id include_global_service_events = true is_multi_region_trail = true enable_log_file_validation = true event_selector { read_write_type = "All" include_management_events = true data_resource { type = "AWS::S3::Object" values = ["arn:aws:s3:::${aws_s3_bucket.sensitive.id}/*"] } data_resource { type = "AWS::Lambda::Function" values = ["arn:aws:lambda"] } } insight_selector { insight_type = "ApiCallRateInsight" } cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*" cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_cloudwatch.arn kms_key_id = aws_kms_key.cloudtrail.arn tags = { Name = "main-cloudtrail" } }
AWS Config Rules
hclresource "aws_config_config_rule" "s3_bucket_public_read_prohibited" { name = "s3-bucket-public-read-prohibited" source { owner = "AWS" source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED" } depends_on = [aws_config_configuration_recorder.main] } resource "aws_config_config_rule" "encrypted_volumes" { name = "encrypted-volumes" source { owner = "AWS" source_identifier = "ENCRYPTED_VOLUMES" } depends_on = [aws_config_configuration_recorder.main] } resource "aws_config_config_rule" "root_account_mfa_enabled" { name = "root-account-mfa-enabled" source { owner = "AWS" source_identifier = "ROOT_ACCOUNT_MFA_ENABLED" } depends_on = [aws_config_configuration_recorder.main] } resource "aws_config_config_rule" "iam_password_policy" { name = "iam-password-policy" source { owner = "AWS" source_identifier = "IAM_PASSWORD_POLICY" } input_parameters = jsonencode({ RequireUppercaseCharacters = true RequireLowercaseCharacters = true RequireSymbols = true RequireNumbers = true MinimumPasswordLength = 14 PasswordReusePrevention = 24 MaxPasswordAge = 90 }) depends_on = [aws_config_configuration_recorder.main] }
GuardDuty for Threat Detection
hclresource "aws_guardduty_detector" "main" { enable = true datasources { s3_logs { enable = true } kubernetes { audit_logs { enable = true } } malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } } } tags = { Name = "main-guardduty" } } # SNS topic for GuardDuty findings resource "aws_sns_topic" "guardduty_alerts" { name = "guardduty-alerts" kms_master_key_id = aws_kms_key.sns.id } resource "aws_cloudwatch_event_rule" "guardduty" { name = "guardduty-findings" description = "GuardDuty findings" event_pattern = jsonencode({ source = ["aws.guardduty"] detail-type = ["GuardDuty Finding"] detail = { severity = [4, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5, 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6, 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7, 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9] } }) } resource "aws_cloudwatch_event_target" "guardduty" { rule = aws_cloudwatch_event_rule.guardduty.name target_id = "SendToSNS" arn = aws_sns_topic.guardduty_alerts.arn }
Secrets Manager
hclresource "aws_secretsmanager_secret" "db_password" { name = "production/database/password" recovery_window_in_days = 30 kms_key_id = aws_kms_key.secrets.id rotation_rules { automatically_after_days = 30 } } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = jsonencode({ username = "dbadmin" password = random_password.db_password.result host = aws_db_instance.main.endpoint port = 5432 }) } resource "random_password" "db_password" { length = 32 special = true }
python# Accessing secrets in application import boto3 import json def get_secret(secret_name): session = boto3.session.Session() client = session.client( service_name='secretsmanager', region_name='us-east-1' ) try: response = client.get_secret_value(SecretId=secret_name) secret = json.loads(response['SecretString']) return secret except Exception as e: print(f"Error retrieving secret: {e}") raise # Usage db_credentials = get_secret('production/database/password') connection_string = f"postgresql://{db_credentials['username']}:{db_credentials['password']}@{db_credentials['host']}:{db_credentials['port']}/mydb"
AWS Security Checklist
- ✅ Enable MFA on root account and all IAM users
- ✅ Use IAM roles instead of access keys
- ✅ Apply least privilege IAM policies
- ✅ Enable CloudTrail in all regions
- ✅ Enable AWS Config for compliance monitoring
- ✅ Block public S3 bucket access by default
- ✅ Encrypt all data at rest using KMS
- ✅ Use VPC with private subnets
- ✅ Restrict security groups to minimum required
- ✅ Enable GuardDuty for threat detection
- ✅ Use AWS Secrets Manager for credentials
- ✅ Enable VPC Flow Logs
- ✅ Implement backup and disaster recovery
- ✅ Use AWS Organizations for multi-account management
- ✅ Enable S3 versioning and logging
- ✅ Regular security assessments and audits
- ✅ Implement least privilege network access
- ✅ Use AWS WAF for web application protection
Conclusion
AWS security requires a comprehensive approach covering identity management, network security, data encryption, and monitoring. By implementing least privilege IAM policies, securing VPCs and S3 buckets, enabling encryption, and using services like GuardDuty and CloudTrail, you build a strong security foundation.
Cloud security is a continuous process requiring regular reviews, updates, and monitoring. For comprehensive AWS security assessments and cloud infrastructure reviews, contact the Whitespots team for expert consultation.


