Your cloud infrastructure is probably misconfigured right now. I've reviewed enough breach postmortems to know that attackers don't need zero-days when you've left an S3 bucket public, over-privileged an IAM role, or forgotten to enable CloudTrail. Let's fix that before someone else finds it.
The Brutal Reality of Cloud Misconfigurations
DevOps teams move fast. Cloud providers make deployment easy. The deadly combination? They also make it easy to deploy things insecurely, and "we'll harden it later" never happens.
Capital One in 2019? Overprivileged IAM role plus SSRF. Pegasus Airlines leaked 23 million records because someone clicked "make public" on an S3 bucket. Tesla got cryptominers because their Kubernetes dashboard sat on the internet without auth.
None of these needed sophisticated exploits. They needed someone to try default credentials or run a bucket scanner.
Most cloud breaches are preventable with proper configuration. But you need to actually implement it, and that requires discipline your sprint velocity probably doesn't account for.
Storage: Lock It Down Before Someone Else Does
Public cloud storage is the easiest way to make headlines for the wrong reasons. Here's how to not become the next data leak cautionary tale.
S3 Bucket Hardening That Actually Works
Enable Block Public Access at the account level. Not per bucket. Not "just for production." Account-wide, immediately.
# Block public access across your entire AWS account
aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Yeah, this breaks that static site someone deployed six months ago. Good. Static sites belong behind CloudFront with Origin Access Identity, not sitting as public buckets.
Audit existing buckets because someone definitely went rogue:
# Check all buckets for public access
aws s3api list-buckets --query 'Buckets[].Name' --output text | while read bucket; do
echo "Checking: $bucket"
aws s3api get-bucket-acl --bucket $bucket
aws s3api get-bucket-policy-status --bucket $bucket 2>/dev/null || echo "No policy configured"
done
Enable versioning and object lock. When credentials leak, versioning prevents attackers from permanently deleting your evidence:
# Enable versioning
aws s3api put-bucket-versioning \
--bucket critical-data \
--versioning-configuration Status=Enabled
# Object lock for compliance and incident response
aws s3api put-object-lock-configuration \
--bucket critical-data \
--object-lock-configuration \
'ObjectLockEnabled=Enabled,Rule={DefaultRetention={Mode=GOVERNANCE,Days=30}}'
Azure Storage: Same Story, Different Cloud
Azure makes public blob access way too easy. Shut it down at the account level:
# Disable anonymous blob access on the storage account
az storage account update \
--name prodstorageacct \
--resource-group production-rg \
--allow-blob-public-access false
# Find containers that someone made public anyway
az storage container list \
--account-name prodstorageacct \
--query "[?properties.publicAccess != 'None'].name"
If that query returns results, someone ignored security. Fix it now.
IAM: Least Privilege Isn't a Suggestion
Identity and Access Management is where teams create their own nightmare scenarios. Least privilege isn't optional—it's the difference between a contained incident and total account compromise.
Kill the Managed Policy Habit
AWS managed policies like AdministratorAccess or PowerUserAccess are training wheels. Remove them. Build custom policies scoped to exactly what's needed:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::production-uploads/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:123456789012:key/abc123def-456g-789h-012i-345jklmnopqr"
}
]
}
Notice the specificity? Not all S3 actions. Not all buckets. Not all KMS keys. Just what's required.
Audit for Privilege Creep
IAM Access Analyzer tells you what permissions are actually used versus what's granted:
# Create analyzer
aws accessanalyzer create-analyzer \
--analyzer-name prod-analyzer \
--type ACCOUNT
# Review overly permissive access
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/prod-analyzer
Run this monthly. Delete unused permissions. If a role hasn't touched a permission in 90 days, it doesn't need it.
MFA is Non-Negotiable
Enforce multi-factor authentication for console access. No exceptions for "emergency" accounts—especially not those.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllWithoutMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Attach this to all human users. They can enable MFA and nothing else until it's active.
Azure RBAC: Scope Properly or Suffer
Azure role assignments work at subscription, resource group, or resource scope. Always use the narrowest scope:
# Right: Assign at resource level
az role assignment create \
--assignee dev-team-sp \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/sub-id/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/prodstore"
# Wrong: Contributor at subscription level - lateral movement paradise
Network Security: Default Deny Everything
If your security groups allow 0.0.0.0/0 on ingress, we have a problem.
Security Groups Are Stateful, Use That
AWS security groups are stateful. Only allow inbound; responses are automatic. Audit for the overly permissive:
# Find security groups allowing internet traffic
aws ec2 describe-security-groups \
--query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].{ID:GroupId,Name:GroupName}" \
--output table
If this returns anything beyond load balancers on 443, investigate immediately.
Build with zero trust:
- Web tier: Allow 443 from CloudFront or ALB security group
- App tier: Allow specific ports from web tier security group
- Database tier: Allow DB port from app tier security group only
- Management: Use Systems Manager Session Manager, not SSH/RDP from the internet
VPC Flow Logs: Visibility is Security
Enable VPC Flow Logs for every VPC. Not just production. Staging will be the compromised one:
# Enable flow logs to CloudWatch
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-abc123 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /aws/vpc/flowlogs/production
Ship these to your SIEM. When incident response starts, you'll want to know what connected where and when.
Private Subnets for Non-Public Resources
Public subnets should only contain:
- Internet gateways
- NAT gateways
- Load balancers
Everything else—app servers, databases, Lambda—goes in private subnets with no internet gateway route.
Secrets Management: Git is Not a Vault
I've found AWS credentials in public GitHub repos more times than I care to count. Use proper secrets management or accept that your keys will leak.
Secrets Manager and Rotation
Store credentials, API keys, and secrets in dedicated services:
import boto3
import json
def get_database_creds():
client = boto3.client('secretsmanager', region_name='us-east-1')
response = client.get_secret_value(SecretId='prod/db/creds')
secret = json.loads(response['SecretString'])
return secret['username'], secret['password']
Enable automatic rotation:
aws secretsmanager rotate-secret \
--secret-id prod/db/creds \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation \
--rotation-rules AutomaticallyAfterDays=30
Don't Do This With Secrets
Still seeing this in production:
# Wrong - avoid this pattern
export DB_PASSWORD="SuperSecret123"
docker run -e DB_PASSWORD=$DB_PASSWORD myapp
Use secrets injection, not environment variables. And never commit .env files.
Logging: Log Everything, Alert on Anomalies
CloudTrail is Mandatory
CloudTrail logs every API call in your AWS account. Without it, you won't know when compromised credentials are being used:
# Create multi-region trail with validation
aws cloudtrail create-trail \
--name org-trail \
--s3-bucket-name cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation
aws cloudtrail start-logging --name org-trail
Store logs in a separate security account. When an app account gets compromised, you don't want attackers deleting evidence.
Alert on Suspicious Activity
CloudWatch alarms for activities that should be rare:
aws cloudwatch put-metric-alarm \
--alarm-name root-account-usage \
--alarm-description "Root account should never be used" \
--metric-name RootAccountUsage \
--namespace CloudTrailMetrics \
--statistic Sum \
--period 60 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1
Monitor for:
- Root account logins (should be zero)
- Security group modifications
- IAM policy changes
- Authentication attempts exceeding threshold
- Unusual API patterns by region or service
Encryption: At Rest and In Transit
Default Encryption for Storage
Enable encryption by default for all storage:
# S3 default encryption with KMS
aws s3api put-bucket-encryption \
--bucket prod-data \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"arn:aws:kms:us-east-1:123456789012:key/abc-123"}}]}'
# EBS encryption by default for entire region
aws ec2 enable-ebs-encryption-by-default --region us-east-1
Use customer-managed KMS keys, not AWS-managed. You want control over rotation and access policies.
TLS 1.2 Minimum Always
Configure load balancers to reject anything older:
aws elbv2 modify-listener \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/prod-alb/abc123/def456 \
--ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01
TLS 1.0 and 1.1 are deprecated and vulnerable. No legitimate reason to support them.
Infrastructure as Code: Policy Enforcement
Manual configuration doesn't scale and creates drift. Use IaC with automated checks.
Terraform with Policy Validation
Define infrastructure in code and enforce security policies:
resource "aws_s3_bucket" "data" {
bucket = "prod-data-bucket"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.bucket_key.arn
}
}
}
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Use Open Policy Agent to block bad configurations:
package terraform.analysis
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.block_public_acls
msg := sprintf("S3 bucket %s must block public ACLs", [resource.address])
}
Implementation Checklist
Your sprint backlog for this week:
Storage:
- Enable account-level Block Public Access
- Audit existing buckets for public access
- Enable versioning on all data buckets
- Configure default encryption with customer-managed keys
IAM:
- Replace managed policies with custom least-privilege policies
- Enforce MFA for all console users
- Enable IAM Access Analyzer
- Remove unused credentials and roles
Network:
- Audit security groups for unrestricted ingress
- Enable VPC Flow Logs
- Move workloads to private subnets
- Implement security group chaining
Secrets:
- Migrate hardcoded secrets to Secrets Manager
- Enable automatic credential rotation
- Scan repos for leaked credentials
- Remove secrets from environment variables
Logging:
- Enable CloudTrail with log validation
- Configure alerts for suspicious activity
- Ship logs to centralized SIEM
- Set up compliance scanning
Encryption:
- Enable default storage encryption
- Enforce TLS 1.2 minimum on endpoints
- Migrate to customer-managed KMS keys
- Audit for unencrypted resources
The Bottom Line
Cloud security isn't difficult, but it requires discipline. The configurations that lead to breaches aren't exotic—they're basic settings teams skip because of velocity pressure.
Treat every environment like production from a security standpoint. Your staging environment has production credentials. Your dev environment connects to real services. Attackers know this.
Build security into your deployment pipeline. Make it impossible to deploy insecure configurations. When someone asks to "just quickly open up that security group," the answer is no.
Attackers are already scanning for your misconfigurations. Don't make it easy for them.