cloud security

Detecting AWS Credential Exposure with TruffleHog

Detecting exposed AWS credentials in source code repositories, CI/CD pipelines, and configuration files using TruffleHog, git-secrets, and AWS-native detection mechanisms to prevent credential theft and unauthorized account access.

awscloud-securitycredential-exposuredevsecopssecrets-detectiontrufflehog
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When integrating secrets detection into CI/CD pipelines to prevent credential commits reaching production
  • When performing a security audit of existing repositories for historically committed AWS credentials
  • When responding to an AWS GuardDuty alert about credential usage from an unexpected IP or region
  • When onboarding repositories from acquired companies or third-party vendors
  • When validating that credential rotation processes have removed all references to old access keys

Do not use for real-time credential monitoring (use AWS GuardDuty or Amazon Macie), for managing secrets (use AWS Secrets Manager or HashiCorp Vault), or for detecting non-credential sensitive data like PII (use Amazon Macie or DLP tools).

Prerequisites

  • TruffleHog v3 installed (brew install trufflehog or pip install trufflehog)
  • git-secrets installed for pre-commit hook integration (brew install git-secrets)
  • Access to source code repositories (GitHub, GitLab, Bitbucket, or local git repos)
  • AWS CLI configured with permissions to check key status (iam:ListAccessKeys, iam:GetAccessKeyLastUsed)
  • GitHub or GitLab API token for scanning organization-wide repositories

Workflow

Step 1: Install and Configure TruffleHog

Install TruffleHog v3 and verify it can detect the AWS credential patterns.

# Install TruffleHog v3
pip install trufflehog
 
# Or install from binary release
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
 
# Verify installation
trufflehog --version
 
# Test with a known test repository
trufflehog git https://github.com/trufflesecurity/test_keys --only-verified

Step 2: Scan Git Repositories for Exposed Credentials

Scan entire git history including all branches and commits for AWS access keys, secret keys, and session tokens.

# Scan a local git repository (full history)
trufflehog git file:///path/to/repo --only-verified --json > trufflehog-results.json
 
# Scan a GitHub organization's repositories
trufflehog github --org=your-organization --token=$GITHUB_TOKEN --only-verified
 
# Scan a specific GitHub repository with all branches
trufflehog git https://github.com/org/repo.git --only-verified --branch=main
 
# Scan a GitLab group
trufflehog gitlab --group=your-group --token=$GITLAB_TOKEN --only-verified
 
# Scan filesystem paths for credentials in config files
trufflehog filesystem /path/to/project --only-verified

Step 3: Analyze and Validate Detected Credentials

Parse TruffleHog results to identify verified (still-active) credentials versus rotated or test keys.

