| name | implementing-api-key-security-controls |
| description | Implements secure API key generation with sufficient entropy, server-side hashing (SHA-256/bcrypt) instead of plaintext storage, per-key scoping to endpoints/IPs/rate limits, zero-downtime rotation, and automated leak monitoring across GitHub repos, logs, and client-side code. Use when designing API key formats, building key rotation or revocation workflows, or protecting server-to-server API credentials from leakage, brute force, and abuse. |
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","api-keys","credential-management","key-rotation","secret-management"] |
| version | 1.0.0 |
| author | mahipal |
| license | Apache-2.0 |
| nist_ai_rmf | ["MEASURE-2.7","MAP-5.1","MANAGE-2.4"] |
| atlas_techniques | ["AML.T0070","AML.T0066","AML.T0082"] |
| nist_csf | ["PR.PS-01","ID.RA-01","PR.DS-10","DE.CM-01"] |
| mitre_attack | ["T1190","T1059.007","T1552.001","T1003","T1110"] |
Implementing API Key Security Controls
When to Use
- Designing secure API key generation with sufficient entropy and identifiable prefixes for leak detection
- Implementing server-side API key hashing (never storing keys in plaintext) with SHA-256 or bcrypt
- Building key rotation workflows that allow zero-downtime key replacement for API consumers
- Configuring per-key scoping to limit each API key to specific endpoints, IP ranges, and rate limits
- Setting up automated monitoring for API key leakage in GitHub repos, logs, and client-side code
Do not use API keys as the sole authentication mechanism for user-facing applications. API keys are best suited for server-to-server communication and developer access.
Prerequisites
- Secure random number generator (os.urandom, secrets module) for key generation
- Database with proper encryption at rest for storing hashed API keys
- Redis or similar store for key-to-metadata caching and rate limiting
- Secret scanning tools (GitHub secret scanning, truffleHog, gitleaks)
- Monitoring and alerting infrastructure for key usage anomalies
Workflow
Step 1: Secure API Key Generation
import secrets
import hashlib
import hmac
import time
import json
from datetime import datetime, timedelta
class APIKeyManager:
"""Manages secure API key lifecycle: generation, storage, validation, rotation."""
KEY_PREFIXES = {
"live_secret": "sk_live_",
"test_secret": "sk_test_",
"live_public": "pk_live_",
"test_public": "pk_test_",
}
def __init__(self, db_connection, redis_connection):
.db = db_connection
.redis = redis_connection
():
prefix = .KEY_PREFIXES.get(key_type, )
random_bytes = secrets.token_bytes()
key_body = secrets.token_urlsafe()
full_key =
key_hash = hashlib.sha256(full_key.encode()).hexdigest()
key_id =
key_metadata = {
: key_hash,
: key_id,
: key_type,
: owner_id,
: scopes [],
: rate_limit {: , : },
: ip_allowlist [],
: datetime.utcnow().isoformat(),
: (datetime.utcnow() + timedelta(days=expires_days)).isoformat(),
: ,
: ,
: ,
}
.db.execute(
,
(key_hash, key_id, json.dumps(key_metadata))
)
.redis.setex(
,
,
json.dumps(key_metadata)
)
{
: full_key,
: key_id,
: key_metadata[],
: key_metadata[],
}
():
key_hash = hashlib.sha256(api_key.encode()).hexdigest()
cached = .redis.get()
cached:
metadata = json.loads(cached)
:
row = .db.execute(
,
(key_hash,)
).fetchone()
row:
,
metadata = json.loads(row[])
.redis.setex(, , row[])
metadata.get():
,
metadata.get():
datetime.fromisoformat(metadata[]) < datetime.utcnow():
,
metadata[] = datetime.utcnow().isoformat()
metadata[] = metadata.get(, ) +
.redis.setex(, , json.dumps(metadata))
metadata,
():
row = .db.execute(
,
(key_id,)
).fetchone()
row:
key_hash = row[]
metadata = json.loads(row[])
metadata[] =
metadata[] = datetime.utcnow().isoformat()
.db.execute(
,
(json.dumps(metadata), key_id)
)
.redis.delete()
():
old_row = .db.execute(
,
(old_key_id,)
).fetchone()
old_row:
,
old_metadata = json.loads(old_row[])
new_key_data = .generate_key(
key_type=old_metadata[],
owner_id=old_metadata[],
scopes=old_metadata[],
rate_limit=old_metadata[],
ip_allowlist=old_metadata[],
)
revoke_at = datetime.utcnow() + timedelta(hours=grace_period_hours)
old_metadata[] = revoke_at.isoformat()
.db.execute(
,
(json.dumps(old_metadata), old_key_id)
)
{
: new_key_data,
: old_key_id,
: revoke_at.isoformat(),
:
},