AWS Security
5 Security Vulnerabilities We Find in 90% of Startups
After auditing dozens of startup AWS environments, we see the same security issues repeatedly. Here are the 5 most common vulnerabilities, why they're dangerous, and how to fix them in under a day.
Cloud Associates
We’ve conducted security reviews for dozens of startups over the years. The vast majority are smart, well-intentioned teams building great products. But when we audit their AWS environments, we find the same security vulnerabilities over and over again.
These aren’t sophisticated zero-day exploits. They’re basic security hygiene issues that create massive risk. The scary part? Most founders don’t know these vulnerabilities exist until we point them out.
This guide covers the 5 most common security vulnerabilities we find, why they’re dangerous, real-world attack scenarios, and step-by-step fixes you can implement today.
Vulnerability #1: Overly Permissive IAM Policies (Found in 95% of Startups)
The Problem
We regularly find IAM policies like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
Translation: “Let this user/role do anything to any AWS resource.”
This is the AWS equivalent of giving everyone in your company admin access to your production database.
Why It Happens
Scenario 1: The Shortcut
- Engineer needs to deploy to S3
- Gets “Access Denied” error
- Googles “AWS permission denied S3”
- Finds Stack Overflow answer: “Just add
Action: *” - Problem solved! (But security destroyed)
Scenario 2: The Legacy Policy
- IAM policy was created during early prototyping
- “We’ll tighten this up later”
- “Later” never happens
- Policy remains in production for 2 years
Real-World Attack Scenario
Step 1: Attacker compromises an EC2 instance via application vulnerability (SQL injection, RCE, etc.)
Step 2: Instance has IAM role with Action: "*"
Step 3: Attacker uses AWS CLI from compromised instance:
# List all S3 buckets
aws s3 ls
# Download entire customer database
aws s3 sync s3://production-customer-data /tmp/stolen/
# Steal database credentials from Secrets Manager
aws secretsmanager get-secret-value --secret-id prod/db/password
# Create new IAM user for persistent access
aws iam create-user --user-name backdoor-admin
aws iam attach-user-policy --user-name backdoor-admin --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
Step 4: Attacker has full control of your AWS account, steals data, plants backdoors, and possibly destroys resources.
How to Fix It
Step 1: Audit existing IAM policies
# List all IAM policies with overly broad permissions
aws iam list-policies --scope Local --query 'Policies[?PolicyName!=`null`].[PolicyName,Arn]' --output table
# For each policy, check if it has Action: "*"
aws iam get-policy-version --policy-arn arn:aws:iam::123456789:policy/YourPolicy --version-id v1
Step 2: Apply least privilege principle
Bad policy:
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
Good policy (specific to use case):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-deployment-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-deployment-bucket"
}
]
}
Step 3: Use AWS IAM Access Analyzer
# Enable IAM Access Analyzer (finds overly permissive policies)
aws accessanalyzer create-analyzer \
--analyzer-name security-audit \
--type ACCOUNT
Access Analyzer will identify policies that grant access to external entities or are overly permissive.
Time to fix: 2-4 hours to audit and tighten all IAM policies
Impact: Prevents lateral movement if one service is compromised
Vulnerability #2: Database Credentials in Code or Environment Variables (Found in 92% of Startups)
The Problem
We find database passwords stored in:
- Application code (committed to Git)
.envfiles (committed to Git)- Environment variables in ECS task definitions (visible in console)
- Lambda environment variables (visible in console)
- EC2 user data scripts
Example we find regularly:
// db.js
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'prod-db.abc123.ap-southeast-2.rds.amazonaws.com',
user: 'admin',
password: 'SuperSecret123!', // Hard-coded password
database: 'production'
});
Or in environment variables:
# .env file (committed to Git repository)
DATABASE_URL=postgresql://admin:SuperSecret123@prod-db.abc123.ap-southeast-2.rds.amazonaws.com:5432/production
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Why It Happens
Scenario 1: Works on My Machine
- Developer sets up local environment with hard-coded credentials
- “I’ll change this before deploying to prod”
- Forgets to change it
- Hard-coded credentials end up in production
Scenario 2: Quick Fix Syndrome
- Application can’t connect to database
- Engineer adds credentials to environment variable to “debug”
- Fixes the issue
- Forgets to move credentials to Secrets Manager
Real-World Attack Scenario
Step 1: Attacker finds your GitHub repository (public or leaked private repo)
Step 2: Searches commit history for credentials:
git log -p | grep -i "password\|secret\|key" -A 2 -B 2
Step 3: Finds database credentials in commit from 6 months ago (even if removed from current code, it’s in Git history)
Step 4: Connects directly to your production database from their laptop:
psql postgresql://admin:SuperSecret123@prod-db.abc123.ap-southeast-2.rds.amazonaws.com:5432/production
Step 5: Exfiltrates customer data, modifies records, or holds data for ransom.
How to Fix It
Step 1: Use AWS Secrets Manager
# Store database password in Secrets Manager
aws secretsmanager create-secret \
--name prod/db/password \
--description "Production database password" \
--secret-string '{"username":"admin","password":"SuperSecret123!"}'
Step 2: Update application code to fetch from Secrets Manager
// db.js - Updated to use Secrets Manager
const AWS = require('aws-sdk');
const mysql = require('mysql');
const secretsManager = new AWS.SecretsManager({ region: 'ap-southeast-2' });
async function getDbCredentials() {
const data = await secretsManager.getSecretValue({ SecretId: 'prod/db/password' }).promise();
return JSON.parse(data.SecretString);
}
async function createConnection() {
const credentials = await getDbCredentials();
return mysql.createConnection({
host: 'prod-db.abc123.ap-southeast-2.rds.amazonaws.com',
user: credentials.username,
password: credentials.password,
database: 'production'
});
}
Step 3: Grant IAM permissions to access the secret
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:ap-southeast-2:123456789:secret:prod/db/password-*"
}
]
}
Step 4: Scan Git history and remove exposed credentials
# Install BFG Repo-Cleaner
brew install bfg
# Remove passwords from Git history
bfg --replace-text passwords.txt .git
# Force push cleaned history (coordinate with team first!)
git push --force
Step 5: Rotate compromised credentials
If credentials were ever in Git (even for 5 minutes), assume they’re compromised. Rotate immediately.
# Rotate RDS password
aws rds modify-db-instance \
--db-instance-identifier prod-db \
--master-user-password NewSecurePassword123! \
--apply-immediately
Time to fix: 3-6 hours (for all secrets in your application)
Cost: Secrets Manager: $0.40/secret/month + $0.05/10,000 API calls (~$5/month total)
Vulnerability #3: No MFA on Privileged AWS Accounts (Found in 88% of Startups)
The Problem
AWS accounts with admin access have passwords but no multi-factor authentication (MFA).
What we typically find:
- Root account: Password-only, no MFA
- IAM users with AdministratorAccess: Password-only, no MFA
- IAM users who can create/delete resources: Password-only, no MFA
Why It Happens
Scenario 1: “We’ll Add It Later”
- Account created during rapid prototyping
- “We’ll enable MFA once we’re in production”
- Forgotten
Scenario 2: “It’s Inconvenient”
- Engineers don’t want to pull out their phone for every login
- Prioritise convenience over security
Real-World Attack Scenario
Step 1: Attacker obtains AWS password via:
- Phishing email (fake AWS notification)
- Reused password from another breach (check haveibeenpwned.com)
- Malware on engineer’s laptop (keylogger)
- Social engineering
Step 2: Attacker logs into AWS console with username and password (no MFA required)
Step 3: Attacker has full control:
# Steal data
aws s3 sync s3://production-data /tmp/stolen/
# Mine cryptocurrency on your AWS bill
aws ec2 run-instances --image-id ami-123456 --instance-type p3.16xlarge --count 20
# Delete everything
aws s3 rb s3://production-data --force
aws rds delete-db-instance --db-instance-identifier prod-db --skip-final-snapshot
Cost to you:
- Data breach: $50K+ in incident response and legal fees
- Cryptocurrency mining: $10K-50K in unexpected AWS charges
- Downtime: $X per hour of revenue loss
How to Fix It
Step 1: Enable MFA on root account (CRITICAL)
- Log into AWS console as root user
- Go to “My Security Credentials”
- Click “Assign MFA device”
- Choose virtual MFA device (Google Authenticator, Authy, 1Password)
- Scan QR code with authenticator app
- Enter two consecutive MFA codes
- Save recovery codes in password manager
Step 2: Enable MFA on all IAM users with console access
# List all IAM users with console access but no MFA
aws iam list-users --query 'Users[*].[UserName]' --output text | while read user; do
mfa=$(aws iam list-mfa-devices --user-name $user --query 'MFADevices' --output text)
console=$(aws iam get-login-profile --user-name $user 2>/dev/null)
if [ -z "$mfa" ] && [ ! -z "$console" ]; then
echo "❌ $user has console access but NO MFA"
fi
done
Step 3: Enforce MFA via IAM policy
Create a policy that denies all actions except enabling MFA if MFA is not enabled:
{
"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"
}
}
}
]
}
Attach this policy to all users.
Step 4: Use IAM roles instead of IAM users
Better approach: Use SSO (AWS IAM Identity Center) with MFA enforced at the SSO provider level.
Time to fix: 30 minutes for root account, 1-2 hours to enable for all IAM users
Cost: $0 (MFA is free)
Vulnerability #4: Publicly Accessible Databases (Found in 78% of Startups)
The Problem
RDS databases with “Publicly Accessible: Yes” setting enabled, allowing anyone on the internet to attempt connections.
What we find:
- RDS instances with public IP addresses
- Security groups allowing 0.0.0.0/0 on port 3306 (MySQL), 5432 (PostgreSQL), 1433 (SQL Server)
Why It Happens
Scenario 1: “I Need to Connect from My Laptop”
- Engineer working from home can’t connect to database
- Sets “Publicly Accessible: Yes” and adds 0.0.0.0/0 to security group
- Intends to fix it later
- Never does
Scenario 2: “Following a Tutorial”
- Online tutorial says to make RDS publicly accessible
- Engineer follows tutorial
- Database goes to production with public access
Real-World Attack Scenario
Step 1: Attacker scans AWS IP ranges for open databases
# Shodan query to find exposed RDS databases
shodan search "port:5432 product:PostgreSQL"
shodan search "port:3306 product:MySQL"
Step 2: Attacker finds your database at public IP
Step 3: Attacker attempts to brute-force admin password
# Automated brute force with common passwords
for password in admin 123456 password postgres admin123; do
psql -h your-db.abc123.ap-southeast-2.rds.amazonaws.com -U admin -d production -c "SELECT 1" -W $password
done
Step 4: If successful (weak password), attacker has full database access
How to Fix It
Step 1: Check if your databases are publicly accessible
# List RDS instances with public accessibility
aws rds describe-db-instances \
--query 'DBInstances[?PubliclyAccessible==`true`].[DBInstanceIdentifier,Endpoint.Address,PubliclyAccessible]' \
--output table
Step 2: Disable public accessibility
aws rds modify-db-instance \
--db-instance-identifier your-db \
--no-publicly-accessible \
--apply-immediately
Step 3: Restrict security group to private subnets only
# Remove 0.0.0.0/0 rules
aws ec2 revoke-security-group-ingress \
--group-id sg-123456 \
--ip-permissions IpProtocol=tcp,FromPort=5432,ToPort=5432,IpRanges='[{CidrIp=0.0.0.0/0}]'
# Add rule allowing only your application servers (private subnet)
aws ec2 authorize-security-group-ingress \
--group-id sg-123456 \
--protocol tcp \
--port 5432 \
--source-group sg-app-servers
Step 4: Set up bastion host or VPN for admin access
If you need to connect from your laptop:
Option A: Bastion host (jump box)
# SSH tunnel through bastion
ssh -L 5432:prod-db.abc123.ap-southeast-2.rds.amazonaws.com:5432 ec2-user@bastion.example.com
psql -h localhost -U admin -d production
Option B: AWS Systems Manager Session Manager (no bastion needed)
# Start port forwarding session
aws ssm start-session \
--target i-instance-id \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["prod-db.abc123.ap-southeast-2.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
# Connect via localhost
psql -h localhost -U admin -d production
Time to fix: 1-2 hours (including bastion/VPN setup)
Cost: Bastion host: ~$10/month (t3.nano), or $0 for SSM Session Manager
Vulnerability #5: No Encryption at Rest (Found in 72% of Startups)
The Problem
Data stored on EBS volumes, RDS databases, and S3 buckets is not encrypted.
What we find:
- EBS volumes: Not encrypted
- RDS databases: Encryption disabled
- S3 buckets: No default encryption
Why It Happens
Scenario 1: “It’s Complicated”
- Engineers assume encryption is hard to set up
- “We’ll add it later when we have time”
Scenario 2: “We Don’t Store Sensitive Data”
- “It’s just logs and cached data”
- Reality: Logs often contain PII, API keys, session tokens
Real-World Attack Scenario
Step 1: AWS accidentally exposes EBS snapshot to public (rare but has happened)
Step 2: Attacker downloads unencrypted snapshot
Step 3: Attacker mounts snapshot and extracts data
# Create volume from snapshot
aws ec2 create-volume --snapshot-id snap-123456 --availability-zone ap-southeast-2a
# Attach to attacker's EC2 instance
aws ec2 attach-volume --volume-id vol-123456 --instance-id i-attacker --device /dev/sdf
# Mount and extract data
mount /dev/xvdf /mnt
cat /mnt/app/logs/access.log | grep "api_key"
Step 4: Data breach
How to Fix It
Step 1: Enable default encryption for EBS volumes
# Enable EBS encryption by default in your AWS account
aws ec2 enable-ebs-encryption-by-default --region ap-southeast-2
Step 2: Encrypt existing EBS volumes
# Take snapshot of volume
aws ec2 create-snapshot --volume-id vol-123456 --description "Before encryption"
# Create encrypted snapshot from unencrypted snapshot
aws ec2 copy-snapshot \
--source-snapshot-id snap-123456 \
--source-region ap-southeast-2 \
--encrypted \
--kms-key-id alias/aws/ebs \
--description "Encrypted snapshot"
# Create new encrypted volume from encrypted snapshot
aws ec2 create-volume \
--snapshot-id snap-encrypted-789 \
--availability-zone ap-southeast-2a \
--encrypted
# Detach old volume, attach new encrypted volume, delete old volume
Step 3: Enable encryption on RDS
For existing databases (requires re-creation):
# Take final snapshot
aws rds create-db-snapshot \
--db-instance-identifier prod-db \
--db-snapshot-identifier prod-db-before-encryption
# Create encrypted copy of snapshot
aws rds copy-db-snapshot \
--source-db-snapshot-identifier prod-db-before-encryption \
--target-db-snapshot-identifier prod-db-encrypted \
--kms-key-id alias/aws/rds
# Restore from encrypted snapshot
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier prod-db-encrypted \
--db-snapshot-identifier prod-db-encrypted
# Update application to use new database endpoint
For new databases: Enable encryption during creation (checkbox in console or --storage-encrypted in CLI)
Step 4: Enable default encryption for S3
# Enable default encryption for S3 bucket
aws s3api put-bucket-encryption \
--bucket my-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'
Time to fix: 4-8 hours (encrypting existing resources requires downtime planning)
Cost: $0 for SSE-S3 and AWS-managed KMS keys, ~$1-5/month if using customer-managed KMS keys
The Security Audit Checklist
Use this checklist to audit your AWS environment today:
IAM & Access
- No IAM policies with
Action: "*"orResource: "*" - Root account has MFA enabled and is not used daily
- All IAM users with console access have MFA enabled
- No AWS credentials in code or Git history
- Secrets stored in AWS Secrets Manager or Parameter Store
- IAM Access Analyzer enabled
Network Security
- No RDS databases publicly accessible
- Security groups follow least privilege (no 0.0.0.0/0 for databases)
- VPC flow logs enabled
- Bastion host or VPN for admin access
Data Protection
- EBS encryption enabled by default
- All RDS instances encrypted
- S3 buckets have default encryption enabled
- S3 buckets block all public access (unless public site)
Monitoring & Logging
- CloudTrail enabled and logging to S3
- CloudWatch alarms for suspicious activity
- AWS Config enabled for compliance checking
- GuardDuty enabled for threat detection
Application Security
- WAF enabled on CloudFront/ALB
- HTTPS enforced (no HTTP)
- Security groups reviewed quarterly
- Regular security patching of EC2 instances
Time to complete audit: 2-3 hours
Conclusion
These 5 vulnerabilities are found in 90% of the startup AWS environments we audit:
- Overly permissive IAM policies - Allows lateral movement after compromise
- Database credentials in code - Easy data breach if code is leaked
- No MFA on privileged accounts - Password alone is not secure
- Publicly accessible databases - Direct target for attackers
- No encryption at rest - Data exposed if snapshots leak
Total time to fix all 5: 1-2 days
Cost to fix: ~$15-20/month (mostly Secrets Manager costs)
Cost of a breach: $50K-500K+ in incident response, legal fees, lost revenue, and customer trust
The ROI is obvious. Block out a day this week, go through this checklist, and close these vulnerabilities before an attacker finds them.
Need help securing your AWS environment? Our AWS Well-Architected Review includes a comprehensive security audit, prioritised remediation plan, and hands-on implementation support delivered in 2 weeks for $4,500.