El problema real#
Hace unos meses tenía un desastre de configuración en mi servidor doméstico. Las contraseñas de bases de datos estaban hardcodeadas en variables de entorno, distribuidas en varios contenedores, y cambiarlas era un dolor de cabeza. Un día necesitaba rotar las credenciales y tuve que reiniciar todo manualmente. Necesitaba algo mejor.
La solución que implementé fue usar HashiCorp Vault como servidor centralizado de secretos con rotación automática. Aquí documento exactamente cómo lo hice.
Instalación de Vault#
Primero levanté Vault como contenedor:
version: '3.8'
services:
vault:
image: vault:latest
container_name: vault
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: "s.xxxxxxxxxx"
VAULT_DEV_LISTEN_ADDRESS: "0.0.0.0:8200"
volumes:
- vault_data:/vault/data
cap_add:
- IPC_LOCK
networks:
- servicios
volumes:
vault_data:
networks:
servicios:
driver: bridgeNota importante: Esto es modo desarrollo. Para producción necesitarías configuración de almacenamiento persistente y TLS adecuado. En mi caso en servidor doméstico uso modo dev pero con volumen persistente.
Configuración de Vault#
Una vez corriendo, inicialicé Vault:
docker exec -it vault vault operator init
docker exec -it vault vault operator unseal <key>Luego habilité el motor de secretos dinámicos para PostgreSQL (que es lo que uso en mi setup):
docker exec -it vault vault secrets enable databaseConfiguré la conexión a PostgreSQL:
docker exec -it vault vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
allowed_roles="readonly,readwrite" \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb" \
username="vault" \
password="vault_pass_strong"Creación de roles y credenciales dinámicas#
Definí roles con permisos específicos:
docker exec -it vault vault write database/roles/readonly \
db_name=postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"Las contraseñas se generan automáticamente y expiran según el TTL configurado. Vault renueva el rol antes de la expiración.
Consumo de secretos en contenedores#
Para que mis aplicaciones accedieran a los secretos, usé Vault Agent como sidecar. Ejemplo con una app Node:
services:
app:
image: node:18
container_name: app
depends_on:
- vault
environment:
VAULT_ADDR: "http://vault:8200"
VAULT_TOKEN: "s.xxxxxxxxxx"
volumes:
- ./vault-agent.hcl:/etc/vault/agent.hcl
- ./app:/app
networks:
- servicios
command: sh -c "vault agent -config=/etc/vault/agent.hcl & sleep 2 && node /app/index.js"El archivo vault-agent.hcl:
vault {
address = "http://vault:8200"
}
auto_auth {
method {
type = "token"
config = {
token = "s.xxxxxxxxxx"
}
}
}
cache {
use_auto_auth_token = true
}
listener "unix" {
address = "/tmp/vault.sock"
tls_disable = true
}
template {
source = "/etc/vault/templates/db.tpl"
destination = "/tmp/secrets.env"
command = "kill -HUP $(pgrep -f 'node /app')"
}Rotación automática#
Vault rota automáticamente las credenciales. Mi aplicación detecta cambios leyendo el archivo generado:
const fs = require('fs');
const path = require('path');
const secretsPath = '/tmp/secrets.env';
function reloadSecrets() {
const secrets = fs.readFileSync(secretsPath, 'utf8');
// Parsear y aplicar nuevas credenciales
Object.assign(process.env, parseEnv(secrets));
console.log('[SECRETS] Credenciales rotadas exitosamente');
}
fs.watchFile(secretsPath, reloadSecrets);Resultados#
Implementar esto me llevó un par de tardes, pero ahora:
- Centralización: Todos los secretos en un único lugar
- Rotación automática: Sin intervención manual
- Auditoría: Vault registra quién accede a qué secretos
- Escalabilidad: Agregar nuevos contenedores es trivial
¿Qué falta? Hacer backup de los datos de Vault y pensar en recuperación ante desastres, pero eso es otro artículo.