We ran cloud security audits for twelve organizations in 2025. AWS, GCP, Azure, mostly mid-sized SaaS. The findings reports could be substituted across clients without anyone noticing. Same five issues, different scale.
This is the report we keep writing, condensed. If your cloud environment has the same patterns, the fixes are below.
Service accounts with administrative roles
The pattern: an automation account (CI/CD deploy, Terraform service, monitoring system) was given the AdministratorAccess policy "temporarily" during initial setup, never narrowed. Years later, the CI/CD account can do anything in the org.
The blast radius: compromise of the CI/CD pipeline becomes compromise of the entire cloud org. We have demonstrated this path in eight of twelve engagements. The CI/CD compromise itself is often easy (see the previous article in this series).
The fix: scope service-account roles to the resources they actually touch. AWS Access Analyzer's "Generate Policy" feature builds the least-privilege policy from CloudTrail history. Run it against your top-10 most-privileged service accounts, every quarter, narrow as you go.
The fix that does not work: rotating credentials on the over-privileged account. Rotation does not reduce blast radius. The new credential has the same scope.
Cross-account assume-role chains
The pattern: multi-account AWS setups (one per environment) have a shared "tooling" account with admin roles in each environment account. The intent: a centralized place for monitoring, audit, and break-glass access. The reality: the tooling account becomes the single most valuable target in the org.
The blast radius: compromise of the tooling account is compromise of everything it can assume into. Often dev, staging, and prod. Sometimes also the corporate AD account if SSO is wired through.
The fix: split the tooling account. Read-only audit account that can read but cannot write. Separate break-glass account with strong MFA and out-of-band approval. Separate CI/CD account scoped to specific repos and resources. The "do everything everywhere" account should not exist.
Public S3 / GCS / Blob buckets that nobody owns
The pattern: a bucket was made public in 2018 for a deprecated marketing site. The site is gone. The bucket is still public, still has files, still has the public read policy. Often nobody knows who created it.
The blast radius: varies by what is in the bucket. We have found everything from public website assets (low impact) to engineering meeting recordings (high impact) to database backups (catastrophic).
The fix: AWS S3 Block Public Access at the account level, exceptions per bucket with documented owner. Same for GCS and Azure. Inventory existing public buckets quarterly. Tag ownership; un-owned buckets get a 30-day notice then locked.
The harder fix: the cultural one. Teams should not be able to make buckets public without a security review. Many orgs have this control in cloud config; few enforce it organizationally.
Metadata service exposure via SSRF
The pattern: an application has a server-side request forgery vulnerability. The application runs on EC2 or GCE or Azure VMs. The cloud metadata service is reachable from the instance.
The blast radius: SSRF becomes credential theft. The IAM role attached to the instance is now in the attacker's hands. Often that role has access to S3 buckets, RDS databases, or further assume-role chains.
The fix has two parts. First, IMDSv2 (AWS) or equivalent on GCP and Azure. IMDSv2 requires the attacker's SSRF payload to handle two-step token exchange, which most SSRF primitives cannot do. Second, scope the instance role to the minimum necessary; most workloads do not need broad S3 access.
The fix that does not work: blocking the metadata service IP (169.254.169.254) at the application layer. Attackers find DNS rebinding bypasses, or use cloud-specific aliases. IMDSv2 is the structural fix.
Control-plane logging gaps
The pattern: CloudTrail is on for the org but data events (S3 object access, Lambda invocations) are not. Or Cloud Audit Logs is on for Admin Activity but Data Access events are not. Or logs are flowing but not centralized; each account writes to its own region.
The blast radius: incidents are unsolvable. An attacker accessing customer data via a compromised IAM role leaves a CloudTrail event the security team cannot see. The incident response team learns that the necessary logs were never captured.
The fix: enable data events for sensitive resources. Centralize log storage to a separate audit account with write-only access from source accounts. Quarterly verify that you can reconstruct a specific action (e.g. "show me every read of bucket X by user Y in the last 90 days"). If the answer is "we cannot", that is your gap.
This is the finding that produces the most pushback. Data events generate significant log volume; teams worry about cost. Run the cost analysis with the cloud provider tooling. For most orgs, data events for the top-50 sensitive resources costs less than the security team thinks.
What we do not find as often as you would expect
Findings that vendor dashboards highlight but that produce less impact in practice:
- Open security groups. Cloud-native services usually need exposure. The actual finding is "exposed without auth", which requires deeper analysis than a port scan.
- Encryption at rest disabled. Compliance finding, not security finding. The attacker who reaches your data has bypassed encryption-at-rest anyway.
- Untagged resources. Operational finding. Useful for cost tracking, not for security posture.
This is not to say these are wrong; they are just lower priority than the five above. Vendor dashboards highlight them because they are easy to detect. The five we listed are harder to detect because they require understanding intent, not just configuration.
If you only do one thing this quarter
Pick your most-privileged service account. Run the access analysis tooling (Access Analyzer, IAM Recommender, Azure PIM analysis). Narrow the role to what the account actually used in the last 90 days.
You will be uncomfortable doing it the first time. The fix that does not break production is the fix that does not happen. Schedule it in a maintenance window. Roll it back if something breaks. Iterate.
"We had been adding least-privilege controls for new accounts for two years. We had never gone back to fix the legacy accounts. The audit found that the legacy ones had grown more permissions, not fewer." — Cloud Security Lead at a US healthcare client, debrief October 2025
The pattern across these findings
Each of the five issues is a decision that aged poorly. Not a configuration that was wrong at the time it was made. Cloud security maturity is mostly about going back to old decisions and asking whether they are still the right decisions.
The audit you book is useful because it forces that revisit. The dashboard you check daily does not. That is why audits keep finding the same things.