# Parse TruffleHog JSON output for AWS findings
cat trufflehog-results.json | python3 -c "
import json, sys
for line in sys.stdin:
    finding = json.loads(line)
    if 'AWS' in finding.get('DetectorName', ''):
        print(f\"Detector: {finding['DetectorName']}\")
        print(f\"Verified: {finding.get('Verified', False)}\")
        print(f\"Source: {finding.get('SourceMetadata', {})}\")
        print(f\"Commit: {finding.get('SourceMetadata', {}).get('Data', {}).get('Git', {}).get('commit', 'N/A')}\")
        print(f\"File: {finding.get('SourceMetadata', {}).get('Data', {}).get('Git', {}).get('file', 'N/A')}\")
        print('---')
"
 
# Check if a detected access key is still active
aws iam get-access-key-last-used --access-key-id AKIAIOSFODNN7EXAMPLE
 
# List all access keys for a user to find active keys
aws iam list-access-keys --user-name target-user \
  --query 'AccessKeyMetadata[*].[AccessKeyId,Status,CreateDate]' --output table

Step 4: Set Up Pre-Commit Hooks with git-secrets

Prevent credentials from being committed in the first place using git-secrets as a pre-commit hook.

# Install git-secrets
git secrets --install  # In each repository
 
# Register AWS credential patterns
git secrets --register-aws
 
# Add custom patterns for internal credential formats
git secrets --add 'AKIA[0-9A-Z]{16}'
git secrets --add 'aws_secret_access_key\s*=\s*.{40}'
git secrets --add 'aws_session_token\s*=\s*.+'
 
# Scan entire repository history
git secrets --scan-history
 
# Add to global git template for all new repos
git secrets --install ~/.git-templates/git-secrets
git config --global init.templateDir ~/.git-templates/git-secrets

Step 5: Integrate TruffleHog into CI/CD Pipeline

Add TruffleHog scanning as a CI/CD gate to block deployments containing exposed credentials.

# GitHub Actions workflow (.github/workflows/secrets-scan.yml)
name: Secrets Scan
on: [push, pull_request]
 
jobs:
  trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: TruffleHog Scan
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified --results=verified
# GitLab CI (.gitlab-ci.yml)
secrets_scan:
  stage: test
  image: trufflesecurity/trufflehog:latest
  script:
    - trufflehog git file://$CI_PROJECT_DIR --since-commit $CI_COMMIT_BEFORE_SHA --only-verified --fail
  allow_failure: false

Step 6: Respond to Detected Credential Exposure

Execute incident response procedures when verified credentials are found exposed.

# IMMEDIATE: Deactivate the exposed access key
aws iam update-access-key \
  --user-name compromised-user \
  --access-key-id AKIAEXPOSEDKEY123456 \
  --status Inactive
 
# Generate new credentials
aws iam create-access-key --user-name compromised-user
 
# Review CloudTrail for unauthorized usage of the exposed key
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXPOSEDKEY123456 \
  --start-time 2026-01-01T00:00:00Z \
  --query 'Events[*].[EventTime,EventName,EventSource,SourceIPAddress]' \
  --output table
 
# Delete the exposed key after rotation is confirmed
aws iam delete-access-key \
  --user-name compromised-user \
  --access-key-id AKIAEXPOSEDKEY123456
 
# Remove the credential from git history using BFG Repo Cleaner
java -jar bfg.jar --replace-text credentials.txt repo.git

Key Concepts

Term Definition
TruffleHog Open-source secrets detection tool that scans git history, filesystems, and cloud services for exposed credentials using regex patterns and verification APIs
Verified Secret A credential that TruffleHog has confirmed is still active by making an API call to the target service (e.g., AWS STS GetCallerIdentity)
git-secrets AWS Labs pre-commit hook tool that prevents committing strings matching AWS credential patterns to git repositories
Access Key Rotation The practice of regularly replacing AWS access key pairs to limit the window of exposure if a key is compromised
BFG Repo Cleaner Tool for removing sensitive data from git history without rewriting the entire repository, faster than git filter-branch
GitHub Secret Scanning GitHub-native feature that scans public repositories for known credential patterns and notifies the credential provider

Tools & Systems

  • TruffleHog v3: Primary scanning engine supporting git, filesystem, S3, and CI/CD integration with verified credential detection
  • git-secrets: AWS Labs pre-commit hook for preventing credential commits at the developer workstation level
  • BFG Repo Cleaner: Fast tool for removing credentials from git history after exposure is detected
  • AWS GuardDuty: Threat detection service that alerts on anomalous usage of AWS credentials from unexpected locations
  • GitHub Advanced Security: Platform-native secret scanning for GitHub repositories with push protection

Common Scenarios

Scenario: Developer Commits AWS Credentials to a Public GitHub Repository

Context: GitHub secret scanning notifies that an AWS access key was pushed to a public repository. The key belongs to a developer with production S3 and DynamoDB access.

Approach:

  1. Immediately deactivate the access key using aws iam update-access-key --status Inactive
  2. Run aws cloudtrail lookup-events filtering by the exposed AccessKeyId to check for unauthorized usage
  3. Scan the full repository history with trufflehog git to find any other exposed credentials
  4. Generate a new access key for the developer and deliver it through Secrets Manager
  5. Remove the credential from git history using BFG Repo Cleaner
  6. Install git-secrets pre-commit hook on the developer's workstation
  7. Add TruffleHog to the repository's CI/CD pipeline to prevent recurrence

Pitfalls: Simply deleting the commit or force-pushing does not remove credentials from GitHub's cache or forks. The key must be deactivated at the AWS level immediately. GitHub secret scanning may have already notified AWS, triggering automated key deactivation.

Output Format

AWS Credential Exposure Scan Report
======================================
Scan Target: github.com/acme-corp (42 repositories)
Scan Date: 2026-02-23
Tool: TruffleHog v3.63.0
Mode: Full git history scan with verification
 
VERIFIED FINDINGS (Active Credentials):
[CRED-001] AWS Access Key - VERIFIED ACTIVE
  Key ID: AKIA...WXYZ
  Repository: acme-corp/backend-api
  File: deploy/config.env
  Commit: a1b2c3d (2025-08-15)
  Author: developer@acme.com
  IAM User: svc-backend-deploy
  Permissions: S3, DynamoDB, SQS (production)
  Status: CRITICAL - Key active and used from 3 IP addresses
  Action Required: Immediate deactivation and rotation
 
[CRED-002] AWS Secret Key - VERIFIED ACTIVE
  Repository: acme-corp/data-pipeline
  File: scripts/etl_config.py
  Commit: d4e5f6g (2025-11-22)
  Author: data-engineer@acme.com
  Status: HIGH - Key active, last used 2 days ago
 
UNVERIFIED FINDINGS (Potential Credentials):
  Total pattern matches: 15
  Likely test/example keys: 12
  Requires manual review: 3
 
SUMMARY:
  Repositories scanned: 42
  Commits analyzed: 125,847
  Verified active credentials: 2
  Unverified credential patterns: 15
  Repositories with pre-commit hooks: 8 / 42
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.1 KB

AWS Credential Exposure Detection API Reference

TruffleHog CLI

# Scan local git repo
trufflehog git file:///path/to/repo --json
 
# Scan GitHub organization
trufflehog github --org my-org --token $GITHUB_TOKEN --json
 
# Scan GitHub repo
trufflehog git https://github.com/org/repo.git --json
 
# Scan filesystem (non-git)
trufflehog filesystem /path/to/dir --json
 
# Scan S3 bucket
trufflehog s3 --bucket my-bucket --json
 
# Only show verified credentials
trufflehog git file:///repo --only-verified --json

git-secrets CLI

# Install hooks in repo
git secrets --install
git secrets --register-aws
 
# Scan repo history
git secrets --scan-history
 
# Scan specific file
git secrets --scan /path/to/file
 
# Add custom pattern
git secrets --add 'PRIVATE KEY'
git secrets --add-provider -- cat /path/to/patterns.txt

AWS IAM CLI - Key Management

# Check key last used
aws iam get-access-key-last-used --access-key-id AKIAXXXXXXXXXXXXXXXX
 
# List access keys for user
aws iam list-access-keys --user-name jsmith
 
# Deactivate exposed key
aws iam update-access-key --access-key-id AKIAXXXXXXXXXXXXXXXX \
  --user-name jsmith --status Inactive
 
# Delete key
aws iam delete-access-key --access-key-id AKIAXXXXXXXXXXXXXXXX \
  --user-name jsmith
 
# Create new key (after rotation)
aws iam create-access-key --user-name jsmith

AWS Access Key Regex Patterns

Access Key ID:  AKIA[A-Z0-9]{16}
Temp Key ID:    ASIA[A-Z0-9]{16}
Secret Key:     [A-Za-z0-9/+=]{40}

GuardDuty Credential Findings

Finding Type Description
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS Instance creds used outside AWS
UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B Console login from unusual IP
Recon:IAMUser/MaliciousIPCaller.Custom API calls from threat intel IPs

AWS CloudTrail - Credential Abuse Queries (Athena)

SELECT eventtime, useridentity.accesskeyid, sourceipaddress, eventname
FROM cloudtrail_logs
WHERE useridentity.accesskeyid = 'AKIAXXXXXXXXXXXXXXXX'
ORDER BY eventtime DESC
LIMIT 100;

Scripts 1

agent.py7.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""AWS credential exposure detection agent using TruffleHog and AWS APIs."""

import json
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path


AWS_KEY_PATTERN = re.compile(r'(?:AKIA|ASIA)[A-Z0-9]{16}')
AWS_SECRET_PATTERN = re.compile(r'(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])')


def scan_repo_trufflehog(repo_path, json_output=True):
    """Run TruffleHog against a local git repository."""
    cmd = ["trufflehog", "git", f"file://{repo_path}", "--json"]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        findings = []
        for line in result.stdout.strip().splitlines():
            if not line:
                continue
            try:
                finding = json.loads(line)
                findings.append({
                    "detector": finding.get("DetectorName", finding.get("SourceMetadata", {}).get("DetectorName")),
                    "verified": finding.get("Verified", False),
                    "raw": finding.get("Raw", "")[:50] + "..." if finding.get("Raw") else None,
                    "source": finding.get("SourceMetadata", {}),
                })
            except json.JSONDecodeError:
                continue
        return {"scan_type": "git_repo", "path": repo_path, "findings_count": len(findings), "findings": findings}
    except FileNotFoundError:
        return {"error": "TruffleHog not installed. Install with: pip install trufflehog"}
    except subprocess.TimeoutExpired:
        return {"error": "Scan timed out after 300s"}


def scan_github_org(org_name):
    """Scan an entire GitHub organization with TruffleHog."""
    cmd = ["trufflehog", "github", "--org", org_name, "--json"]
    token = os.environ.get("GITHUB_TOKEN")
    if token:
        cmd.extend(["--token", token])
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
        findings = []
        for line in result.stdout.strip().splitlines():
            try:
                findings.append(json.loads(line))
            except json.JSONDecodeError:
                continue
        return {"scan_type": "github_org", "org": org_name, "findings_count": len(findings), "findings": findings[:50]}
    except FileNotFoundError:
        return {"error": "TruffleHog not installed"}
    except subprocess.TimeoutExpired:
        return {"error": "Scan timed out after 600s"}


def scan_file_for_aws_keys(file_path):
    """Scan a single file for AWS access key patterns."""
    try:
        with open(file_path, "r", errors="replace") as f:
            content = f.read()
    except Exception as e:
        return {"error": str(e)}

    access_keys = AWS_KEY_PATTERN.findall(content)
    secrets = AWS_SECRET_PATTERN.findall(content)

    return {
        "file": file_path,
        "access_keys_found": len(access_keys),
        "potential_secrets_found": len(secrets),
        "access_keys": access_keys,
    }


def scan_directory_recursive(directory):
    """Scan all files in a directory for AWS credentials."""
    findings = []
    skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv", ".tox"}
    skip_extensions = {".pyc", ".so", ".dll", ".exe", ".bin", ".jpg", ".png", ".gif", ".zip", ".tar", ".gz"}

    for root, dirs, files in os.walk(directory):
        dirs[:] = [d for d in dirs if d not in skip_dirs]
        for fname in files:
            if Path(fname).suffix.lower() in skip_extensions:
                continue
            fpath = os.path.join(root, fname)
            try:
                with open(fpath, "r", errors="replace") as f:
                    content = f.read(1024 * 1024)
                keys = AWS_KEY_PATTERN.findall(content)
                if keys:
                    findings.append({
                        "file": fpath,
                        "access_keys": keys,
                        "line_numbers": [
                            i + 1 for i, line in enumerate(content.splitlines())
                            if AWS_KEY_PATTERN.search(line)
                        ],
                    })
            except Exception:
                continue

    return {"directory": directory, "files_with_keys": len(findings), "findings": findings}


def check_aws_key_status(access_key_id):
    """Check if an AWS access key is active using AWS CLI."""
    cmd = [
        "aws", "iam", "get-access-key-last-used",
        "--access-key-id", access_key_id,
        "--output", "json",
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
        if result.returncode == 0:
            data = json.loads(result.stdout)
            return {
                "access_key": access_key_id,
                "status": "active",
                "username": data.get("UserName", "unknown"),
                "last_used": data.get("AccessKeyLastUsed", {}),
            }
        return {"access_key": access_key_id, "status": "error", "message": result.stderr.strip()}
    except Exception as e:
        return {"access_key": access_key_id, "status": "error", "message": str(e)}


def deactivate_exposed_key(access_key_id, username):
    """Deactivate an exposed AWS access key."""
    cmd = [
        "aws", "iam", "update-access-key",
        "--access-key-id", access_key_id,
        "--user-name", username,
        "--status", "Inactive",
    ]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
        return {
            "action": "deactivate_key",
            "access_key": access_key_id,
            "success": result.returncode == 0,
            "error": result.stderr.strip() if result.returncode != 0 else None,
            "timestamp": datetime.utcnow().isoformat() + "Z",
        }
    except Exception as e:
        return {"action": "deactivate_key", "success": False, "error": str(e)}


def setup_git_secrets():
    """Install git-secrets hooks for the current repository."""
    commands = [
        ["git", "secrets", "--install"],
        ["git", "secrets", "--register-aws"],
    ]
    results = []
    for cmd in commands:
        try:
            r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
            results.append({"cmd": " ".join(cmd), "success": r.returncode == 0, "output": r.stdout.strip()})
        except Exception as e:
            results.append({"cmd": " ".join(cmd), "success": False, "error": str(e)})
    return results


def generate_report(target):
    """Generate a credential exposure scan report."""
    if os.path.isdir(os.path.join(target, ".git")):
        scan = scan_repo_trufflehog(target)
    elif os.path.isdir(target):
        scan = scan_directory_recursive(target)
    else:
        scan = scan_file_for_aws_keys(target)

    return {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "target": target,
        "scan_results": scan,
    }


if __name__ == "__main__":
    action = sys.argv[1] if len(sys.argv) > 1 else "help"
    if action == "scan" and len(sys.argv) > 2:
        print(json.dumps(generate_report(sys.argv[2]), indent=2, default=str))
    elif action == "scan-org" and len(sys.argv) > 2:
        print(json.dumps(scan_github_org(sys.argv[2]), indent=2, default=str))
    elif action == "check-key" and len(sys.argv) > 2:
        print(json.dumps(check_aws_key_status(sys.argv[2]), indent=2))
    elif action == "deactivate" and len(sys.argv) > 3:
        print(json.dumps(deactivate_exposed_key(sys.argv[2], sys.argv[3]), indent=2))
    elif action == "git-secrets":
        print(json.dumps(setup_git_secrets(), indent=2))
    else:
        print("Usage: agent.py [scan <path>|scan-org <org>|check-key <key_id>|deactivate <key_id> <user>|git-secrets]")
Keep exploring