8 AWS Misconfigurations That Will Get You Hacked (And How to Fix Them)
The most dangerous misconfigurations we see in production AWS environments — ranked by real-world risk, with detection commands and compliance impact for each.
What We'll Cover
- S3 Public Buckets — The Open Door
- Overly Permissive IAM Policies
- Security Groups: SSH/RDP Open to the Internet
- RDS Publicly Accessible
- RDS Without Encryption at Rest
- Missing or Misconfigured CloudTrail
- Secrets in Lambda Environment Variables
- KMS Keys Without Annual Rotation
1. S3 Public Buckets — The Open Door
Public S3 buckets are the single most common AWS misconfiguration. A single bucket with BlockPublicAcls: false and a relaxed bucket policy can expose customer data, backups, database exports, or API keys to the entire internet. The 2019 Capital One breach started exactly this way.
Even buckets created "just for internal use" often end up public when someone uploads a file with the wrong ACL, or a bucket policy is modified without checking the block public access settings.
Detection — AWS CLI
# Check all buckets for public access block
aws s3api list-buckets --query 'Buckets[].Name' --output text | while read bucket; do
echo "=== $bucket ==="
aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null || echo "No block policy"
done
# Check bucket ACLs for public grants
aws s3api list-buckets --query 'Buckets[].Name' --output text | while read bucket; do
acl=$(aws s3api get-bucket-acl --bucket "$bucket" 2>/dev/null)
if echo "$acl" | grep -q "Grants.*URI.*http://acs.amazonaws.com/groups/global/AllUsers"; then
echo "PUBLIC READ: $bucket"
fi
done
How to fix it
- Enable
S3 Block Public Accessat the account level — this prevents any bucket from going public, even by accident - Set
BlockPublicAcls: true,IgnorePublicAcls: true,BlockPublicPolicy: true,RestrictPublicBuckets: true - Audit existing buckets with
aws s3api list-buckets --query 'Buckets[*].[Name]'and check each one - Enable S3 server access logging to track all access requests
- Enable S3 versioning so you can recover from accidental overwrites
2. Overly Permissive IAM Policies
Action: "*" and Resource: "*" in an IAM policy is the cloud security equivalent of leaving your root password on a sticky note. These wildcard policies are the fastest path to privilege escalation — if an attacker gets even minimal access, they can escalate to full account control.
We also see iam:* attached to non-admin roles, inline policies with Effect: Allow on all actions, and permissions boundaries set too wide. Any of these creates a privilege escalation path.
Detection — AWS CLI
# Find all policies with wildcard actions or resources
aws iam list-policies --scope Local --query 'Policies[*].[PolicyName,Arn]' --output text | while read name arn; do
version=$(aws iam get-policy --policy-arn "$arn" --query 'Policy.DefaultVersionId' --output text)
doc=$(aws iam get-policy-version --policy-arn "$arn" --version-id "$version" --query 'PolicyVersion.Document' --output json)
if echo "$doc" | grep -q '"Action":\n.*"[*]'; then
echo "WILDCARD ACTION: $name ($arn)"
fi
if echo "$doc" | grep -q '"Resource":\n.*"[*]'; then
echo "WILDCARD RESOURCE: $name ($arn)"
fi
done
How to fix it
- Use AWS Access Analyzer to identify external access and overprivileged policies
- Apply least-privilege principle: each role gets only the permissions it needs for its specific task
- Enable IAM Access Advisors to see which services each role actually uses — remove everything else
- Set a permissions boundary on all roles to limit the maximum scope of permissions
- Replace inline policies with customer-managed policies so changes are tracked in version history
3. Security Groups: SSH/RDP Open to the Internet
A security group rule with 0.0.0.0/0 on port 22 (SSH) or port 3389 (RDP) is an open invitation for brute force attacks. Even if you think "it's just for admin access," exposed admin ports scan continuously across AWS IP ranges — you will be hit within hours of deployment.
We also see overly broad ranges like 10.0.0.0/8 when the actual requirement is a single jump host, and security groups that allow all traffic out (0.0.0.0/0 on all egress) which bypasses network-level controls.
Detection — AWS CLI
# Find security groups with SSH/RDP open to 0.0.0.0/0
aws ec2 describe-security-groups --query 'SecurityGroups[*].[GroupId,GroupName]' --output text | while read sgid sgname; do
rules=$(aws ec2 describe-security-groups --group-ids "$sgid" --query 'SecurityGroups[0].IpPermissions[]' --output json)
if echo "$rules" | grep -q '"FromPort": 22'; then
if echo "$rules" | grep -q '"CidrIp": "0.0.0.0/0"'; then
echo "SSH OPEN TO INTERNET: $sgid ($sgname)"
fi
fi
if echo "$rules" | grep -q '"FromPort": 3389'; then
if echo "$rules" | grep -q '"CidrIp": "0.0.0.0/0"'; then
echo "RDP OPEN TO INTERNET: $sgid ($sgname)"
fi
fi
done
How to fix it
- Use Systems Manager Session Manager or AWS SSM Fleet Manager instead of direct SSH — no port 22 needed, fully audited
- Use a bastion host in a dedicated admin subnet, not the default VPC
- If you must use SSH, lock it to known IP ranges via a Network ACL or a dedicated security group attached only to jump hosts
- For RDP, always use AWS Client VPN or AWS Site-to-Site VPN — never expose 3389 directly
- Enable VPC Flow Logs so you can audit what actually hit your security groups
4. RDS Publicly Accessible
Setting PubliclyAccessible: true on an RDS instance exposes your database to the internet. Even if the security group restricts access, this setting fundamentally defeats the network isolation that VPC provides — a misconfigured security group rule is all it takes for exposure.
Detection — AWS CLI
aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,PubliclyAccessible]' --output text | grep -E 'true$'
How to fix it
- Set
PubliclyAccessible: falseon all RDS instances — this is a one-time fix via the ModifyDBInstance API (no reboot required) - Connect via bastion host or VPN within the VPC
- For application servers in private subnets, use RDS in private subnets — no internet path exists
- For multi-region disaster recovery, use RDS read replicas in a secondary region with VPN connectivity, not public DNS
5. RDS Without Encryption at Rest
RDS instances created without StorageEncrypted: true store your data in plaintext on disk. AWS-managed encryption at rest uses AES-256 with AWS KMS keys — zero performance penalty, full compliance coverage. Unencrypted RDS is a direct violation of HIPAA and PCI-DSS storage requirements.
Detection — AWS CLI
# Check all RDS instances for encryption status
aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,StorageEncrypted,KmsKeyId]' --output text
How to fix it
You cannot enable encryption on an existing unencrypted RDS instance — you must create a new encrypted instance and migrate. This is why encryption should be mandatory at creation time. Use AWS Backup or DMS (Database Migration Service) to migrate with minimal downtime.
- Create a new encrypted RDS instance from a snapshot of the unencrypted database
- Update application connection strings to point to the new instance
- Validate data integrity before cutting over
- Delete the old unencrypted instance after migration is verified
- Use AWS Config rules to automatically alert on new unencrypted RDS creations
6. Missing or Misconfigured CloudTrail
CloudTrail is your audit log for everything that happens in your AWS account. Without it, you have no visibility into who did what, when, and from where. Compromises that should take minutes to detect become months-long breaches. The Uber breach of 2016 is a textbook case of what happens when audit logs aren't reviewed.
Detection — AWS CLI
# Check if CloudTrail is enabled and configured
aws cloudtrail describe-trails --query 'Trails[*].[Name,IsMultiAccountTrail,IsOrganization,IsLogging]' --output text
# Verify log file validation is enabled (SHA-256 integrity)
aws cloudtrail get-trail-status --name YOUR_TRAIL_NAME --query 'LogFileValidationEnabled'
How to fix it
- Create a multi-region CloudTrail that writes to a dedicated S3 bucket — never the same bucket as your application data
- Enable log file validation (
LogFileValidationEnabled: true) so tampering with logs is detectable - Enable CloudTrail Insights to detect anomalies automatically (e.g., spike in Describe calls, unusual API activity)
- Store CloudTrail logs in a separate account (Security Tooling account) so a compromised application account can't alter its own logs
- Enable S3 object lock on the log bucket to prevent deletion of historical logs
7. Secrets in Lambda Environment Variables
Storing API keys, database passwords, or Stripe tokens in Lambda environment variables is one of the most common cloud security assessment findings. Environment variables are visible in plain text in the AWS console, logged in CloudWatch, and stored in function configuration files. Anyone with lambda:GetFunctionConfiguration permission can read them.
The most common culprits: STRIPE_SECRET_KEY, AWS_SECRET_ACCESS_KEY, DATABASE_PASSWORD, API_KEY, and GITHUB_TOKEN. If any of these are in your Lambda config, you have a critical exposure.
Detection — AWS CLI
# Check all Lambda functions for plaintext secrets in env vars
aws lambda list-functions --query 'Functions[*].FunctionName' --output text | while read fn; do
env=$(aws lambda get-function-configuration --function-name "$fn" --query 'Environment.Variables' --output json)
if echo "$env" | grep -iE 'PASSWORD|SECRET|KEY|TOKEN|PRIVATE'; then
echo "SECRETS IN ENV: $fn"
fi
done
How to fix it
- Move all secrets to AWS Secrets Manager or AWS Systems Manager Parameter Store (use the
SecureStringtype) - Grant Lambda functions minimal IAM permissions to
secretsmanager:GetSecretValuefor the specific secret they need - Enable Lambda environment variable encryption at rest (AWS KMS) — this is free and enabled by default on new functions
- Use Lambda Layers to inject secrets at deployment time rather than storing them in config
- Never log
process.envor environment objects in Lambda — CloudWatch logs are often accessible to DevOps engineers who shouldn't see production credentials
8. KMS Keys Without Annual Rotation
AWS KMS keys that don't have automatic annual rotation enabled accumulate risk over time. The longer a key is in use, the larger the blast radius if it's ever compromised. PCI-DSS 4.0 explicitly requires annual rotation for customer-managed keys used in cardholder data environments.
Equally dangerous: KMS keys pending deletion. AWS enforces a minimum 7-day waiting period before deletion — if you accidentally scheduled deletion on a key protecting production data, you have 7 days to cancel before that data is permanently inaccessible.
Detection — AWS CLI
# List CMK keys and check rotation status
aws kms list-keys --query 'Keys[*].[KeyId,KeyArn]' --output text | while read kid arn; do
rotation=$(aws kms get-key-rotation-status --key-id "$kid" --query 'KeyRotationEnabled' --output text)
if [ "$rotation" = "False" ]; then
alias=$(aws kms list-aliases --key-id "$kid" --query 'Aliases[0].AliasName' --output text 2>/dev/null || echo "no-alias")
echo "ROTATION DISABLED: $kid (alias: $alias)"
fi
done
# Check for keys pending deletion
aws kms schedule-key-deletion --key-id YOUR_KEY_ID --pending-window-in-days 7 --dry-run 2>&1 | head -5
How to fix it
- Enable automatic key rotation for all customer-managed KMS keys:
aws kms enable-key-rotation --key-id <key-id> - Rotation is free and uses the same key ID — no migration needed, existing data stays accessible
- Set a calendar reminder 30 days before any key deletion window expires
- Use AWS Config rules to detect keys with rotation disabled and auto-remediate
Find These Issues in Your AWS Environment
Guardrail scans all 8 of these misconfigurations — plus 24 more — and maps each finding to HIPAA, PCI-DSS 4.0, and SOX compliance requirements. No AWS credentials required for the assessment.
Run a Free AWS Security Assessment →