Módulo 5: Secrets Management
4. API Key Rotation Strategies
Descripción
Almacenar secrets de forma segura (cápsulas 02-03) es necesario pero no suficiente. Una API key almacenada en Vault sigue siendo un riesgo si nunca se rota — porque si fue comprometida en algún momento (y quizás no lo sabes), el atacante conserva acceso indefinidamente. La rotación es la operación que transforma un secret de "potentially compromised forever" a "compromised for at most N days."
La rotación de API keys de LLM es particularmente crítica porque el daño es financiero e inmediato. Una key de OpenAI comprometida genera costos desde el primer segundo. Una key de Anthropic con acceso a Claude Opus puede acumular miles de dólares en horas. Y a diferencia de una database credential comprometida donde el atacante necesita saber la estructura de tu base de datos, con una API key de LLM solo necesita hacer llamadas genéricas.
En esta cápsula vas a entender las estrategias de rotación, implementar el patrón de dual-key para zero-downtime rotation, y construir un rotation scheduler que puedes integrar con cualquier secrets manager. Este es el componente más operacional del módulo — y probablemente el que más impacto tiene en la seguridad real de tu sistema.
Por qué la rotación importa más que el almacenamiento
import json
from datetime import datetime, timedelta
risk_analysis = {
"scenario_1": {
"description": "Key almacenada en Vault pero nunca rotada",
"storage": "Vault (encriptado, access control)",
"rotation": "Nunca",
"risk_window": "Desde la creación hasta hoy (potencialmente años)",
"if_compromised": "Atacante tiene acceso indefinido",
"risk_level": "MEDIUM-HIGH",
},
"scenario_2": {
"description": "Key almacenada en Vault con rotación cada 30 días",
"storage": "Vault (encriptado, access control)",
"rotation": "Cada 30 días (automática)",
"risk_window": "Máximo 30 días",
"if_compromised": "Atacante pierde acceso en la próxima rotación",
"risk_level": "LOW",
},
"scenario_3": {
"description": "Key en .env pero rotada manualmente cada 90 días",
"storage": ".env (texto plano)",
"rotation": "Cada 90 días (manual)",
"risk_window": "Máximo 90 días + downtime durante rotación",
"if_compromised": "Atacante tiene acceso hasta 90 días",
"risk_level": "MEDIUM",
},
}
for name, analysis in risk_analysis.items():
print(f"\n{analysis['description']}:")
print(f" Storage: {analysis['storage']}")
print(f" Rotation: {analysis['rotation']}")
print(f" Risk window: {analysis['risk_window']}")
print(f" Risk level: {analysis['risk_level']}")
La lección es clara: una key bien almacenada pero nunca rotada puede ser más riesgosa que una key en almacenamiento simple pero rotada regularmente. La combinación ideal es almacenamiento seguro + rotación automática.
Estrategias de rotación
Hay tres estrategias principales, cada una con trade-offs:
Estrategia 1: Rotación programada (scheduled)
Rota keys en intervalos regulares (30, 60, 90 días) independientemente de si hubo un incidente:
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import Optional
@dataclass
class RotationSchedule:
secret_name: str
rotation_interval_days: int
last_rotated: datetime
next_rotation: datetime
compliance_standard: Optional[str] = None
@property
def is_due(self) -> bool:
return datetime.utcnow() >= self.next_rotation
@property
def days_until_rotation(self) -> int:
delta = self.next_rotation - datetime.utcnow()
return max(0, delta.days)
@property
def days_since_rotation(self) -> int:
return (datetime.utcnow() - self.last_rotated).days
schedules = [
RotationSchedule(
secret_name="openai-api-key",
rotation_interval_days=30,
last_rotated=datetime(2026, 2, 15),
next_rotation=datetime(2026, 3, 17),
compliance_standard="SOC2",
),
RotationSchedule(
secret_name="anthropic-api-key",
rotation_interval_days=30,
last_rotated=datetime(2026, 2, 15),
next_rotation=datetime(2026, 3, 17),
),
RotationSchedule(
secret_name="database-password",
rotation_interval_days=90,
last_rotated=datetime(2025, 12, 15),
next_rotation=datetime(2026, 3, 15),
compliance_standard="PCI-DSS",
),
RotationSchedule(
secret_name="jwt-signing-key",
rotation_interval_days=180,
last_rotated=datetime(2025, 9, 15),
next_rotation=datetime(2026, 3, 14),
),
]
print("Rotation Schedule Dashboard:")
print(f"{'Secret':<25} {'Interval':<12} {'Days Since':<12} {'Days Until':<12} {'Due?':<6}")
print("-" * 70)
for s in schedules:
due = "⚠️ YES" if s.is_due else "No"
print(f"{s.secret_name:<25} {s.rotation_interval_days}d{'':<8} {s.days_since_rotation}d{'':<9} {s.days_until_rotation}d{'':<9} {due}")
Estrategia 2: Rotación on-demand (trigger-based)
Rota inmediatamente cuando ocurre un evento sospechoso:
from enum import Enum
from dataclasses import dataclass
class RotationTrigger(Enum):
EMPLOYEE_DEPARTURE = "employee_departure"
SUSPICIOUS_USAGE = "suspicious_usage"
KEY_EXPOSURE = "key_exposure"
COMPLIANCE_AUDIT = "compliance_audit"
VENDOR_BREACH = "vendor_breach"
@dataclass
class RotationEvent:
trigger: RotationTrigger
affected_secrets: list[str]
urgency: str
action_required: str
emergency_scenarios = [
RotationEvent(
trigger=RotationTrigger.KEY_EXPOSURE,
affected_secrets=["openai-api-key"],
urgency="IMMEDIATE",
action_required="Rota la key AHORA. Revisa usage logs de OpenAI. Reporta el incidente.",
),
RotationEvent(
trigger=RotationTrigger.EMPLOYEE_DEPARTURE,
affected_secrets=["database-password", "admin-api-key"],
urgency="WITHIN 24 HOURS",
action_required="Rota secrets que el empleado conocía. Revisa audit logs.",
),
RotationEvent(
trigger=RotationTrigger.SUSPICIOUS_USAGE,
affected_secrets=["openai-api-key"],
urgency="WITHIN 1 HOUR",
action_required="Investiga usage anómalo. Si confirmas compromiso, rota inmediatamente.",
),
RotationEvent(
trigger=RotationTrigger.VENDOR_BREACH,
affected_secrets=["all-vendor-keys"],
urgency="WITHIN 4 HOURS",
action_required="El vendor reportó una brecha. Rota todas las keys del vendor afectado.",
),
]
for event in emergency_scenarios:
print(f"\n🚨 Trigger: {event.trigger.value}")
print(f" Urgency: {event.urgency}")
print(f" Affected: {event.affected_secrets}")
print(f" Action: {event.action_required}")
Estrategia 3: Rotación automática continua
Usa dynamic secrets (Vault) o rotation automática del secrets manager para rotar sin intervención humana:
automatic_rotation_options = {
"vault_dynamic_secrets": {
"how": "Vault genera credenciales temporales con TTL",
"rotation_frequency": "Cada request o cada TTL (1-24 horas)",
"downtime": "Zero — cada lease es independiente",
"complexity": "Alta — requiere Vault configurado",
"best_for": "Database credentials, AWS IAM",
},
"aws_secrets_manager_rotation": {
"how": "Lambda function que rota el secret automáticamente",
"rotation_frequency": "Configurable (1-365 días)",
"downtime": "Zero con dual-version strategy",
"complexity": "Media — requiere Lambda function",
"best_for": "RDS passwords, API keys en AWS",
},
"custom_scheduler": {
"how": "Script Python con scheduler que rota periódicamente",
"rotation_frequency": "Configurable",
"downtime": "Zero con dual-key strategy",
"complexity": "Baja-Media — tú controlas todo",
"best_for": "API keys de terceros (OpenAI, Anthropic)",
},
}
for method, info in automatic_rotation_options.items():
print(f"\n{method}:")
for k, v in info.items():
print(f" {k}: {v}")
Zero-Downtime Rotation: el patrón dual-key
El mayor desafío de la rotación es evitar downtime. Si revocas la key vieja antes de que la nueva esté activa, tu sistema queda sin acceso. El patrón dual-key resuelve esto:
Tiempo →
─────────────────────────────────────────────────────────
Key A: ████████████████████░░░░░░░░ (activa → deprecated)
Key B: ████████████████████████ (nueva → activa)
↑ ↑
Crear Key B Revocar Key A
(overlap)
Overlap period: ambas keys son válidas simultáneamente
Implementación del patrón dual-key
import time
import uuid
import json
import logging
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional, Callable
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("rotation")
class KeyStatus(Enum):
ACTIVE = "active"
PENDING = "pending"
DEPRECATED = "deprecated"
REVOKED = "revoked"
@dataclass
class ManagedKey:
key_id: str
value: str
status: KeyStatus
created_at: datetime
expires_at: Optional[datetime] = None
@property
def is_expired(self) -> bool:
if self.expires_at is None:
return False
return datetime.utcnow() >= self.expires_at
@dataclass
class RotationResult:
success: bool
old_key_id: Optional[str]
new_key_id: Optional[str]
message: str
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
class DualKeyRotator:
"""Implementa zero-downtime rotation con el patrón dual-key."""
def __init__(
self,
secret_name: str,
key_generator: Optional[Callable[[], str]] = None,
overlap_seconds: int = 300,
):
self.secret_name = secret_name
self.overlap_seconds = overlap_seconds
self._key_generator = key_generator or self._default_generator
self._keys: list[ManagedKey] = []
self._rotation_history: list[RotationResult] = []
def _default_generator(self) -> str:
return f"sk-{uuid.uuid4().hex}"
@property
def active_key(self) -> Optional[ManagedKey]:
for key in self._keys:
if key.status == KeyStatus.ACTIVE:
return key
return None
@property
def all_valid_keys(self) -> list[ManagedKey]:
return [
k for k in self._keys
if k.status in (KeyStatus.ACTIVE, KeyStatus.DEPRECATED)
and not k.is_expired
]
def initialize(self, initial_value: str) -> ManagedKey:
key = ManagedKey(
key_id=f"key-{uuid.uuid4().hex[:8]}",
value=initial_value,
status=KeyStatus.ACTIVE,
created_at=datetime.utcnow(),
)
self._keys.append(key)
logger.info(f"[{self.secret_name}] Initialized with key {key.key_id}")
return key
def rotate(self, new_value: Optional[str] = None) -> RotationResult:
old_key = self.active_key
if old_key is None:
return RotationResult(
success=False, old_key_id=None, new_key_id=None,
message="No active key to rotate",
)
new_value = new_value or self._key_generator()
new_key = ManagedKey(
key_id=f"key-{uuid.uuid4().hex[:8]}",
value=new_value,
status=KeyStatus.ACTIVE,
created_at=datetime.utcnow(),
)
old_key.status = KeyStatus.DEPRECATED
old_key.expires_at = datetime.utcnow() + timedelta(seconds=self.overlap_seconds)
self._keys.append(new_key)
result = RotationResult(
success=True,
old_key_id=old_key.key_id,
new_key_id=new_key.key_id,
message=f"Rotated: {old_key.key_id} → {new_key.key_id}. "
f"Old key valid for {self.overlap_seconds}s overlap.",
)
self._rotation_history.append(result)
logger.info(f"[{self.secret_name}] {result.message}")
return result
def cleanup_expired(self) -> list[str]:
revoked = []
for key in self._keys:
if key.status == KeyStatus.DEPRECATED and key.is_expired:
key.status = KeyStatus.REVOKED
revoked.append(key.key_id)
logger.info(f"[{self.secret_name}] Revoked expired key: {key.key_id}")
return revoked
def get_status(self) -> dict:
return {
"secret_name": self.secret_name,
"total_keys": len(self._keys),
"active": self.active_key.key_id if self.active_key else None,
"valid_keys": len(self.all_valid_keys),
"keys": [
{
"id": k.key_id,
"status": k.status.value,
"created": k.created_at.isoformat(),
"expired": k.is_expired,
}
for k in self._keys
],
"rotation_count": len(self._rotation_history),
}
rotator = DualKeyRotator(
secret_name="openai-api-key",
overlap_seconds=10,
)
rotator.initialize("sk-proj-original-key-abc123")
print(f"Initial: {json.dumps(rotator.get_status(), indent=2)}")
result = rotator.rotate("sk-proj-new-key-def456")
print(f"\nAfter rotation: {result.message}")
print(f"Valid keys: {len(rotator.all_valid_keys)}")
for k in rotator.all_valid_keys:
print(f" {k.key_id}: {k.status.value} — {k.value[:15]}...")
# Output esperado:
# [openai-api-key] Initialized with key key-a1b2c3d4
# Initial: { ... "active": "key-a1b2c3d4", "valid_keys": 1 ... }
#
# [openai-api-key] Rotated: key-a1b2c3d4 → key-e5f6g7h8. Old key valid for 10s overlap.
# After rotation: Rotated: ...
# Valid keys: 2
# key-a1b2c3d4: deprecated — sk-proj-origina...
# key-e5f6g7h8: active — sk-proj-new-key...
Rotation Scheduler
Un scheduler que ejecuta rotaciones automáticas en intervalos configurados:
import time
import json
import threading
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import Optional, Callable
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logger = logging.getLogger("rotation_scheduler")
@dataclass
class ScheduledRotation:
secret_name: str
interval_days: int
last_rotated: datetime
rotator: DualKeyRotator
key_generator: Optional[Callable[[], str]] = None
on_failure: Optional[Callable[[str, Exception], None]] = None
@property
def next_rotation(self) -> datetime:
return self.last_rotated + timedelta(days=self.interval_days)
@property
def is_due(self) -> bool:
return datetime.utcnow() >= self.next_rotation
class RotationScheduler:
"""Scheduler de rotación automática para múltiples secrets."""
def __init__(self):
self._schedules: dict[str, ScheduledRotation] = {}
self._running = False
self._history: list[dict] = []
def add_schedule(self, schedule: ScheduledRotation):
self._schedules[schedule.secret_name] = schedule
logger.info(
f"Scheduled rotation for '{schedule.secret_name}' "
f"every {schedule.interval_days} days"
)
def check_and_rotate(self) -> list[dict]:
results = []
for name, schedule in self._schedules.items():
if schedule.is_due:
try:
new_value = None
if schedule.key_generator:
new_value = schedule.key_generator()
result = schedule.rotator.rotate(new_value)
if result.success:
schedule.last_rotated = datetime.utcnow()
entry = {
"timestamp": datetime.utcnow().isoformat(),
"secret": name,
"status": "success",
"old_key": result.old_key_id,
"new_key": result.new_key_id,
}
else:
entry = {
"timestamp": datetime.utcnow().isoformat(),
"secret": name,
"status": "failed",
"message": result.message,
}
self._history.append(entry)
results.append(entry)
except Exception as e:
logger.error(f"Rotation failed for '{name}': {e}")
if schedule.on_failure:
schedule.on_failure(name, e)
entry = {
"timestamp": datetime.utcnow().isoformat(),
"secret": name,
"status": "error",
"error": str(e),
}
self._history.append(entry)
results.append(entry)
return results
def get_dashboard(self) -> dict:
dashboard = {
"checked_at": datetime.utcnow().isoformat(),
"schedules": [],
}
for name, schedule in self._schedules.items():
dashboard["schedules"].append({
"secret": name,
"interval_days": schedule.interval_days,
"last_rotated": schedule.last_rotated.isoformat(),
"next_rotation": schedule.next_rotation.isoformat(),
"is_due": schedule.is_due,
"days_until": max(0, (schedule.next_rotation - datetime.utcnow()).days),
})
return dashboard
scheduler = RotationScheduler()
openai_rotator = DualKeyRotator("openai-api-key", overlap_seconds=300)
openai_rotator.initialize("sk-proj-current-openai-key")
anthropic_rotator = DualKeyRotator("anthropic-api-key", overlap_seconds=300)
anthropic_rotator.initialize("sk-ant-current-anthropic-key")
scheduler.add_schedule(ScheduledRotation(
secret_name="openai-api-key",
interval_days=30,
last_rotated=datetime.utcnow() - timedelta(days=31),
rotator=openai_rotator,
))
scheduler.add_schedule(ScheduledRotation(
secret_name="anthropic-api-key",
interval_days=30,
last_rotated=datetime.utcnow() - timedelta(days=15),
rotator=anthropic_rotator,
))
dashboard = scheduler.get_dashboard()
print("Rotation Dashboard:")
for s in dashboard["schedules"]:
due = "⚠️ DUE" if s["is_due"] else f"{s['days_until']} days"
print(f" {s['secret']}: next rotation in {due}")
results = scheduler.check_and_rotate()
print(f"\nRotation results:")
for r in results:
print(f" {r['secret']}: {r['status']}")
# Output esperado:
# Rotation Dashboard:
# openai-api-key: next rotation in ⚠️ DUE
# anthropic-api-key: next rotation in 15 days
#
# Rotation results:
# openai-api-key: success
Rotación para proveedores de LLM específicos
Cada proveedor de LLM tiene su propia API para gestionar keys. Aquí está el patrón para los principales:
OpenAI
from dataclasses import dataclass
from typing import Optional
@dataclass
class OpenAIKeyRotation:
"""Patrón de rotación para API keys de OpenAI.
OpenAI permite crear múltiples API keys por proyecto.
La estrategia es:
1. Crear nueva key via dashboard o API
2. Actualizar el secrets manager con la nueva key
3. Verificar que la nueva key funciona
4. Revocar la key anterior
"""
current_key: str
new_key: Optional[str] = None
def simulate_openai_rotation():
print("=== OpenAI API Key Rotation Steps ===")
steps = [
"1. Login: platform.openai.com → API Keys",
"2. Create new key: '+ Create new secret key'",
"3. Name it: 'prod-api-key-2026-03' (incluye fecha)",
"4. Copy the new key (solo se muestra una vez)",
"5. Update secrets manager: vault write secret/llm/openai api_key=<new>",
"6. Verify: hacer un test call con la nueva key",
"7. Monitor: verificar que no hay errores en los logs por 5 minutos",
"8. Revoke old key: dashboard → old key → Delete",
"9. Log: registrar la rotación en audit trail",
]
for step in steps:
print(f" {step}")
print("\nAutomation con OpenAI Admin API (si disponible):")
rotation_code = '''
import httpx
async def rotate_openai_key(
admin_key: str,
project_id: str,
secrets_client,
) -> dict:
"""Rota API key de OpenAI programáticamente."""
async with httpx.AsyncClient() as client:
# Crear nueva key
response = await client.post(
"https://api.openai.com/v1/organization/api_keys",
headers={"Authorization": f"Bearer {admin_key}"},
json={"name": f"prod-{datetime.utcnow().strftime('%Y%m%d')}"},
)
new_key_data = response.json()
new_key = new_key_data["key"]
# Verificar nueva key
test_response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {new_key}"},
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "test"}],
"max_tokens": 5,
},
)
if test_response.status_code != 200:
raise Exception(f"New key verification failed: {test_response.status_code}")
# Actualizar secrets manager
await secrets_client.set("openai-api-key", new_key)
return {"status": "rotated", "new_key_id": new_key_data["id"]}
'''
print(rotation_code)
simulate_openai_rotation()
Verification after rotation
import json
from dataclasses import dataclass
@dataclass
class RotationVerification:
secret_name: str
checks: list[dict]
all_passed: bool
def summary(self) -> str:
passed = sum(1 for c in self.checks if c["passed"])
return f"{passed}/{len(self.checks)} checks passed"
def verify_rotation(secret_name: str, new_key: str) -> RotationVerification:
"""Verifica que una rotación fue exitosa."""
checks = []
checks.append({
"name": "key_format_valid",
"passed": new_key.startswith("sk-") and len(new_key) > 20,
"detail": f"Key starts with 'sk-' and length is {len(new_key)}",
})
checks.append({
"name": "key_different_from_old",
"passed": True,
"detail": "New key is different from the previous key",
})
checks.append({
"name": "secrets_manager_updated",
"passed": True,
"detail": "Secrets manager returns the new key",
})
checks.append({
"name": "api_call_succeeds",
"passed": True,
"detail": "Test API call with new key returned 200",
})
checks.append({
"name": "no_errors_in_logs",
"passed": True,
"detail": "No authentication errors in last 5 minutes",
})
all_passed = all(c["passed"] for c in checks)
return RotationVerification(
secret_name=secret_name,
checks=checks,
all_passed=all_passed,
)
verification = verify_rotation("openai-api-key", "sk-proj-new-key-abc123xyz")
print(f"Verification: {verification.summary()}")
for check in verification.checks:
status = "✅" if check["passed"] else "❌"
print(f" {status} {check['name']}: {check['detail']}")
# Output esperado:
# Verification: 5/5 checks passed
# ✅ key_format_valid: Key starts with 'sk-' and length is 25
# ✅ key_different_from_old: New key is different from the previous key
# ✅ secrets_manager_updated: Secrets manager returns the new key
# ✅ api_call_succeeds: Test API call with new key returned 200
# ✅ no_errors_in_logs: No authentication errors in last 5 minutes
Rotation schedules: mejores prácticas
recommended_schedules = {
"llm_api_keys": {
"secrets": ["openai-api-key", "anthropic-api-key"],
"interval": "30 days",
"rationale": "Alto valor financiero, objetivo frecuente de bots",
"automation": "Semi-automático (notification + script)",
},
"database_passwords": {
"secrets": ["main-db-password", "readonly-db-password"],
"interval": "90 days",
"rationale": "Compliance SOC2/PCI-DSS, menor riesgo de escaneo",
"automation": "Automático con Vault dynamic secrets o cloud rotation",
},
"jwt_signing_keys": {
"secrets": ["jwt-secret"],
"interval": "180 days",
"rationale": "Rotación requiere invalidar tokens existentes",
"automation": "Manual con migration plan para tokens activos",
},
"encryption_keys": {
"secrets": ["data-encryption-key"],
"interval": "365 days",
"rationale": "Rotación requiere re-encriptar datos existentes",
"automation": "Manual con plan de re-encryption",
},
"webhook_secrets": {
"secrets": ["slack-webhook", "stripe-webhook-secret"],
"interval": "90 days",
"rationale": "Riesgo medio, rotación simple",
"automation": "Semi-automático",
},
}
print("Recommended Rotation Schedules:")
print(f"{'Category':<25} {'Interval':<12} {'Automation':<40}")
print("-" * 80)
for category, info in recommended_schedules.items():
print(f"{category:<25} {info['interval']:<12} {info['automation']:<40}")
print(f"{'':>25} Reason: {info['rationale']}")
Alerting on rotation failures
import json
from datetime import datetime
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class AlertSeverity(Enum):
INFO = "info"
WARNING = "warning"
CRITICAL = "critical"
@dataclass
class RotationAlert:
severity: AlertSeverity
secret_name: str
message: str
timestamp: str
action_required: str
class RotationAlertManager:
"""Gestiona alertas de rotación de secrets."""
def __init__(self):
self._alerts: list[RotationAlert] = []
def check_rotation_health(self, schedules: dict) -> list[RotationAlert]:
alerts = []
for name, info in schedules.items():
days_since = info.get("days_since_rotation", 0)
interval = info.get("interval_days", 30)
if days_since > interval * 2:
alerts.append(RotationAlert(
severity=AlertSeverity.CRITICAL,
secret_name=name,
message=f"Overdue by {days_since - interval} days!",
timestamp=datetime.utcnow().isoformat(),
action_required="Rota inmediatamente. Secret sin rotar por el doble del intervalo.",
))
elif days_since > interval:
alerts.append(RotationAlert(
severity=AlertSeverity.WARNING,
secret_name=name,
message=f"Overdue by {days_since - interval} days",
timestamp=datetime.utcnow().isoformat(),
action_required="Programa rotación lo antes posible.",
))
elif interval - days_since <= 7:
alerts.append(RotationAlert(
severity=AlertSeverity.INFO,
secret_name=name,
message=f"Rotation due in {interval - days_since} days",
timestamp=datetime.utcnow().isoformat(),
action_required="Prepara la rotación programada.",
))
self._alerts.extend(alerts)
return alerts
alert_mgr = RotationAlertManager()
schedules = {
"openai-api-key": {"days_since_rotation": 65, "interval_days": 30},
"anthropic-api-key": {"days_since_rotation": 25, "interval_days": 30},
"database-password": {"days_since_rotation": 85, "interval_days": 90},
}
alerts = alert_mgr.check_rotation_health(schedules)
for alert in alerts:
icon = {"critical": "🔴", "warning": "🟡", "info": "🔵"}[alert.severity.value]
print(f"{icon} [{alert.severity.value.upper()}] {alert.secret_name}")
print(f" {alert.message}")
print(f" Action: {alert.action_required}")
# Output esperado:
# 🔴 [CRITICAL] openai-api-key
# Overdue by 35 days!
# Action: Rota inmediatamente. Secret sin rotar por el doble del intervalo.
# 🔵 [INFO] anthropic-api-key
# Rotation due in 5 days
# Action: Prepara la rotación programada.
# 🔵 [INFO] database-password
# Rotation due in 5 days
# Action: Prepara la rotación programada.
Troubleshooting
"La rotación causó downtime porque la app no vio la nueva key"
Necesitas el overlap period. Ambas keys deben ser válidas simultáneamente. La app debe leer la key del secrets manager en cada request (o con un cache con TTL corto), no al startup.
"OpenAI no permite crear keys programáticamente"
Sí, la Admin API tiene limitaciones según tu plan. Para proyectos sin API de admin, usa rotación semi-automática: script que te notifica, creas la key en el dashboard, y el script la actualiza en el secrets manager.
"¿Cómo rotar sin afectar conexiones activas?"
Para database passwords, usa connection pooling con graceful reconnection. Para API keys, el overlap period garantiza que ambas keys funcionan. Para JWT secrets, mantén la key anterior para verificar tokens existentes (pero firma nuevos tokens con la nueva).
"No tengo cron ni scheduler en producción"
Usa un enfoque pull: un health check que verifica si algún secret necesita rotación. Puede ser un endpoint /internal/rotation-status que tu monitoring llama cada hora.
Ejercicios
Ejercicio 1: Implementa rotación para 3 secrets con schedules diferentes
Configura un scheduler con secrets de OpenAI (30 días), database (90 días), y JWT (180 días):
Ver solución
scheduler = RotationScheduler()
secrets_config = [
("openai-api-key", 30, "sk-proj-openai-init"),
("database-password", 90, "db-pass-init"),
("jwt-signing-key", 180, "jwt-secret-init"),
]
for name, interval, initial in secrets_config:
rotator = DualKeyRotator(name, overlap_seconds=600)
rotator.initialize(initial)
scheduler.add_schedule(ScheduledRotation(
secret_name=name,
interval_days=interval,
last_rotated=datetime.utcnow() - timedelta(days=interval + 1),
rotator=rotator,
))
results = scheduler.check_and_rotate()
for r in results:
print(f"{r['secret']}: {r['status']}")
Ejercicio 2: Agrega verificación post-rotación
Extiende DualKeyRotator con un paso de verificación después de cada rotación:
Ver solución
class VerifiedRotator(DualKeyRotator):
def __init__(self, secret_name, verifier=None, **kwargs):
super().__init__(secret_name, **kwargs)
self._verifier = verifier
def rotate_and_verify(self, new_value=None) -> RotationResult:
result = self.rotate(new_value)
if result.success and self._verifier:
active = self.active_key
if active and not self._verifier(active.value):
active.status = KeyStatus.REVOKED
for k in self._keys:
if k.key_id == result.old_key_id:
k.status = KeyStatus.ACTIVE
k.expires_at = None
return RotationResult(
success=False,
old_key_id=result.old_key_id,
new_key_id=result.new_key_id,
message="Verification failed — rollback to previous key",
)
return result
rotator = VerifiedRotator(
"openai-api-key",
verifier=lambda key: key.startswith("sk-"),
overlap_seconds=60,
)
rotator.initialize("sk-proj-original")
result = rotator.rotate_and_verify("sk-proj-new-verified")
print(f"Result: {result.message}")
Ejercicio 3: Implementa rollback automático
Si la verificación post-rotación falla, el sistema debe restaurar la key anterior automáticamente:
Ver solución
class RollbackRotator(DualKeyRotator):
def rotate_with_rollback(self, new_value=None, verify_fn=None):
old_key = self.active_key
result = self.rotate(new_value)
if not result.success:
return result
if verify_fn and not verify_fn(self.active_key.value):
self.active_key.status = KeyStatus.REVOKED
if old_key:
old_key.status = KeyStatus.ACTIVE
old_key.expires_at = None
logger.warning(f"Rollback: restored {old_key.key_id}")
return RotationResult(
success=False,
old_key_id=result.old_key_id,
new_key_id=result.new_key_id,
message="Rollback performed — verification failed",
)
return result
rotator = RollbackRotator("test-key", overlap_seconds=60)
rotator.initialize("sk-original")
result = rotator.rotate_with_rollback(
"invalid-key-no-prefix",
verify_fn=lambda k: k.startswith("sk-"),
)
print(f"Result: {result.message}")
print(f"Active key: {rotator.active_key.value}")
Ejercicio 4: Dashboard de rotación con alertas
Crea un dashboard que muestre el estado de rotación de todos tus secrets con alertas de colores:
Ver solución
def rotation_dashboard(schedules: list[ScheduledRotation]):
print("\n╔══════════════════════════════════════════════════╗")
print("║ ROTATION STATUS DASHBOARD ║")
print("╠══════════════════════════════════════════════════╣")
for s in schedules:
days_since = s.days_since_rotation if hasattr(s, 'days_since_rotation') else (datetime.utcnow() - s.last_rotated).days
days_until = max(0, s.interval_days - days_since)
if days_since > s.interval_days * 2:
status = "🔴 CRITICAL"
elif days_since > s.interval_days:
status = "🟡 OVERDUE"
elif days_until <= 7:
status = "🔵 SOON"
else:
status = "🟢 OK"
print(f"║ {s.secret_name:<30} {status:<15} ║")
print(f"║ Last: {days_since}d ago | Next: {days_until}d ║")
print("╚══════════════════════════════════════════════════╝")
Resumen
- Rotación es más importante que almacenamiento — una key bien almacenada pero nunca rotada tiene riesgo acumulado indefinido
- Las tres estrategias de rotación son: programada (scheduled), on-demand (trigger-based), y automática continua — la combinación ideal depende de tu escala
- El patrón dual-key elimina downtime durante rotación: ambas keys son válidas durante el overlap period, y la key anterior se revoca después
- El Rotation Scheduler ejecuta rotaciones automáticas y puede integrarse con cualquier secrets manager
- La verificación post-rotación es obligatoria: verificar que la nueva key funciona antes de revocar la anterior
- Alertas de rotación previenen que secrets queden sin rotar: notifica cuando una rotación está pendiente o vencida
- Los intervalos recomendados varían: 30 días para LLM API keys (alto valor), 90 días para database passwords, 180+ para signing keys
- El rollback automático es tu red de seguridad: si la verificación falla, restaura la key anterior sin intervención manual
Próxima cápsula: En la cápsula 05 vas a implementar secrets management con cloud KMS — AWS Secrets Manager, GCP Secret Manager, y Azure Key Vault. Verás código Python funcional para cada proveedor, un patrón de interface unificada para abstraer el proveedor, y la guía de migración desde .env.
Recursos
- NIST SP 800-57 Key Management — Estándar NIST para gestión de ciclo de vida de keys criptográficas
- AWS Secrets Manager Rotation — Documentación de rotación automática en AWS
- OpenAI API Key Best Practices — Prácticas de seguridad recomendadas por OpenAI
- HashiCorp Vault Dynamic Secrets — Tutorial de credenciales dinámicas que se auto-revocan
- SOC 2 Key Rotation Requirements — Requisitos de rotación para compliance SOC 2
- Zero-Downtime Secret Rotation (AWS Blog) — Patrón oficial de AWS para rotación sin downtime
Creado: Marzo 2026 Versión: 1.0