Módulo 5: Secrets Management
5. Cloud KMS: AWS, GCP, Azure
Descripción
En la cápsula 03 conociste HashiCorp Vault como la referencia enterprise para secrets management. Vault es poderoso pero requiere infraestructura y expertise para operar. Para la mayoría de equipos — startups, equipos medianos, y proyectos que ya corren en un cloud provider — los servicios managed de secrets management son la opción más pragmática: cero infraestructura que operar, integración nativa con IAM, y costos predecibles.
Esta cápsula cubre los tres principales: AWS Secrets Manager (boto3), GCP Secret Manager (google-cloud-secret-manager), y Azure Key Vault (azure-keyvault-secrets). Vas a ver código Python funcional para cada uno, una comparación detallada, y un patrón de interface unificada que abstrae el proveedor — para que tu aplicación no dependa de un vendor específico.
Al terminar vas a poder elegir el KMS correcto para tu setup, implementar el acceso desde Python, y tener un path claro de migración desde .env.
AWS Secrets Manager
AWS Secrets Manager es el servicio más adoptado para secrets management en la nube. Ofrece almacenamiento encriptado, rotación automática con Lambda, y control de acceso vía IAM.
Setup
pip install boto3
Para desarrollo local, configura credenciales de AWS:
aws configure
# O usa variables de entorno:
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_DEFAULT_REGION="us-east-1"
Operaciones con boto3
import boto3
import json
from datetime import datetime
from typing import Optional
from botocore.exceptions import ClientError
class AWSSecretsClient:
"""Cliente para AWS Secrets Manager con operaciones comunes."""
def __init__(self, region_name: str = "us-east-1"):
self.client = boto3.client("secretsmanager", region_name=region_name)
self.region = region_name
def create_secret(self, name: str, value: dict, description: str = "") -> dict:
try:
response = self.client.create_secret(
Name=name,
Description=description,
SecretString=json.dumps(value),
Tags=[
{"Key": "managed-by", "Value": "secrets-management-module"},
{"Key": "created-at", "Value": datetime.utcnow().isoformat()},
],
)
return {
"status": "created",
"arn": response["ARN"],
"name": response["Name"],
"version_id": response["VersionId"],
}
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceExistsException":
return self.update_secret(name, value)
raise
def get_secret(self, name: str) -> Optional[dict]:
try:
response = self.client.get_secret_value(SecretId=name)
secret_data = json.loads(response["SecretString"])
return {
"data": secret_data,
"version_id": response["VersionId"],
"created_date": str(response.get("CreatedDate", "")),
}
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code in ("ResourceNotFoundException", "DecryptionFailureException"):
return None
raise
def update_secret(self, name: str, value: dict) -> dict:
response = self.client.update_secret(
SecretId=name,
SecretString=json.dumps(value),
)
return {
"status": "updated",
"arn": response["ARN"],
"version_id": response["VersionId"],
}
def delete_secret(self, name: str, force: bool = False) -> dict:
kwargs = {"SecretId": name}
if force:
kwargs["ForceDeleteWithoutRecovery"] = True
else:
kwargs["RecoveryWindowInDays"] = 7
response = self.client.delete_secret(**kwargs)
return {
"status": "scheduled_deletion" if not force else "deleted",
"name": response["Name"],
"deletion_date": str(response.get("DeletionDate", "")),
}
def list_secrets(self, prefix: str = "") -> list[dict]:
paginator = self.client.get_paginator("list_secrets")
secrets = []
for page in paginator.paginate():
for secret in page["SecretList"]:
if prefix and not secret["Name"].startswith(prefix):
continue
secrets.append({
"name": secret["Name"],
"arn": secret["ARN"],
"last_changed": str(secret.get("LastChangedDate", "")),
"last_accessed": str(secret.get("LastAccessedDate", "")),
})
return secrets
def configure_rotation(
self, name: str, lambda_arn: str, days: int = 30
) -> dict:
response = self.client.rotate_secret(
SecretId=name,
RotationLambdaARN=lambda_arn,
RotationRules={"AutomaticallyAfterDays": days},
)
return {
"status": "rotation_configured",
"version_id": response["VersionId"],
"rotation_days": days,
}
# Ejemplo de uso (requiere credenciales AWS configuradas):
# aws_client = AWSSecretsClient(region_name="us-east-1")
#
# aws_client.create_secret(
# "prod/llm/openai",
# {"api_key": "sk-proj-abc123", "org_id": "org-xyz"},
# description="OpenAI API key for production"
# )
#
# secret = aws_client.get_secret("prod/llm/openai")
# print(f"API Key: {secret['data']['api_key']}")
Costo de AWS Secrets Manager
aws_pricing = {
"per_secret_per_month": 0.40,
"per_10k_api_calls": 0.05,
"examples": [
{"secrets": 5, "calls_per_month": 10000, "cost": 5 * 0.40 + 1 * 0.05},
{"secrets": 20, "calls_per_month": 100000, "cost": 20 * 0.40 + 10 * 0.05},
{"secrets": 50, "calls_per_month": 500000, "cost": 50 * 0.40 + 50 * 0.05},
],
}
print("AWS Secrets Manager Pricing:")
print(f" ${aws_pricing['per_secret_per_month']}/secret/month")
print(f" ${aws_pricing['per_10k_api_calls']}/10K API calls")
print("\nEstimated monthly costs:")
for ex in aws_pricing["examples"]:
print(f" {ex['secrets']} secrets, {ex['calls_per_month']:,} calls → ${ex['cost']:.2f}/month")
# Output esperado:
# AWS Secrets Manager Pricing:
# $0.4/secret/month
# $0.05/10K API calls
#
# Estimated monthly costs:
# 5 secrets, 10,000 calls → $2.05/month
# 20 secrets, 100,000 calls → $8.50/month
# 50 secrets, 500,000 calls → $22.50/month
GCP Secret Manager
Google Cloud Secret Manager es la opción nativa de GCP con integración directa con IAM y Cloud Audit Logs.
Setup
pip install google-cloud-secret-manager
Para desarrollo local:
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"
# O usa gcloud auth:
gcloud auth application-default login
Operaciones con google-cloud-secret-manager
from typing import Optional
import json
class GCPSecretsClient:
"""Cliente para GCP Secret Manager con operaciones comunes."""
def __init__(self, project_id: str):
from google.cloud import secretmanager
self.client = secretmanager.SecretManagerServiceClient()
self.project_id = project_id
self.parent = f"projects/{project_id}"
def create_secret(self, secret_id: str, value: dict) -> dict:
from google.cloud import secretmanager
from google.api_core.exceptions import AlreadyExists
try:
secret = self.client.create_secret(
request={
"parent": self.parent,
"secret_id": secret_id,
"secret": {
"replication": {"automatic": {}},
"labels": {"managed-by": "secrets-management-module"},
},
}
)
except AlreadyExists:
pass
payload = json.dumps(value).encode("UTF-8")
version = self.client.add_secret_version(
request={
"parent": f"{self.parent}/secrets/{secret_id}",
"payload": {"data": payload},
}
)
return {
"status": "created",
"name": version.name,
"state": version.state.name,
}
def get_secret(self, secret_id: str, version: str = "latest") -> Optional[dict]:
from google.api_core.exceptions import NotFound
try:
name = f"{self.parent}/secrets/{secret_id}/versions/{version}"
response = self.client.access_secret_version(request={"name": name})
secret_data = json.loads(response.payload.data.decode("UTF-8"))
return {
"data": secret_data,
"version": response.name.split("/")[-1],
}
except NotFound:
return None
def delete_secret(self, secret_id: str) -> bool:
from google.api_core.exceptions import NotFound
try:
name = f"{self.parent}/secrets/{secret_id}"
self.client.delete_secret(request={"name": name})
return True
except NotFound:
return False
def list_secrets(self, prefix: str = "") -> list[dict]:
secrets = []
for secret in self.client.list_secrets(request={"parent": self.parent}):
name = secret.name.split("/")[-1]
if prefix and not name.startswith(prefix):
continue
secrets.append({
"name": name,
"full_name": secret.name,
"created": str(secret.create_time),
"labels": dict(secret.labels),
})
return secrets
# Ejemplo de uso (requiere credenciales GCP configuradas):
# gcp_client = GCPSecretsClient(project_id="my-ai-project")
#
# gcp_client.create_secret("openai-api-key", {"api_key": "sk-proj-abc123"})
#
# secret = gcp_client.get_secret("openai-api-key")
# print(f"API Key: {secret['data']['api_key']}")
Azure Key Vault
Azure Key Vault es la opción nativa de Azure con integración con Azure Active Directory y RBAC.
Setup
pip install azure-keyvault-secrets azure-identity
Operaciones con azure-keyvault-secrets
import json
from typing import Optional
class AzureSecretsClient:
"""Cliente para Azure Key Vault con operaciones comunes."""
def __init__(self, vault_url: str):
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
self.client = SecretClient(vault_url=vault_url, credential=credential)
self.vault_url = vault_url
def create_secret(self, name: str, value: dict, content_type: str = "application/json") -> dict:
secret = self.client.set_secret(
name=name,
value=json.dumps(value),
content_type=content_type,
tags={"managed-by": "secrets-management-module"},
)
return {
"status": "created",
"name": secret.name,
"version": secret.properties.version,
"created_on": str(secret.properties.created_on),
}
def get_secret(self, name: str, version: str = "") -> Optional[dict]:
from azure.core.exceptions import ResourceNotFoundError
try:
if version:
secret = self.client.get_secret(name, version)
else:
secret = self.client.get_secret(name)
secret_data = json.loads(secret.value)
return {
"data": secret_data,
"version": secret.properties.version,
"created_on": str(secret.properties.created_on),
}
except ResourceNotFoundError:
return None
def delete_secret(self, name: str) -> bool:
from azure.core.exceptions import ResourceNotFoundError
try:
self.client.begin_delete_secret(name).wait()
return True
except ResourceNotFoundError:
return False
def list_secrets(self, prefix: str = "") -> list[dict]:
secrets = []
for prop in self.client.list_properties_of_secrets():
if prefix and not prop.name.startswith(prefix):
continue
secrets.append({
"name": prop.name,
"enabled": prop.enabled,
"created_on": str(prop.created_on),
"updated_on": str(prop.updated_on),
"tags": dict(prop.tags) if prop.tags else {},
})
return secrets
# Ejemplo de uso (requiere credenciales Azure configuradas):
# azure_client = AzureSecretsClient(
# vault_url="https://my-vault.vault.azure.net"
# )
#
# azure_client.create_secret("openai-api-key", {"api_key": "sk-proj-abc123"})
#
# secret = azure_client.get_secret("openai-api-key")
# print(f"API Key: {secret['data']['api_key']}")
Comparación detallada: AWS vs GCP vs Azure
comparison = {
"headers": ["Feature", "AWS Secrets Manager", "GCP Secret Manager", "Azure Key Vault"],
"rows": [
["Encryption", "AES-256 (AWS KMS)", "AES-256 (Google KMS)", "AES-256 (Azure KMS)"],
["Versioning", "Staging labels", "Numbered versions", "Named versions"],
["Rotation", "Lambda-based auto", "Cloud Functions (manual setup)", "Managed identity rotation"],
["Audit", "CloudTrail", "Cloud Audit Logs", "Azure Monitor"],
["Access Control", "IAM policies", "IAM + conditions", "Azure RBAC"],
["Pricing (per secret/mo)", "$0.40", "$0.06", "$0.03/10K ops"],
["API Calls pricing", "$0.05/10K", "$0.03/10K", "Included in ops"],
["Max secret size", "64 KB", "64 KB", "25 KB (secret), 128 KB (cert)"],
["Cross-region", "Replication available", "Automatic replication", "Geo-replication"],
["Python SDK", "boto3", "google-cloud-secret-manager", "azure-keyvault-secrets"],
["Free tier", "No (30-day trial)", "6 active versions free", "10K ops free/month"],
["Best for", "AWS-native apps", "GCP-native, cost-sensitive", "Azure/Microsoft shops"],
],
}
print(f"| {' | '.join(comparison['headers'])} |")
print(f"| {' | '.join(['---'] * len(comparison['headers']))} |")
for row in comparison["rows"]:
print(f"| {' | '.join(row)} |")
Patrón de Interface Unificada
El patrón más importante de esta cápsula: una interface unificada que abstrae el proveedor de secrets. Tu aplicación no debería saber si usa AWS, GCP, Azure, o Vault:
import os
import json
import time
import logging
from abc import ABC, abstractmethod
from typing import Optional
from dataclasses import dataclass, field
logger = logging.getLogger("secrets_client")
@dataclass
class SecretResult:
key: str
value: Optional[str]
found: bool
provider: str
version: str = ""
cached: bool = False
access_time_ms: float = 0.0
class SecretsProvider(ABC):
"""Interface unificada para cualquier secrets provider."""
@abstractmethod
def get(self, key: str) -> SecretResult:
pass
@abstractmethod
def set(self, key: str, value: dict) -> bool:
pass
@abstractmethod
def delete(self, key: str) -> bool:
pass
@abstractmethod
def list(self, prefix: str = "") -> list[str]:
pass
@property
@abstractmethod
def provider_name(self) -> str:
pass
class EnvProvider(SecretsProvider):
"""Provider para desarrollo local usando variables de entorno."""
def get(self, key: str) -> SecretResult:
start = time.perf_counter()
value = os.environ.get(key)
elapsed = (time.perf_counter() - start) * 1000
return SecretResult(
key=key, value=value, found=value is not None,
provider="env", access_time_ms=elapsed,
)
def set(self, key: str, value: dict) -> bool:
os.environ[key] = json.dumps(value) if isinstance(value, dict) else str(value)
return True
def delete(self, key: str) -> bool:
if key in os.environ:
del os.environ[key]
return True
return False
def list(self, prefix: str = "") -> list[str]:
return [k for k in os.environ if k.startswith(prefix)]
@property
def provider_name(self) -> str:
return "environment"
class AWSProvider(SecretsProvider):
"""Provider para AWS Secrets Manager."""
def __init__(self, region: str = "us-east-1"):
import boto3
self.client = boto3.client("secretsmanager", region_name=region)
def get(self, key: str) -> SecretResult:
start = time.perf_counter()
try:
response = self.client.get_secret_value(SecretId=key)
data = json.loads(response["SecretString"])
elapsed = (time.perf_counter() - start) * 1000
return SecretResult(
key=key,
value=data.get("value", json.dumps(data)),
found=True,
provider="aws",
version=response.get("VersionId", ""),
access_time_ms=elapsed,
)
except Exception:
elapsed = (time.perf_counter() - start) * 1000
return SecretResult(
key=key, value=None, found=False,
provider="aws", access_time_ms=elapsed,
)
def set(self, key: str, value: dict) -> bool:
try:
self.client.create_secret(Name=key, SecretString=json.dumps(value))
return True
except self.client.exceptions.ResourceExistsException:
self.client.update_secret(SecretId=key, SecretString=json.dumps(value))
return True
except Exception:
return False
def delete(self, key: str) -> bool:
try:
self.client.delete_secret(SecretId=key, ForceDeleteWithoutRecovery=True)
return True
except Exception:
return False
def list(self, prefix: str = "") -> list[str]:
secrets = []
paginator = self.client.get_paginator("list_secrets")
for page in paginator.paginate():
for s in page["SecretList"]:
if not prefix or s["Name"].startswith(prefix):
secrets.append(s["Name"])
return secrets
@property
def provider_name(self) -> str:
return "aws_secrets_manager"
class CachedProvider(SecretsProvider):
"""Wrapper que agrega caching a cualquier provider."""
def __init__(self, provider: SecretsProvider, ttl_seconds: int = 300):
self._provider = provider
self._ttl = ttl_seconds
self._cache: dict[str, tuple[SecretResult, float]] = {}
def get(self, key: str) -> SecretResult:
if key in self._cache:
result, cached_at = self._cache[key]
if time.time() - cached_at < self._ttl:
result.cached = True
return result
del self._cache[key]
result = self._provider.get(key)
if result.found:
self._cache[key] = (result, time.time())
return result
def set(self, key: str, value: dict) -> bool:
success = self._provider.set(key, value)
if success:
self._cache.pop(key, None)
return success
def delete(self, key: str) -> bool:
success = self._provider.delete(key)
self._cache.pop(key, None)
return success
def list(self, prefix: str = "") -> list[str]:
return self._provider.list(prefix)
@property
def provider_name(self) -> str:
return f"cached({self._provider.provider_name})"
def clear_cache(self):
self._cache.clear()
class FallbackProvider(SecretsProvider):
"""Intenta múltiples providers en orden — para migración gradual."""
def __init__(self, providers: list[SecretsProvider]):
self._providers = providers
def get(self, key: str) -> SecretResult:
for provider in self._providers:
result = provider.get(key)
if result.found:
return result
return SecretResult(
key=key, value=None, found=False,
provider="fallback_exhausted",
)
def set(self, key: str, value: dict) -> bool:
return self._providers[0].set(key, value)
def delete(self, key: str) -> bool:
return self._providers[0].delete(key)
def list(self, prefix: str = "") -> list[str]:
all_keys = set()
for provider in self._providers:
all_keys.update(provider.list(prefix))
return sorted(all_keys)
@property
def provider_name(self) -> str:
names = [p.provider_name for p in self._providers]
return f"fallback({', '.join(names)})"
def create_secrets_provider(
provider_type: str = "env",
cache_ttl: int = 300,
**kwargs,
) -> SecretsProvider:
"""Factory function para crear el provider apropiado."""
providers = {
"env": lambda: EnvProvider(),
"aws": lambda: AWSProvider(region=kwargs.get("region", "us-east-1")),
}
if provider_type not in providers:
raise ValueError(f"Unknown provider: {provider_type}. Options: {list(providers.keys())}")
provider = providers[provider_type]()
if cache_ttl > 0:
provider = CachedProvider(provider, ttl_seconds=cache_ttl)
return provider
os.environ["OPENAI_API_KEY"] = "sk-proj-from-env"
provider = create_secrets_provider("env", cache_ttl=60)
result = provider.get("OPENAI_API_KEY")
print(f"Provider: {provider.provider_name}")
print(f"Key: {result.key}, Value: {result.value[:15]}..., Found: {result.found}")
result2 = provider.get("OPENAI_API_KEY")
print(f"Cached: {result2.cached}")
# Output esperado:
# Provider: cached(environment)
# Key: OPENAI_API_KEY, Value: sk-proj-from-en..., Found: True
# Cached: True
Migración de .env a Cloud KMS
Plan de migración paso a paso
migration_plan = {
"phase_1_preparation": {
"duration": "1-2 days",
"steps": [
"Inventariar todos los secrets en .env",
"Clasificar por riesgo (CRITICAL, HIGH, MEDIUM, LOW)",
"Elegir provider (AWS, GCP, Azure) según tu infraestructura",
"Crear cuenta/proyecto en el provider",
"Configurar IAM roles/policies",
],
},
"phase_2_abstraction": {
"duration": "1 day",
"steps": [
"Implementar SecretsProvider interface en tu código",
"Reemplazar os.getenv() con secrets_provider.get()",
"Usar FallbackProvider: cloud → env (para backward compatibility)",
"Verificar que la app funciona igual con el nuevo abstraction layer",
],
},
"phase_3_migration": {
"duration": "1-2 days",
"steps": [
"Crear secrets en el cloud provider (empezar con CRITICAL)",
"Deploy con FallbackProvider: cloud primero, env como backup",
"Monitorear: verificar que secrets se leen del cloud",
"Migrar HIGH y MEDIUM secrets",
"Verificar audit trail en el cloud provider",
],
},
"phase_4_cleanup": {
"duration": "1 day",
"steps": [
"Remover secrets de .env (dejar solo LOW/non-sensitive config)",
"Remover FallbackProvider → usar solo cloud provider",
"Configurar rotation schedules",
"Documentar la nueva arquitectura de secrets",
"Configurar alertas de rotación",
],
},
}
print("Migration Plan: .env → Cloud KMS")
for phase, info in migration_plan.items():
print(f"\n📋 {phase.replace('_', ' ').title()} ({info['duration']})")
for i, step in enumerate(info["steps"], 1):
print(f" {i}. {step}")
Script de migración
import os
import json
from typing import Optional
def parse_env_file(filepath: str) -> dict[str, str]:
"""Parsea un archivo .env y retorna un diccionario de secrets."""
secrets = {}
try:
with open(filepath, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
key, value = line.split("=", 1)
secrets[key.strip()] = value.strip().strip('"').strip("'")
except FileNotFoundError:
pass
return secrets
def classify_secrets(secrets: dict[str, str]) -> dict[str, list[str]]:
"""Clasifica secrets por nivel de riesgo basado en el nombre."""
critical_patterns = ["api_key", "secret_key", "password", "token"]
high_patterns = ["database", "redis", "url"]
medium_patterns = ["webhook", "smtp", "sentry"]
classified = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": []}
for key in secrets:
key_lower = key.lower()
if any(p in key_lower for p in critical_patterns):
classified["CRITICAL"].append(key)
elif any(p in key_lower for p in high_patterns):
classified["HIGH"].append(key)
elif any(p in key_lower for p in medium_patterns):
classified["MEDIUM"].append(key)
else:
classified["LOW"].append(key)
return classified
env_content = """
OPENAI_API_KEY=sk-proj-abc123
ANTHROPIC_API_KEY=sk-ant-xyz789
DATABASE_URL=postgresql://user:pass@host:5432/db
DATABASE_PASSWORD=super-secret
REDIS_URL=redis://:pass@host:6379
SENTRY_DSN=https://key@sentry.io/123
APP_ENV=production
LOG_LEVEL=info
PORT=8000
""".strip()
with open(".env.example", "w") as f:
f.write(env_content)
secrets = parse_env_file(".env.example")
classified = classify_secrets(secrets)
print("Secret Classification:")
for level, keys in classified.items():
if keys:
print(f"\n {level}:")
for key in keys:
print(f" - {key}")
print(f"\nMigration priority:")
print(f" 1. CRITICAL ({len(classified['CRITICAL'])} secrets) → Migrate first")
print(f" 2. HIGH ({len(classified['HIGH'])} secrets) → Migrate second")
print(f" 3. MEDIUM ({len(classified['MEDIUM'])} secrets) → Migrate third")
print(f" 4. LOW ({len(classified['LOW'])} secrets) → Keep in env vars")
os.remove(".env.example")
# Output esperado:
# Secret Classification:
# CRITICAL:
# - OPENAI_API_KEY
# - ANTHROPIC_API_KEY
# - DATABASE_PASSWORD
# HIGH:
# - DATABASE_URL
# - REDIS_URL
# MEDIUM:
# - SENTRY_DSN
# LOW:
# - APP_ENV
# - LOG_LEVEL
# - PORT
Choosing the right KMS
decision_tree = {
"question_1": {
"q": "¿En qué cloud provider corres tu aplicación?",
"aws": "→ AWS Secrets Manager (integración nativa, IAM)",
"gcp": "→ GCP Secret Manager (más barato, Cloud Audit Logs)",
"azure": "→ Azure Key Vault (Azure AD, RBAC nativo)",
"multi_cloud": "→ HashiCorp Vault (provider-agnostic)",
"on_premises": "→ HashiCorp Vault (self-hosted)",
},
"question_2": {
"q": "¿Cuántos secrets gestionas?",
"less_than_10": "Cloud KMS es más que suficiente (~$2-4/mes)",
"10_to_50": "Cloud KMS sigue siendo práctico (~$5-20/mes)",
"50_plus": "Evalúa Vault si necesitas dynamic secrets",
},
"question_3": {
"q": "¿Necesitas dynamic secrets (credenciales temporales)?",
"yes": "→ HashiCorp Vault (único que soporta esto nativamente)",
"no": "→ Cloud KMS con rotation automática es suficiente",
},
}
print("Decision Tree: ¿Qué KMS usar?")
for name, tree in decision_tree.items():
print(f"\n{tree['q']}")
for option, recommendation in tree.items():
if option != "q":
print(f" {option}: {recommendation}")
Troubleshooting
"boto3 da NoCredentialsError"
Verifica que tienes credenciales AWS configuradas: aws configure o variables de entorno AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY. Para desarrollo local, usa AWS SSO o profiles.
"GCP da PermissionDenied"
El service account necesita el rol roles/secretmanager.secretAccessor para leer secrets y roles/secretmanager.admin para crear/eliminar. Verifica en IAM & Admin.
"Azure da AuthenticationError"
DefaultAzureCredential intenta múltiples métodos de autenticación en orden. Para desarrollo local, usa az login. Para producción, usa Managed Identity.
"La migración rompió mi app en producción"
Usa FallbackProvider durante la migración: el cloud provider como primera opción y env vars como backup. Así, si el cloud provider falla o un secret no se migró, la app sigue funcionando con el valor de .env.
"No puedo testear localmente sin credenciales cloud"
Usa EnvProvider para desarrollo local y AWSProvider/GCPProvider para staging/producción. La interface unificada hace que el switch sea transparente.
Ejercicios
Ejercicio 1: Implementa un GCPProvider
Siguiendo el patrón de AWSProvider, implementa un GCPProvider que implemente la interface SecretsProvider:
Ver solución
class GCPProvider(SecretsProvider):
def __init__(self, project_id: str):
from google.cloud import secretmanager
self.client = secretmanager.SecretManagerServiceClient()
self.project_id = project_id
self.parent = f"projects/{project_id}"
def get(self, key: str) -> SecretResult:
start = time.perf_counter()
try:
name = f"{self.parent}/secrets/{key}/versions/latest"
response = self.client.access_secret_version(request={"name": name})
value = response.payload.data.decode("UTF-8")
elapsed = (time.perf_counter() - start) * 1000
return SecretResult(key=key, value=value, found=True, provider="gcp", access_time_ms=elapsed)
except Exception:
elapsed = (time.perf_counter() - start) * 1000
return SecretResult(key=key, value=None, found=False, provider="gcp", access_time_ms=elapsed)
def set(self, key: str, value: dict) -> bool:
try:
from google.api_core.exceptions import AlreadyExists
try:
self.client.create_secret(request={
"parent": self.parent,
"secret_id": key,
"secret": {"replication": {"automatic": {}}},
})
except AlreadyExists:
pass
self.client.add_secret_version(request={
"parent": f"{self.parent}/secrets/{key}",
"payload": {"data": json.dumps(value).encode("UTF-8")},
})
return True
except Exception:
return False
def delete(self, key: str) -> bool:
try:
self.client.delete_secret(request={"name": f"{self.parent}/secrets/{key}"})
return True
except Exception:
return False
def list(self, prefix: str = "") -> list[str]:
secrets = []
for s in self.client.list_secrets(request={"parent": self.parent}):
name = s.name.split("/")[-1]
if not prefix or name.startswith(prefix):
secrets.append(name)
return secrets
@property
def provider_name(self) -> str:
return "gcp_secret_manager"
Ejercicio 2: Migra 5 secrets de .env a un provider
Escribe un script que lea un .env, clasifique los secrets, y los migre al provider que elijas:
Ver solución
def migrate_env_to_provider(env_path: str, provider: SecretsProvider) -> dict:
secrets = parse_env_file(env_path)
classified = classify_secrets(secrets)
results = {"migrated": [], "skipped": [], "failed": []}
for level in ["CRITICAL", "HIGH", "MEDIUM"]:
for key in classified.get(level, []):
try:
success = provider.set(key, {"value": secrets[key]})
if success:
results["migrated"].append({"key": key, "level": level})
else:
results["failed"].append({"key": key, "level": level})
except Exception as e:
results["failed"].append({"key": key, "error": str(e)})
for key in classified.get("LOW", []):
results["skipped"].append({"key": key, "reason": "LOW risk — keep in env"})
return results
provider = EnvProvider()
# En producción usarías: provider = AWSProvider() o GCPProvider()
Ejercicio 3: Implementa un health check para el secrets provider
Crea un endpoint que verifique la conectividad con el secrets provider:
Ver solución
def health_check(provider: SecretsProvider) -> dict:
start = time.perf_counter()
try:
result = provider.get("__health_check__")
elapsed = (time.perf_counter() - start) * 1000
return {
"provider": provider.provider_name,
"status": "healthy",
"response_time_ms": round(elapsed, 2),
"note": "Secret not found is expected for health check",
}
except Exception as e:
elapsed = (time.perf_counter() - start) * 1000
return {
"provider": provider.provider_name,
"status": "unhealthy",
"response_time_ms": round(elapsed, 2),
"error": str(e),
}
check = health_check(EnvProvider())
print(json.dumps(check, indent=2))
Ejercicio 4: Compara latencia entre providers
Escribe un benchmark que compare la latencia de lectura entre EnvProvider y CachedProvider:
Ver solución
def benchmark_provider(provider: SecretsProvider, key: str, iterations: int = 100) -> dict:
times = []
for _ in range(iterations):
start = time.perf_counter()
provider.get(key)
elapsed = (time.perf_counter() - start) * 1000
times.append(elapsed)
return {
"provider": provider.provider_name,
"iterations": iterations,
"avg_ms": round(sum(times) / len(times), 4),
"min_ms": round(min(times), 4),
"max_ms": round(max(times), 4),
"p99_ms": round(sorted(times)[int(len(times) * 0.99)], 4),
}
os.environ["BENCH_KEY"] = "test-value"
env = EnvProvider()
cached = CachedProvider(env, ttl_seconds=60)
cached.get("BENCH_KEY")
print("Benchmark Results:")
for result in [benchmark_provider(env, "BENCH_KEY"), benchmark_provider(cached, "BENCH_KEY")]:
print(f" {result['provider']}: avg={result['avg_ms']}ms, p99={result['p99_ms']}ms")
Resumen
- Los tres principales cloud KMS (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) ofrecen almacenamiento encriptado, versioning, y audit — sin infraestructura que operar
- AWS Secrets Manager es el más maduro con rotation automática vía Lambda y la mayor adopción
- GCP Secret Manager es el más económico y tiene integración directa con Cloud Audit Logs
- Azure Key Vault se integra nativamente con Azure AD y RBAC para organizaciones Microsoft
- El patrón de interface unificada (
SecretsProvider) abstrae el proveedor para que tu app no dependa de un vendor específico - CachedProvider agrega caching con TTL para reducir latencia y llamadas al servicio cloud
- FallbackProvider permite migración gradual: cloud como primario, env como backup
- La migración de .env a cloud KMS es incremental: clasificar → abstraer → migrar → limpiar
- Costo mensual para la mayoría de proyectos AI: $2-20/mes — significativamente menor que el costo de una key filtrada
Próxima cápsula: En la cápsula 06 vas a aprender sobre token lifecycle management y audit trails — cómo gestionar el ciclo de vida completo de tus secrets (creación, distribución, rotación, revocación) y cómo registrar cada acceso para compliance y detección de incidentes.
Recursos
- AWS Secrets Manager Documentation — Documentación oficial del servicio de secrets de AWS
- GCP Secret Manager Documentation — Documentación oficial del servicio de secrets de Google Cloud
- Azure Key Vault Documentation — Documentación oficial del servicio de secrets de Azure
- boto3 Secrets Manager Reference — Referencia del SDK Python para AWS
- google-cloud-secret-manager PyPI — Paquete Python para GCP Secret Manager
- azure-keyvault-secrets PyPI — Paquete Python para Azure Key Vault
- AWS Secrets Manager Pricing — Detalles de precios de AWS para planificación de costos
- Multi-Cloud Secrets Management (CNCF) — Guía CNCF para gestión de secrets en entornos multi-cloud
Creado: Marzo 2026 Versión: 1.0