Lab: API Key Security
Learn common API key exposure vectors, secure key management with .env files, detect keys in git history, implement key rotation, and apply least-privilege principles.
Prerequisites
- Git installed and basic familiarity with git commands
- Python 3.10+ installed
- A test API key (use a free-tier or test key, never a production key for this lab)
pip install python-dotenvBackground
API keys are the credentials that authenticate your requests to LLM providers. In red teaming, you work with API keys for multiple providers, making key hygiene critical. A leaked key can result in unauthorized usage charges, data exposure, and loss of access to testing infrastructure.
Lab Exercises
Understand Common Exposure Vectors
Create a demonstration showing the most common ways API keys get exposed.
#!/usr/bin/env python3 """Demonstrate common API key exposure vectors. DO NOT use real keys.""" # === EXPOSURE VECTOR 1: Hardcoded in source code === # This is the most common mistake. The key is visible to anyone # who can read the source code, including in public repositories. OPENAI_API_KEY = "sk-fake-NEVER-DO-THIS-abc123" # BAD! # === EXPOSURE VECTOR 2: Logged in output === # Keys accidentally printed in logs, error messages, or debug output. import logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) def make_api_call_bad(key: str): """Accidentally logs the API key.""" logger.debug(f"Making API call with key: {key}") # BAD! # ... api call ... # === EXPOSURE VECTOR 3: In URL parameters === # Some APIs accept keys as URL parameters, which get logged by # web servers, proxies, and browser history. url = f"https://api.example.com/v1/chat?api_key={OPENAI_API_KEY}" # BAD! # === EXPOSURE VECTOR 4: Committed to git === # Even if deleted later, the key remains in git history forever. # See Step 3 for how to detect this. # === EXPOSURE VECTOR 5: In error messages returned to users === def handle_error_bad(error): """Exposes the key in the error response.""" return f"API call failed with key sk-fake-... Error: {error}" # BAD! # === EXPOSURE VECTOR 6: Shell history === # Running commands like: curl -H "Authorization: Bearer sk-real-key" ... # The key is saved in ~/.bash_history # === EXPOSURE VECTOR 7: Client-side code === # Including API keys in JavaScript, mobile apps, or any code # that runs on user devices where it can be extracted. if __name__ == "__main__": print("=== API Key Exposure Vectors ===\n") vectors = [ ("Hardcoded in source", "Keys visible in code repositories"), ("Logged in output", "Keys appear in log files and console output"), ("URL parameters", "Keys captured by proxies, servers, browser history"), ("Git history", "Keys persist in version control even after deletion"), ("Error messages", "Keys leaked through error responses to users"), ("Shell history", "Keys saved in ~/.bash_history or similar"), ("Client-side code", "Keys extractable from browser/mobile apps"), ] for name, desc in vectors: print(f" {name}: {desc}")python exposure_demo.pyImplement Secure Key Management
Set up proper key management using environment variables and
.envfiles.#!/usr/bin/env python3 """Demonstrate secure API key management patterns.""" import os import sys from pathlib import Path from dotenv import load_dotenv def setup_env_file(): """Create a properly configured .env file.""" env_path = Path(".env") if not env_path.exists(): env_content = """# API Keys - NEVER commit this file # Add your actual keys here OPENAI_API_KEY=sk-your-key-here ANTHROPIC_API_KEY=sk-ant-your-key-here OLLAMA_BASE_URL=http://localhost:11434/v1 """ env_path.write_text(env_content) print(f"Created {env_path} - add your keys there") # Ensure .gitignore exists and includes .env gitignore = Path(".gitignore") gitignore_content = gitignore.read_text() if gitignore.exists() else "" if ".env" not in gitignore_content: with open(gitignore, "a") as f: f.write("\n# Secret files\n.env\n.env.*\n*.key\n") print("Updated .gitignore to exclude .env files") def get_api_key(key_name: str) -> str: """Securely retrieve an API key from environment variables.""" load_dotenv() # Load from .env file key = os.getenv(key_name) if not key: print(f"Warning: {key_name} not set in environment or .env file") sys.exit(1) # Validate key format (basic check) if key_name == "OPENAI_API_KEY" and not key.startswith("sk-"): print(f"Warning: {key_name} does not look like a valid OpenAI key") return key def safe_log_key(key: str) -> str: """Return a redacted version of a key for logging.""" if len(key) < 8: return "***" return f"{key[:4]}...{key[-4:]}" if __name__ == "__main__": setup_env_file() # Demonstrate secure retrieval load_dotenv() key = os.getenv("OPENAI_API_KEY", "sk-test-placeholder") print(f"\n=== Secure Key Management ===") print(f"Key retrieved: {safe_log_key(key)}") print(f"Key length: {len(key)} characters") print(f"Never log the full key!")python secure_key_management.pyDetect Keys in Git History
Learn to scan for accidentally committed keys using automated tools.
#!/usr/bin/env python3 """Scan git history for leaked API keys.""" import re import subprocess import os # Common API key patterns KEY_PATTERNS = { "OpenAI": r'sk-[a-zA-Z0-9]{20,}', "Anthropic": r'sk-ant-[a-zA-Z0-9\-]{20,}', "AWS Access Key": r'AKIA[0-9A-Z]{16}', "GitHub Token": r'ghp_[a-zA-Z0-9]{36}', "Generic Secret": r'(?i)(api[_-]?key|secret|token|password)\s*[=:]\s*["\']?[a-zA-Z0-9\-_.]{20,}', "Bearer Token": r'Bearer\s+[a-zA-Z0-9\-_.]+', } def scan_file(filepath: str) -> list[dict]: """Scan a single file for potential API keys.""" findings = [] try: with open(filepath, "r", errors="ignore") as f: for line_num, line in enumerate(f, 1): for key_type, pattern in KEY_PATTERNS.items(): matches = re.findall(pattern, line) for match in matches: findings.append({ "file": filepath, "line": line_num, "type": key_type, "match": match[:20] + "..." if len(match) > 20 else match, }) except (IOError, UnicodeDecodeError): pass return findings def scan_git_history() -> list[dict]: """Scan git log for potential API keys in diffs.""" findings = [] try: result = subprocess.run( ["git", "log", "--all", "-p", "--max-count=50"], capture_output=True, text=True, timeout=30, ) except (subprocess.TimeoutExpired, FileNotFoundError): print("Git not available or timed out") return findings for line_num, line in enumerate(result.stdout.split("\n"), 1): if line.startswith("+") and not line.startswith("+++"): for key_type, pattern in KEY_PATTERNS.items(): matches = re.findall(pattern, line) for match in matches: findings.append({ "source": "git_history", "line": line_num, "type": key_type, "match": match[:20] + "..." if len(match) > 20 else match, }) return findings def scan_directory(directory: str = ".") -> list[dict]: """Scan all files in a directory for potential API keys.""" findings = [] skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv"} for root, dirs, files in os.walk(directory): dirs[:] = [d for d in dirs if d not in skip_dirs] for filename in files: if filename.endswith((".py", ".js", ".ts", ".env", ".yaml", ".yml", ".json", ".toml", ".cfg", ".ini", ".sh")): filepath = os.path.join(root, filename) findings.extend(scan_file(filepath)) return findings if __name__ == "__main__": print("=== API Key Scanner ===\n") # Scan current directory print("Scanning current directory...") dir_findings = scan_directory(".") if dir_findings: print(f"\nFound {len(dir_findings)} potential keys in files:") for f in dir_findings: print(f" [{f['type']}] {f['file']}:{f['line']} - {f['match']}") else: print(" No keys found in current files.") # Scan git history print("\nScanning git history (last 50 commits)...") git_findings = scan_git_history() if git_findings: print(f"\nFound {len(git_findings)} potential keys in git history:") for f in git_findings[:10]: # Show first 10 print(f" [{f['type']}] line {f['line']} - {f['match']}") else: print(" No keys found in git history.") total = len(dir_findings) + len(git_findings) print(f"\n=== Total findings: {total} ===") if total > 0: print("ACTION REQUIRED: Review and rotate any real keys found above.")python git_key_scanner.pySet Up Pre-Commit Key Detection
Prevent keys from being committed in the first place using a pre-commit hook.
#!/usr/bin/env python3 """Install a pre-commit hook that detects API keys.""" import os import stat from pathlib import Path HOOK_SCRIPT = '''#!/bin/bash # Pre-commit hook: detect potential API key leaks echo "Checking for API key leaks..." # Patterns to check for PATTERNS=( 'sk-[a-zA-Z0-9]{20,}' 'sk-ant-[a-zA-Z0-9-]{20,}' 'AKIA[0-9A-Z]{16}' 'ghp_[a-zA-Z0-9]{36}' ) FOUND=0 for pattern in "${PATTERNS[@]}"; do # Check staged files only MATCHES=$(git diff --cached --diff-filter=ACMR -U0 | grep -P "^\\+" | grep -oP "$pattern" 2>/dev/null) if [ -n "$MATCHES" ]; then echo "BLOCKED: Potential API key detected matching pattern: $pattern" echo "$MATCHES" FOUND=1 fi done # Also check for common .env files being committed ENV_FILES=$(git diff --cached --name-only | grep -E '^\\.env') if [ -n "$ENV_FILES" ]; then echo "BLOCKED: .env file staged for commit: $ENV_FILES" FOUND=1 fi if [ $FOUND -eq 1 ]; then echo "" echo "Commit blocked. Remove secrets before committing." echo "Use 'git commit --no-verify' to bypass (NOT recommended)." exit 1 fi echo "No API keys detected. Proceeding with commit." exit 0 ''' def install_hook(): """Install the pre-commit hook.""" hooks_dir = Path(".git/hooks") if not hooks_dir.exists(): print("Not a git repository. Initialize with 'git init' first.") return hook_path = hooks_dir / "pre-commit" if hook_path.exists(): print(f"Pre-commit hook already exists at {hook_path}") response = input("Overwrite? (y/n): ").strip().lower() if response != "y": return hook_path.write_text(HOOK_SCRIPT) hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC) print(f"Pre-commit hook installed at {hook_path}") if __name__ == "__main__": install_hook()python install_hook.pyTest the hook by trying to commit a file with a fake key:
echo 'API_KEY = "sk-test1234567890abcdefghijklmnop"' > test_leak.py git add test_leak.py git commit -m "test commit" # Should be blocked by the pre-commit hookKey Rotation and Least Privilege
Practice key rotation and understand how to apply least-privilege principles.
#!/usr/bin/env python3 """Key rotation checklist and least-privilege API key guide.""" def print_rotation_checklist(): """When and how to rotate API keys.""" print("=== API Key Rotation Checklist ===\n") triggers = [ "Key was accidentally committed to a public repository", "Key appeared in logs, error messages, or monitoring dashboards", "Team member with key access left the organization", "Key has not been rotated in 90 days (or per your policy)", "Unusual API usage detected (unexpected charges, unknown IPs)", "Key was shared via insecure channel (email, Slack, etc.)", "Security incident or breach suspected", ] print("Rotate keys when:") for t in triggers: print(f" - {t}") steps = [ "Generate a new key from the provider dashboard", "Update the key in your secrets management system / .env files", "Verify the new key works in all environments", "Revoke (not just delete) the old key", "Verify the old key no longer works", "Update any documentation referencing the old key", "Log the rotation event with timestamp and reason", ] print("\nRotation steps:") for i, s in enumerate(steps, 1): print(f" {i}. {s}") def print_least_privilege_guide(): """Guide for applying least-privilege to API keys.""" print("\n=== Least-Privilege API Key Guide ===\n") principles = [ { "name": "Scope keys to specific projects", "example": "Create separate keys for red-team-testing vs production-monitoring", }, { "name": "Use read-only keys where possible", "example": "For response analysis, you only need read access, not model training", }, { "name": "Set spending limits per key", "example": "Cap test keys at $10/day to prevent runaway costs from automated tests", }, { "name": "Restrict IP allowlists", "example": "Limit API keys to your testing lab's IP range", }, { "name": "Use short-lived tokens when available", "example": "Prefer OAuth tokens that expire over permanent API keys", }, { "name": "Separate keys by environment", "example": "Different keys for dev, staging, and production", }, ] for p in principles: print(f" {p['name']}") print(f" Example: {p['example']}\n") if __name__ == "__main__": print_rotation_checklist() print_least_privilege_guide()python key_rotation.py
Troubleshooting
| Issue | Solution |
|---|---|
| Pre-commit hook not running | Ensure the hook file is executable: chmod +x .git/hooks/pre-commit |
| Scanner finds false positives | Add false-positive patterns to an ignore list in the scanner |
.env file not loading | Ensure python-dotenv is installed and load_dotenv() is called before os.getenv() |
| Key already leaked publicly | Immediately revoke the key, rotate all related credentials, and audit usage logs |
Related Topics
- Environment Setup - Initial environment setup including key configuration
- Anthropic Claude API Basics - Using API keys securely with Claude
- Security Logging - Logging API key usage for audit trails
- Input Sanitization - Preventing key leakage through input validation
References
- "OWASP API Security Top 10" - OWASP (2025) - Industry standard for API security including authentication best practices
- "GitHub Secret Scanning" - GitHub (2025) - GitHub's built-in tool for detecting committed secrets
- "Trufflehog: Find and Verify Credentials" - Truffle Security (2025) - Open-source tool for comprehensive secret scanning
- "API Key Best Practices" - Google Cloud (2025) - Provider guidance on secure API key management
Why is deleting a leaked key from your source code not sufficient to fix the exposure?
Which least-privilege practice is most important for red team API keys?