El problema real#
Descubrí que una de mis credenciales de base de datos estaba expuesta en un repositorio público. No fue divertido. Lo peor no fue el acceso no autorizado, sino darme cuenta de que rotar esa credencial significaría derribar todos mis contenedores en producción. No tenía tiempo para eso.
Así que decidí implementar un sistema de rotación de secretos sin downtime. Aquí está lo que hice.
Por qué Vault#
HashiCorp Vault centraliza la gestión de secretos. En lugar de tener credenciales hardcodeadas o en variables de entorno, mis contenedores ahora las solicitan a Vault en tiempo de ejecución. Cuando cambia un secreto, no necesito redesplegable nada.
Arquitectura de la solución#
La idea es simple:
- Vault almacena todos los secretos
- Los contenedores leen de Vault, no de variables de entorno
- Cuando un secreto se compromete, lo roto en Vault
- Un webhook notifica a los contenedores activos
- Los contenedores reconectan con nuevas credenciales sin reiniciarse
Instalación de Vault#
Instalé Vault localmente con Docker:
version: '3.8'
services:
vault:
image: vault:latest
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: "myroot-token"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
command: server -dev
volumes:
- vault_data:/vault/file
networks:
- servicios
volumes:
vault_data:
networks:
servicios:
driver: bridgeAccedí a http://localhost:8200 y activé el motor de secretos KV:
vault secrets enable -version=2 kv
vault kv put kv/database/prod username=dbuser password=oldpassword host=db.localAdaptación del cliente Docker#
Mis aplicaciones ahora incluyen un cliente que se conecta a Vault. Usé un sidecar en Python para este propósito:
import hvac
import os
import signal
import time
class VaultClient:
def __init__(self):
self.client = hvac.Client(url='http://vault:8200', token=os.getenv('VAULT_TOKEN'))
self.secret_path = os.getenv('SECRET_PATH', 'kv/database/prod')
self.cache = {}
def get_secret(self):
try:
response = self.client.secrets.kv.read_secret_version(path=self.secret_path)
return response['data']['data']
except Exception as e:
print(f"Error reading secret: {e}")
return self.cache.get('last_known_good', {})
def watch_for_updates(self, callback):
while True:
new_secret = self.get_secret()
if new_secret != self.cache:
self.cache = new_secret
callback(new_secret)
time.sleep(10)Mi aplicación no se reinicia, simplemente recibe la notificación y reconecta:
def on_secret_change(new_creds):
global db_connection
print(f"Rotating credentials...")
db_connection.close()
db_connection = connect_database(new_creds)
print(f"New connection established")
vault = VaultClient()
thread = threading.Thread(target=vault.watch_for_updates, args=(on_secret_change,), daemon=True)
thread.start()Rotación real#
Cuando descubrí que un secreto estaba comprometido, simplemente ejecuté:
vault kv put kv/database/prod username=dbuser password=newpassword host=db.localMi sidecar detectó el cambio en el siguiente ciclo de verificación (máximo 10 segundos después). La aplicación cerró la conexión anterior y estableció una nueva con las credenciales actualizadas.
Sin downtime. Sin redesplegable.
Webhook para urgencias#
Para situaciones críticas, implementé un webhook que Vault llama cuando se rotarota un secreto:
vault write -f auth/token/renew
# Trigger webhook manualmente
curl -X POST http://localhost:8000/rotate-secret \
-H "Content-Type: application/json" \
-d '{"secret_path":"kv/database/prod"}'El webhook puede forzar una reconexión inmediata en lugar de esperar al siguiente ciclo de verificación.
Lecciones aprendidas#
- No confíes en variables de entorno para secretos sensibles
- La rotación sin downtime es posible si planificas desde el inicio
- Vault tiene overhead, pero vale la pena por la seguridad
- Mantén un mecanismo de fallback en caso de que Vault falle
Ahora duermo mejor sabiendo que puedo rotar cualquier credencial comprometida en segundos sin afectar mis servicios.