Skip to main content

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

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)
  • .env files (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)

  1. Log into AWS console as root user
  2. Go to “My Security Credentials”
  3. Click “Assign MFA device”
  4. Choose virtual MFA device (Google Authenticator, Authy, 1Password)
  5. Scan QR code with authenticator app
  6. Enter two consecutive MFA codes
  7. 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: "*" or Resource: "*"
  • 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:

  1. Overly permissive IAM policies - Allows lateral movement after compromise
  2. Database credentials in code - Easy data breach if code is leaked
  3. No MFA on privileged accounts - Password alone is not secure
  4. Publicly accessible databases - Direct target for attackers
  5. 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.