The 2026 Cloud Posture Audit: 12 Misconfigurations We Find in Every AWS Account
A field-tested audit checklist of the 12 findings present in 9 out of 10 AWS account audits — IAM, networking, logging, KMS, SCPs, and the contextual misconfigurations CSPM tools still miss in 2026.
Every AWS account audit we run finds roughly the same dozen misconfigurations. CSPM tools catch about half of them. The other half live in the gap between what a scanner can detect and what an engineer with five years of AWS scar tissue notices.
This is the field-tested checklist we walk every cloud posture audit against. Twelve findings present in 9 out of 10 audits we run, regardless of company size, vertical, or how recently the team thought they'd hardened the estate. Each item names the misconfiguration, the blast radius, the detection method, and the fix — including the boring details that make the fix actually stick instead of regressing in three months.
Why "we ran a CSPM scan" is not a posture audit
Cloud Security Posture Management tools (Wiz, Prisma Cloud, Orca, AWS Security Hub, Defender for Cloud) are good at the explicit cases: a public S3 bucket, an open security group, an unencrypted RDS instance. They are bad at the contextual cases: an IAM role with reasonable individual permissions that combine into a privilege escalation, a CloudTrail enabled in the wrong account, a KMS key that is technically configured correctly but managed by a person who left two years ago.
The 12 findings below are the contextual ones. The CSPM-detectable ones are a separate baseline — we run CSPM first, then do the human audit on top. Skipping the human audit because "the scanner came back green" is the most common pattern that leaves real risk in production.
Finding 01: Root account that is not actually offline
The AWS root account should have no access keys, MFA enforced, and a documented "break glass" rotation procedure. Most accounts we audit have at least one of: leftover root access keys from a 2019 provisioning script, root MFA disabled because someone needed to do a billing change last year, or no documented procedure for the day root is actually needed.
The root account compromise is the worst-case AWS scenario — full org takeover, all SCPs bypassable, all audit logs deletable. The fix is non-negotiable.
Detection
# Run from the management account
aws iam get-account-summary --query 'SummaryMap.{
RootMFA:AccountMFAEnabled,
RootKeys:AccountAccessKeysPresent,
RootCertificates:AccountSigningCertificatesPresent
}'
# Expected output: all zeros / false
# If RootKeys = 1 or RootMFA = 0, this is a finding
Fix that sticks
- Delete every root access key today. There is no legitimate reason to have them.
- Enforce hardware MFA on root (FIDO2 / YubiKey, not virtual MFA).
- Store the root password in a sealed envelope or a break-glass vault entry that requires two-person approval to unseal.
- Document the 8 actions that genuinely require root (billing changes, account closure, certain support-case operations) and pre-approve the procedure.
- Add a Config rule that pages an operator if root credentials are used.
Finding 02: CloudTrail in the wrong place
CloudTrail is supposed to be on, in every region, with logs delivered to an immutable S3 bucket in a separate account, with log file validation enabled. The pattern we find in 8 out of 10 audits: CloudTrail is on, but the logs land in the same account being audited, which means an attacker with admin in the account can delete them.
The architecture that holds up
Organization-level CloudTrail, multi-region, delivered to a dedicated "log archive" account, with the bucket policy denying deletion to every principal except the log archive's root (which itself has SCP guardrails). Log file validation enabled. Object Lock with compliance retention for an immutable last-write defence.
resource "aws_cloudtrail" "org" {
name = "org-trail"
s3_bucket_name = aws_s3_bucket.log_archive.id
include_global_service_events = true
is_multi_region_trail = true
is_organization_trail = true
enable_log_file_validation = true
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::"]
}
}
}
resource "aws_s3_bucket_object_lock_configuration" "log_archive" {
bucket = aws_s3_bucket.log_archive.id
rule {
default_retention {
mode = "COMPLIANCE"
days = 365
}
}
}
Finding 03: IAM roles with cross-account trust to "*"
Roles with "Principal": "*" in the trust policy are a recurring finding. Sometimes they were created during a vendor onboarding ("we'll lock it down later"), sometimes by an engineer who misunderstood the trust-policy semantics. Either way, any AWS account in the world can assume them with the right role name.
Detection via Access Analyzer
aws accessanalyzer create-analyzer \
--analyzer-name org-external-access \
--type ORGANIZATION
# Then query findings
aws accessanalyzer list-findings \
--analyzer-arn $ANALYZER_ARN \
--filter '{"isPublic":{"eq":["true"]}}' \
--query 'findings[*].{resource:resource,principal:principal,action:action}'
Access Analyzer is free and catches both the "trust *" case and the more subtle "trust unknown external account" case where the external account was once legitimate and is now defunct.
The fix template
Every cross-account trust should specify the external account ID, an External ID for further scoping, and the specific principal (role or user) that may assume. If you cannot enumerate "who from where", the trust is wrong-shaped and should be rebuilt with explicit principals.
Finding 04: Security groups with 0.0.0.0/0 on non-web ports
Port 80 and 443 from 0.0.0.0/0 are fine for public-facing load balancers. Port 22, 3389, 3306, 5432, 1433, 6379, 27017, 9200 from 0.0.0.0/0 are findings. Period.
We find these in three flavours: dev environments that "temporarily" opened SSH for a contractor; production environments where someone debugged a connectivity issue and forgot to close it; default VPC security groups that nobody noticed allowed all-traffic ingress.
Detection at scale
aws ec2 describe-security-groups --query '
SecurityGroups[*].{
GroupId:GroupId,
GroupName:GroupName,
BadRules:IpPermissions[?(IpRanges[?CidrIp==`0.0.0.0/0`] != null && (FromPort != `80` && FromPort != `443`))]
}
' --output table
The fix is closing the rules. The fix that sticks is a Service Control Policy at the organization level that denies the action of creating a security group rule with 0.0.0.0/0 on non-web ports, full stop. No human discipline survives 18 months; SCPs do.
Finding 05: S3 buckets without default encryption + public access blocks
Default encryption (SSE-S3 or SSE-KMS) is now the default on new buckets, but legacy buckets pre-2023 often lack it. Public access blocks are not the default at all and need to be explicitly enabled at account + bucket level.
The four settings that should be on every bucket
BlockPublicAcls: trueIgnorePublicAcls: trueBlockPublicPolicy: trueRestrictPublicBuckets: true
Plus default encryption with a KMS key (CMK preferred for regulated workloads, AWS-managed key for everything else). Plus versioning enabled. Plus a lifecycle policy that transitions to cheaper tiers and eventually expires non-current versions.
Finding 06: KMS keys with no key policy guard rails
The default KMS key policy grants kms:* to the account root. Combined with IAM users / roles in the account, this means a compromised principal can decrypt, schedule deletion, or change the policy of any CMK in the account.
The hardening pattern: key policies that name the specific roles allowed to encrypt and decrypt, with key administration restricted to a small set of "Key Admin" principals, and key deletion explicitly denied except via the dedicated workflow.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Allow encryption only by application role",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::ACCOUNT:role/app-role"},
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey*"],
"Resource": "*"
},
{
"Sid": "Deny key deletion by anyone except KeyAdmins",
"Effect": "Deny",
"Principal": "*",
"Action": ["kms:ScheduleKeyDeletion", "kms:DisableKey"],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::ACCOUNT:role/KeyAdmin"
}
}
}
]
}
Finding 07: GuardDuty disabled or single-region
GuardDuty is AWS's threat detection. It is cheap (cents per million events), covers VPC Flow Logs + DNS + CloudTrail + Kubernetes audit in one service, and is on by default in new accounts since 2024. We still find accounts where it is disabled — sometimes because the previous team turned it off "to reduce costs", sometimes because the account predates the default-on policy and was never updated.
The fix: enable GuardDuty in every region (yes, even regions you don't use — that's where the malicious activity tends to land first), with delegated administration to a security account, and findings forwarded to your SIEM or chat-ops channel.
What good detection looks like
GuardDuty findings of "High" severity should page someone. "Medium" should ticket. "Low" should aggregate weekly. The trap is enabling GuardDuty without wiring the findings anywhere, in which case the "detection" is detection-in-theory only.
Finding 08: EBS snapshots that are public
EBS snapshots can be made public. People do this accidentally — usually when sharing a snapshot with another account, the "public" option is one menu over from the "specific account" option. Public EBS snapshots are a known data-exfiltration vector.
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?contains(GroupNames,`all`)].{
SnapshotId:SnapshotId,
VolumeSize:VolumeSize,
StartTime:StartTime
}'
# Should return empty. If not, fix immediately.
Then add an EventBridge rule that fires on ModifySnapshotAttribute with CreateVolumePermission.Group: all and reverses the action automatically. The race condition matters — public snapshots get scraped within minutes of being made public.
Finding 09: Lambda functions with VPC misconfigured but world-readable env vars
Lambda environment variables look like configuration. They get treated like configuration. They are not configuration — they are visible to any principal with lambda:GetFunction, including audit-style roles that nobody thinks of as "secret-accessing".
Two findings often coexist: secrets in plaintext environment variables, and IAM roles that grant lambda:GetFunction broadly because "read-only is safe". Read-only is not safe when read-only includes the function configuration.
The fix pattern
- Encrypt environment variables with a customer-managed KMS key. Configure
encrypted = trueon the relevant fields. - Move all secrets to AWS Secrets Manager or SSM Parameter Store (SecureString). The Lambda fetches at cold-start.
- Tighten the IAM policies that grant
lambda:GetFunction— restrict to specific function ARNs, not*. - Add a Config rule:
lambda-function-settings-checkwith environment-variable-encryption requirements.
Finding 10: RDS without IAM auth + missing Performance Insights
Two adjacent findings we usually report together. RDS instances without IAM database authentication mean the team is managing database passwords through some out-of-band process (1Password, Vault, shared Slack channels). Performance Insights disabled means a slow-query problem cannot be diagnosed without SSHing into the database server (which we hope isn't possible because of Finding 04, but often is).
IAM database authentication
Replace database passwords with IAM-issued tokens that expire in 15 minutes. The application requests a token at connection time using its IAM role; the database verifies the token. No password rotation, no shared credentials, no Slack-leakable strings.
import { Signer } from "@aws-sdk/rds-signer";
const signer = new Signer({
region: "eu-central-1",
hostname: "db.example.internal",
port: 5432,
username: "app_role",
});
const token = await signer.getAuthToken();
// Use as the password when connecting; valid for 15 minutes
Finding 11: VPC endpoints absent for S3 + DynamoDB
Without VPC endpoints, traffic from private subnets to S3 or DynamoDB traverses the public internet via a NAT Gateway. This costs money (NAT egress is one of the most expensive AWS line items) and exposes the traffic to the public path.
VPC endpoints (Gateway type for S3 + DynamoDB, free; Interface type for other services, hourly cost) keep the traffic on AWS's private backbone and let you scope endpoint policies to specific buckets / tables.
The endpoint-policy trick most teams miss
The VPC endpoint can carry a policy that restricts which buckets the VPC can reach. This is the missing piece in many data-exfiltration defences:
{
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-org-data-bucket",
"arn:aws:s3:::my-org-data-bucket/*"
]
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringNotEqualsIfExists": {
"aws:ResourceOrgID": "o-xxxxxxxxxx"
}
}
}
]
}
Now compromise of an EC2 instance cannot exfiltrate to an external S3 bucket through the VPC endpoint — the endpoint policy refuses the traffic.
Finding 12: SCPs absent or too permissive
Service Control Policies are the organisation-level guardrails that no individual account can override. They are also the single most under-deployed control in AWS estates. The default ("FullAWSAccess") effectively means "all of AWS is in scope for every account".
The starter SCP set we deploy
- Deny region: block creation of resources outside EU regions (eu-west-1, eu-central-1, eu-west-2).
- Deny GuardDuty disable: nobody, including admins, can turn off GuardDuty in any account.
- Deny CloudTrail disable: ditto for the organization trail.
- Deny root usage: any action by the root principal is denied except in a documented emergency procedure.
- Deny SCP-bypass: deny modification of the SCPs themselves except by a dedicated security account.
- Deny IAM user creation: all human access must be SSO-federated; IAM users are denied as a class.
- Deny key deletion in <30 days: KMS keys cannot be scheduled for deletion with less than 30 days' notice.
- Deny S3 bucket public access: block changes to public-access-block settings.
Each SCP is short, surgical, and lives in version control alongside the landing-zone code. The combination changes the security posture of the entire estate without requiring per-account changes.
The remediation pipeline
Finding the 12 issues is the easy part. Fixing them in a way that survives 18 months is the operational discipline. The pattern we ship with every audit engagement:
- Capture findings in a prioritised backlog, ranked by exploitability × blast-radius.
- Fix the highest-priority items as PRs against the IaC repository, not via the console. Console fixes regress; Terraform fixes survive.
- Add a Config rule or SCP for each finding that prevents the issue from re-occurring.
- Add the finding to the security baseline documentation so the next engineer onboarding the platform knows why the control exists.
- Schedule a follow-up audit 6 months later to verify the fixes held and to catch the next dozen findings that have accumulated.
What CSPM tools are good for
The honest section. CSPM tools (Wiz, Prisma Cloud, Orca, Defender for Cloud, Security Hub) genuinely add value for:
- Continuous monitoring after the initial hardening. Posture drift gets caught faster than manual audits.
- Inventory at scale. Knowing what resources exist across hundreds of accounts is itself a problem CSPM solves well.
- Compliance reporting. The "we are 87% PCI-compliant" dashboards are useful for management even if the underlying signals are mixed.
- Cross-cloud visibility. If your estate spans AWS + Azure + GCP, the single pane of glass is worth real money.
What they are not good for: replacing a manual posture audit. They are complementary controls, not substitutes.
Real impact from a recent engagement
A 200-seat EU FinTech, multi-account AWS organisation, 4 years of accumulated estate. Our posture audit findings:
- Findings present from our 12-item list: 11 of 12
- Additional contextual findings outside the canonical 12: 17
- Findings flagged by their existing CSPM tool prior to engagement: 8
- Time to remediate the top 12 with IaC + SCPs: 3.5 weeks
- SCPs added to prevent regression: 9
- Config rules deployed for continuous detection: 22
The 6-month follow-up audit found 1 new finding (a security group opened during a vendor onboarding and not closed). The SCP guardrails prevented the other 11 from recurring entirely.
The one paragraph version
Every AWS account audit finds roughly the same dozen findings: root account not hardened, CloudTrail misconfigured, IAM trust policies too permissive, security groups too open, S3 bucket settings drifted, KMS keys without policy guardrails, GuardDuty single-region or off, public EBS snapshots, Lambda env-var secrets, RDS without IAM auth, missing VPC endpoints, SCPs absent. CSPM tools catch about half. The other half need a human with AWS scar tissue. The fix that sticks is IaC + Service Control Policies, not console patches, because human discipline does not survive 18 months but SCPs do.
If you want this audit run against your estate — fixed-fee, 5 business days, end-to-end report with a written remediation backlog — that is the engagement shape. The Bloodbath Audit is the productised version we sell at €890. The SCP + Config-rule pipeline that prevents regression is what Azure Cloud Infrastructure and Microsoft 365 Defender services include by default. Either way, the 12-item list above is the audit floor — your team can walk it tomorrow morning and start fixing what you find.