API Key Management
Secure the lifecycle of API keys from generation to revocation. Covers key generation best practices, rotation policies, scope limiting, usage monitoring, and the patterns that prevent API key compromise from becoming a security incident.
API keys are the most common authentication mechanism for service-to-service communication and developer APIs. They are also the most commonly leaked credential — committed to public GitHub repos, embedded in client-side code, shared in Slack messages, and stored in plain text configuration files. API key management is the discipline of generating, distributing, monitoring, and revoking keys securely.
Key Lifecycle
API Key Lifecycle:
1. Generation:
✓ Cryptographically random (32+ bytes, base64url encoded)
✓ Include prefix for identification: sk_live_xxx, pk_test_xxx
✓ Hash before storing (bcrypt/argon2) — never store plaintext
✗ Do NOT use sequential IDs, timestamps, or predictable patterns
2. Distribution:
✓ Show key exactly once at creation time
✓ Deliver via secure channel (not email, not Slack)
✓ Environment variables or secret manager
✗ Never embed in source code
3. Scoping:
✓ Minimum permissions required
✓ Rate limits per key
✓ IP allowlisting where possible
✓ Environment separation (test vs. production)
4. Monitoring:
✓ Log all key usage (IP, endpoint, timestamp)
✓ Alert on anomalous usage patterns
✓ Track unused keys (> 90 days unused = candidate for revocation)
5. Rotation:
✓ Scheduled rotation (90 days recommended)
✓ Support overlapping validity (old key works during transition)
✓ Automated rotation with secret manager integration
6. Revocation:
✓ Immediate revocation capability
✓ Revoke on suspected compromise
✓ Audit trail of revocation reason and timestamp
Implementation
import secrets
import hashlib
from datetime import datetime, timedelta
class APIKeyManager:
"""Secure API key lifecycle management."""
def generate_key(self, owner: str, scopes: list,
environment: str = "live"):
"""Generate a new API key with proper security."""
# 1. Generate cryptographically random key
random_bytes = secrets.token_bytes(32)
key_suffix = secrets.token_urlsafe(32)
# 2. Add identifiable prefix
prefix = f"sk_{environment}_"
full_key = f"{prefix}{key_suffix}"
# 3. Hash for storage (NEVER store plaintext)
key_hash = hashlib.sha256(full_key.encode()).hexdigest()
# 4. Store metadata
self.db.insert("api_keys", {
"key_hash": key_hash,
"prefix": prefix + key_suffix[:8], # Store prefix for identification
"owner": owner,
"scopes": scopes,
"environment": environment,
"created_at": datetime.utcnow(),
"expires_at": datetime.utcnow() + timedelta(days=90),
"last_used_at": None,
"status": "active",
})
# 5. Return key exactly once — cannot be retrieved again
return {
"key": full_key, # Show ONCE, then never again
"prefix": prefix + key_suffix[:8],
"expires_at": (datetime.utcnow() + timedelta(days=90)).isoformat(),
"scopes": scopes,
}
def validate_key(self, provided_key: str):
"""Validate an API key and check permissions."""
key_hash = hashlib.sha256(provided_key.encode()).hexdigest()
record = self.db.find("api_keys", {"key_hash": key_hash})
if not record:
return {"valid": False, "reason": "Key not found"}
if record["status"] != "active":
return {"valid": False, "reason": f"Key is {record['status']}"}
if record["expires_at"] < datetime.utcnow():
return {"valid": False, "reason": "Key expired"}
# Update last used timestamp
self.db.update("api_keys", {"key_hash": key_hash}, {
"last_used_at": datetime.utcnow(),
})
return {
"valid": True,
"owner": record["owner"],
"scopes": record["scopes"],
}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| API keys in source code | Key leaked if repo is public or cloned | Environment variables or secret manager |
| Store plaintext keys in database | Database breach = all keys compromised | Store only hashed keys |
| No key rotation policy | Old keys accumulate, attack surface grows | 90-day rotation with automated reminders |
| Single key with all permissions | Compromise = full access | Scoped keys with minimum required permissions |
| No usage monitoring | Cannot detect compromised key usage | Log all key usage, alert on anomalies |
API key security is only as strong as its weakest lifecycle stage. A properly generated key stored in a Slack message is compromised. A well-protected key with no rotation policy becomes a ticking time bomb. Secure the entire lifecycle, not just generation.