Tools
Tools: 17 AWS security issues I spot in almost every infrastructure audit
2026-03-03
0 views
admin
IAM, the stuff everyone avoids ## 1. Root account without MFA ## 2. IAM users without MFA ## 3. Access keys older than 90 days ## 4. Access keys that nobody uses ## S3, because it's always S3 ## 5. Buckets without Public Access Block ## 6. No default encryption ## 7. Versioning disabled ## EC2, where clutter quietly piles up ## 8. Publicly shared AMIs ## 9. Unencrypted EBS volumes ## 10. Stopped instances nobody remembers ## VPC, where real breaches start ## 11. Workloads running in the default VPC ## 12. Security groups open to the world ## 13. VPC flow logs disabled ## RDS, the crown jewels ## 14. Publicly accessible RDS instances ## 15. Unencrypted RDS storage ## 16. No Multi-AZ deployment ## Cost, the quiet warning light ## 17. Unattached Elastic IPs ## So… what now? I've been doing cloud infrastructure audits for a while now - different companies, different industries, tiny teams and huge ones. And almost every time I open an AWS account, I run into the same set of problems. They're not exotic zero-days or clever multi-step attack chains. They're basic misconfigurations that stick around because no one ever circles back to clean them up. Here are the 17 checks I run every time. Most are 10-minute fixes. A lot of them have been sitting there for months. IAM is boring. Reviewing policies is tedious. So it gets messy fast. This one always makes me uneasy. The root user can do everything: billing changes, closing the account, changing the support plan - things even an IAM user with AdministratorAccess can't do. And yet… I still find root accounts protected by just a password. No MFA. Sometimes the password is literally in a shared spreadsheet. Fix: 5 minutes. Go to IAM → Security credentials and add MFA. Use a hardware key if you can, or a virtual MFA app if you can't. Skipping this is not an option. Same problem, but for regular humans. Someone creates a developer IAM user, enables console access, and MFA never gets set up. Six months later, that user has admin privileges and logs in from a random Wi-Fi network. I check every user with a console login profile. If they don't have at least one MFA device attached, that's a finding. Access keys don't expire. If you don't rotate them, the same key pair works forever - until someone deactivates it. I regularly see keys that are 400, 600, even 900+ days old. They end up in CI/CD, hardcoded in scripts, or living in .env files that got committed years ago. CIS recommends rotating every 90 days. Honestly, in 2026, if you're still relying on long-lived keys, strongly consider moving to IAM Identity Center or OIDC federation. If you must keep keys, rotate them. This one's sneaky: an access key that's still "Active" but hasn't been used in 30+ days - or worse, was created and never used at all. Unused keys are pure risk with zero benefit. Like leaving a door unlocked to a room nobody ever enters. Delete them. AWS shows LastUsedDate. If it says "None" or it's from last year: deactivate it, wait a week to ensure nothing breaks, then delete it. If you've been around cloud long enough, you've seen the headlines. Capital One, Twitch, even US military-related incidents - S3 comes up again and again. You'd think people would learn. They don't. S3 has "Block Public Access" and it's one of the best safety rails AWS has shipped. There are four toggles, and in most cases all four should be ON: With all four enabled, it doesn't matter if someone accidentally adds a public ACL or a wildcard bucket policy - S3 will refuse to go public. I still find buckets where one or more are off, or all four are disabled because "the app needs public access." No - it needs CloudFront with OAC, not a public bucket. One command wipes out a whole class of problems. Since January 2023, AWS encrypts new S3 objects with SSE-S3 by default. But buckets created before that change might still not have default encryption configured. Your real threat model probably isn't someone stealing disks from an AWS datacenter - but compliance frameworks care. CIS cares. Auditors care. And it's basically free to enable. Not strictly security, but it's reliability - and I've watched teams lose important data because someone ran aws s3 rm --recursive on the wrong prefix. Versioning keeps older copies of overwritten/deleted objects. It's cheap insurance. Turn it on for anything that matters. EC2 is great at accumulating debt: old instances, forgotten AMIs, resources with no tags, things no one "owns" anymore. Every account has them. Custom AMIs can contain credentials, internal config, baked-in secrets, proprietary software… and if someone makes one public (even by accident), anyone can launch it and inspect the filesystem. I check for any owned AMI where Public: true. It's almost never intentional - usually someone was testing cross-account sharing and forgot to undo the setting. Same story as S3 encryption: compliance + defense in depth. Unencrypted EBS means the data is stored unencrypted on the underlying hardware. The annoying part: you can't encrypt an existing volume in place. You snapshot, copy the snapshot with encryption, then create a new encrypted volume. It's doable, just not "one click." The better move is enabling EBS encryption by default in each region. Everything new is encrypted automatically, and you migrate old volumes over time. This is more cost than security, but it's a very loud signal about the account's hygiene. Stopped instances still rack up charges for attached EBS volumes. They stick around because nobody knows if it's safe to terminate them: "Maybe someone needs it." "It might have data." So it sits there forever. If an instance has been stopped for 30+ days: create an AMI, document what it's for, then terminate it. You'll cut waste and reduce the "mystery infrastructure" pile. Network misconfigurations are how the bad stuff happens. Open security groups are basically open doors. Every region has a default VPC. It's built for quick-start demos: public subnets, an internet gateway, public IPs by default. It's not where production should live. I check whether the default VPC has any ENIs attached. If it does, something's running there - and it's usually not supposed to be. The fix isn't "delete the default VPC" immediately. The fix is: migrate workloads to a custom VPC with private subnets, NAT, proper routing - then delete the default VPC once it's empty. This is the big one. I see it everywhere. Inbound rules allowing 0.0.0.0/0 on sensitive ports: The worst one I've seen: a security group that allowed all traffic (protocol -1) from 0.0.0.0/0. Every port, every protocol, from anywhere. On a production database server. "But we have a firewall in front." Security groups are your firewall. That's the point. Flow logs give you visibility: source, destination, port, accept/reject. Without them, when something weird happens, you're basically guessing. I usually skip empty default VPCs here because it's noise. But any custom VPC that runs workloads should have flow logs turned on (CloudWatch Logs or S3). Databases hold the money: customer data, financial records, PII. If your DB is exposed or poorly configured, nothing else matters. RDS has a setting: "Publicly accessible." It defaults to No… and yet I keep finding it set to Yes. A publicly accessible RDS instance gets a public DNS name that resolves to a public IP. Even if the security group is tight today, one accidental change later and your database is exposed to the internet. One flag. Immediate effect. No downtime. Same as EBS, but higher stakes because databases usually contain your most sensitive data. And the same painful limitation: you can't enable encryption on an existing running instance. You have to snapshot, copy with encryption, restore - meaning downtime and planning. That's why it gets postponed forever: "Next sprint." For the last 18 months. Not security - reliability. But it matters because when a single-AZ database dies at 3 AM on a Friday, the on-call person will instantly wish someone had enabled Multi-AZ. Multi-AZ gives you automatic failover: minutes of disruption instead of hours of recovery. I don't usually flag tiny dev/test instances here, but anything that looks like production should have Multi-AZ. Not security findings, but they tell you how well the team manages the account. If money is leaking through obvious holes, security is usually leaking too. An unused Elastic IP costs about $3.65/month. Not huge - until you find 15-20 of them scattered across regions. That's $70/month for nothing. More importantly: if nobody noticed 20 orphaned EIPs, what else has been sitting around unnoticed? If you read this and thought "we probably have a few of these," you do. Everyone does. It's not a knowledge problem. Most engineers already know they should enforce MFA and encrypt databases. The real issue is visibility: nobody runs these checks regularly. No dashboard. No routine. So each quarterly audit finds 40 issues, the team fixes 20, and by next quarter there are 45 again. I used to run these checks by hand before every audit. Same commands, same console clicks, same mental checklist. Eventually I just automated the whole thing. The result is a CLI tool that runs all 17 checks in ~12 seconds and outputs a prioritized report with fixes, AWS CLI commands, and Terraform snippets for each finding. It's open source: cloud-audit on GitHub No config, no stored credentials, no SaaS dashboard. It uses the AWS credentials you already have and prints a report. If your setup needs more than automated checks - architecture review, remediation planning, Terraform migration - that's what I do for a living. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
aws s3api put-public-access-block \ --bucket my-bucket \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
aws s3api put-public-access-block \ --bucket my-bucket \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true CODE_BLOCK:
aws s3api put-public-access-block \ --bucket my-bucket \ --public-access-block-configuration \ BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true CODE_BLOCK:
aws ec2 describe-images --owners self \ --query "Images[?Public==\`true\`].[ImageId,Name]" Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
aws ec2 describe-images --owners self \ --query "Images[?Public==\`true\`].[ImageId,Name]" CODE_BLOCK:
aws ec2 describe-images --owners self \ --query "Images[?Public==\`true\`].[ImageId,Name]" CODE_BLOCK:
aws ec2 enable-ebs-encryption-by-default --region eu-central-1 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
aws ec2 enable-ebs-encryption-by-default --region eu-central-1 CODE_BLOCK:
aws ec2 enable-ebs-encryption-by-default --region eu-central-1 CODE_BLOCK:
aws rds modify-db-instance \ --db-instance-identifier my-database \ --no-publicly-accessible \ --apply-immediately Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
aws rds modify-db-instance \ --db-instance-identifier my-database \ --no-publicly-accessible \ --apply-immediately CODE_BLOCK:
aws rds modify-db-instance \ --db-instance-identifier my-database \ --no-publicly-accessible \ --apply-immediately COMMAND_BLOCK:
pip install cloud-audit
cloud-audit scan Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
pip install cloud-audit
cloud-audit scan COMMAND_BLOCK:
pip install cloud-audit
cloud-audit scan - BlockPublicAcls
- IgnorePublicAcls
- BlockPublicPolicy
- RestrictPublicBuckets
how-totutorialguidedev.toaiservernetworkdnsfirewallroutingsubnetdatabaseterraformgitgithub