Credenciales en Vivo de Base de Datos con HashiCorp Vault
Como usar HashiCorp Vault para generar credenciales de base de datos de corta duracion, eliminando passwords hardcodeados y reduciendo secret sprawl
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Credenciales en Vivo de Base de Datos con HashiCorp Vault
Las credenciales de base de datos hardcodeadas en archivos de configuracion son un riesgo persistente de seguridad. HashiCorp Vault resuelve esto generando credenciales gestionadas en vivo que se crean bajo demanda y se revocan automaticamente despues de un TTL configurable.
Cuando Usar Esto
- Quieres eliminar passwords estaticas de base de datos de la configuracion de aplicaciones
- La rotacion de credenciales debe ocurrir sin reinicios de aplicacion
- Necesitas un audit trail de cada acceso a base de datos con atribucion de usuario
Requisitos Previos
- Servidor Vault ejecutandose (modo dev aceptable para testing)
- Base de datos PostgreSQL o MySQL
- Token de Vault con permisos para configurar el database secrets engine
Solucion
1. Habilitar el Database Secrets Engine
vault secrets enable database
2. Configurar Conexion a Base de Datos
vault write database/config/postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="app" \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb" \
username="vaultadmin" \
password="vaultadmin-password"
3. Crear un Rol en Vivo
vault write database/roles/app \
db_name=postgres \
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"
4. Solicitar Credenciales en Vivo
// vault-client.ts
import vault from 'node-vault';
const client = vault({ apiVersion: 'v1', endpoint: 'http://localhost:8200' });
export async function getDatabaseCredentials() {
const result = await client.read('database/creds/app');
return {
username: result.data.username,
password: result.data.password,
leaseId: result.lease_id,
leaseDuration: result.lease_duration,
};
}
5. Integracion de Aplicacion con Renovacion de Lease
// db/ConnectionPool.ts
import { getDatabaseCredentials } from './vault-client';
import { Pool } from 'pg';
class ManagedConnectionPool {
private pool: Pool | null = null;
private leaseTimer: NodeJS.Timeout | null = null;
async initialize() {
const creds = await getDatabaseCredentials();
this.pool = new Pool({
host: 'localhost',
database: 'mydb',
user: creds.username,
password: creds.password,
max: 20,
});
// Renueva o rota antes de que expire el lease
const renewalMs = (creds.leaseDuration - 60) * 1000;
this.leaseTimer = setTimeout(() => this.rotate(), renewalMs);
}
private async rotate() {
await this.pool?.end();
await this.initialize();
}
async query(sql: string, params: unknown[]) {
return this.pool!.query(sql, params);
}
async close() {
if (this.leaseTimer) clearTimeout(this.leaseTimer);
await this.pool?.end();
}
}
6. Revocar Credenciales al Apagar
// Graceful shutdown handler
process.on('SIGTERM', async () => {
await connectionPool.close();
await vault.revoke({ lease_id: currentLeaseId });
process.exit(0);
});
Como Funciona
- Database Plugin se conecta a PostgreSQL con credenciales admin
- Role Definition especifica SQL de creacion con username y password templated
- Credential Request dispara a Vault para crear un nuevo rol en PostgreSQL
- TTL Enforcement elimina automaticamente el rol despues de la expiracion
- Lease Renewal extiende o reemplaza credenciales antes de la expiracion
Consideraciones de Produccion
- Ejecuta Vault en modo HA con almacenamiento Raft para ambientes de produccion
- Usa AppRole o Kubernetes auth en lugar de tokens de larga duracion
- Habilita audit devices para loggear cada generacion de credenciales y acceso
- Configura max_ttl para enforcear duracion maxima de sesion sin importar renovacion
Errores Comunes
- Olvidar revocar leases, dejando roles huérfanos en base de datos
- Configurar TTL demasiado corto, causando churn excesivo de credenciales
- No manejar indisponibilidad de Vault gracefulmente en la aplicacion. Consulta respuesta a incidentes on-call.
FAQ
P: Que pasa si Vault esta caido cuando la app necesita credenciales? R: La aplicacion deberia fallar al iniciar o caer a un connection pool cacheado. Para sistemas criticos, ejecuta Vault en modo HA con multiples replicas.
P: Puede Vault rotar tambien la password admin estatica?
R: Si. Usa vault write database/rotate-root/postgres para rotar las credenciales root que Vault usa para gestionar roles en vivo.
P: Funciona esto con connection pooling? R: Si, pero el pool debe recrearse cuando las credenciales rotan. Usa un factory pattern que gestione el ciclo de vida del pool junto con el TTL del lease.
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Soluciones Avanzadas
Cliente hvac de Python con auth AppRole
import hvac
import os
from typing import TypedDict
class DBCredentials(TypedDict):
username: str
password: str
lease_id: str
lease_duration: int
class VaultClient:
"""Cliente de Vault con autenticación AppRole y cacheo de credenciales."""
def __init__(self, vault_addr: str, role_id: str, secret_id: str):
self.client = hvac.Client(url=vault_addr)
self._authenticate(role_id, secret_id)
self._cached_creds: DBCredentials | None = None
def _authenticate(self, role_id: str, secret_id: str):
"""Autenticar usando AppRole (identidad de máquina)."""
resp = self.client.auth.approle.login(
role_id=role_id,
secret_id=secret_id,
)
self.client.token = resp['auth']['client_token']
def get_db_credentials(self, role: str = 'app') -> DBCredentials:
"""Solicitar credenciales de base de datos de corta duración a Vault."""
resp = self.client.read(f'database/creds/{role}')
creds = DBCredentials(
username=resp['data']['username'],
password=resp['data']['password'],
lease_id=resp['lease_id'],
lease_duration=resp['lease_duration'],
)
self._cached_creds = creds
return creds
def renew_lease(self, lease_id: str, increment: int = 3600):
"""Renovar un lease antes de que expire."""
self.client.sys.renew_lease(
lease_id=lease_id,
increment=increment,
)
def revoke_lease(self, lease_id: str):
"""Revocar credenciales cuando ya no se necesiten."""
self.client.sys.revoke_lease(lease_id=lease_id)
# Uso
vault = VaultClient(
vault_addr=os.environ['VAULT_ADDR'],
role_id=os.environ['VAULT_ROLE_ID'],
secret_id=os.environ['VAULT_SECRET_ID'],
)
creds = vault.get_db_credentials()
# Usar creds para conectar a PostgreSQL...
# Al apagar:
vault.revoke_lease(creds['lease_id'])
Método de auth Kubernetes
Cuando ejecutas en Kubernetes, usa el backend de auth de Kubernetes para que los pods se autentiquen con su token de service account en lugar de secretos compartidos:
import hvac
def authenticate_kubernetes(vault_addr: str, role: str, jwt_path: str = '/var/run/secrets/kubernetes.io/serviceaccount/token'):
"""Autenticar a Vault usando el token de service account de Kubernetes."""
client = hvac.Client(url=vault_addr)
with open(jwt_path, 'r') as f:
jwt = f.read().strip()
resp = client.auth.kubernetes.login(
role=role,
jwt=jwt,
)
client.token = resp['auth']['client_token']
return client
# Configuración admin de Vault (una vez):
# vault auth enable kubernetes
# vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
# vault write auth/kubernetes/role/database-app \
# bound_service_account_names=app-sa \
# bound_service_account_namespaces=production \
# policies=database-access \
# ttl=1h
Configuración multi-rol con separación lectura/escritura
Crea roles separados de Vault con diferentes privilegios de base de datos para enforcear least-privilege:
# Rol de solo lectura para analytics / reporting
vault write database/roles/app-readonly \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="4h" \
max_ttl="24h"
# Rol de lectura-escritura para mutaciones de aplicación
vault write database/roles/app-readwrite \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="8h"
# Rol de migración con privilegios DDL (TTL corto, solicitud manual)
vault write database/roles/app-migration \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\"; \
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="15m" \
max_ttl="1h"
# Solicitar las credenciales correctas para la tarea
def get_readonly_creds(vault: VaultClient) -> DBCredentials:
return vault.get_db_credentials(role='app-readonly')
def get_readwrite_creds(vault: VaultClient) -> DBCredentials:
return vault.get_db_credentials(role='app-readwrite')
def get_migration_creds(vault: VaultClient) -> DBCredentials:
# TTL corto, siempre revocadas inmediatamente después de la migración
creds = vault.get_db_credentials(role='app-migration')
return creds
Cacheo de credenciales con fallback (Node.js)
Cuando Vault no está disponible temporalmente, usa credenciales cacheadas con un warning:
import vault from 'node-vault';
import { Pool } from 'pg';
const client = vault({ apiVersion: 'v1', endpoint: process.env.VAULT_ADDR });
interface CachedCreds {
username: string;
password: string;
leaseId: string;
leaseDuration: number;
fetchedAt: number;
}
let cached: CachedCreds | null = null;
const MAX_CACHE_AGE = 2 * 60 * 60 * 1000; // 2 horas
async function getCredsWithFallback(): Promise<CachedCreds> {
try {
const result = await client.read('database/creds/app');
cached = {
username: result.data.username,
password: result.data.password,
leaseId: result.lease_id,
leaseDuration: result.lease_duration,
fetchedAt: Date.now(),
};
return cached;
} catch (err) {
if (cached && Date.now() - cached.fetchedAt < MAX_CACHE_AGE) {
console.warn('Vault no disponible, usando credenciales cacheadas', {
age: Date.now() - cached.fetchedAt,
leaseId: cached.leaseId,
});
return cached;
}
throw new Error('Vault no disponible y no hay credenciales cacheadas válidas');
}
}
Mejores Prácticas Adicionales
- Usa Vault Agent para inyección de credenciales sidecar. En lugar de que el código de la aplicación llame a Vault directamente, despliega Vault Agent como sidecar que escribe credenciales a un archivo que la aplicación lee:
# vault-agent.hcl
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "database-app"
}
}
sink "file" {
config = {
path = "/vault/token"
}
}
}
template {
source = "/vault/templates/db-creds.tpl"
destination = "/vault/secrets/db-creds.json"
}
template_config {
static_secret_render_interval = "5m"
}
// /vault/templates/db-creds.tpl
{{ with secret "database/creds/app" }}
{
"username": "{{ .Data.username }}",
"password": "{{ .Data.password }}",
"leaseId": "{{ .LeaseID }}",
"leaseDuration": {{ .LeaseDuration }}
}
{{ end }}
- Monitorea conteo de leases y eventos de revocación. Trackea métricas para detectar leases huérfanos o churn de credenciales:
def check_lease_health(vault_client, max_leases: int = 100):
"""Monitorear leases activos y alertar sobre anomalías."""
leases = vault_client.sys.list_leases()
active_count = len(leases)
if active_count > max_leases:
logging.warning(
f'Alto conteo de leases: {active_count} (max: {max_leases})'
)
# Verificar leases antiguos que deberían haber sido revocados
for lease in leases:
# Alertar si el lease es más antiguo de lo esperado
pass
return {'active_leases': active_count}
Errores Comunes Adicionales
- Usar un solo token de Vault para todos los servicios. Cada servicio debería autenticarse independientemente con su propia identidad (AppRole, rol de Kubernetes, AWS IAM). Un token compartido significa que comprometer un servicio compromete todos:
# INCORRECTO: compartir un token entre servicios
export VAULT_TOKEN=s.shared-token
# CORRECTO: cada servicio obtiene su propio rol
vault write auth/approle/role/api-server token_ttl=1h token_max_ttl=4h
vault write auth/approle/role/worker token_ttl=1h token_max_ttl=4h
- No configurar
max_ttlen los roles. Sinmax_ttl, los leases renovados pueden persistir indefinidamente. Configura un techo duro para enforcear re-autenticación periódica:
# Bien: 1h por defecto, 8h max (fuerza re-auth al menos cada 8h)
vault write database/roles/app \
db_name=postgres \
creation_statements="..." \
default_ttl="1h" \
max_ttl="8h"
Preguntas Frecuentes Adicionales
¿Cómo roto las credenciales root de base de datos de Vault?
Usa el endpoint rotate-root. Vault genera una nueva password para la cuenta admin y la almacena internamente. La password anterior se descarta:
vault write -force database/rotate-root/postgres
Programa esta rotación trimestralmente o después de cambios de personal. Vault maneja la rotación sin downtime — usa las credenciales actuales para generar una nueva password, luego actualiza su almacenamiento interno.
¿Puedo usar credenciales dinámicas de Vault con Redis o MongoDB?
Sí. Vault soporta Redis, MongoDB, Cassandra y otras bases de datos a través de plugins. El patrón de configuración es el mismo: habilitar el database engine, configurar la conexión, crear un rol con creation statements. Para bases de datos NoSQL, las creation statements usan los comandos nativos de gestión de usuarios de la base de datos (ej: db.createUser() para MongoDB).
¿Cuál es el overhead de las credenciales dinámicas?
Cada solicitud de credenciales crea un rol de base de datos, que es una operación ligera. Para PostgreSQL, la creación de rol toma 1-5ms. El overhead principal es la recreación del connection pool cuando rotan las credenciales. Mitiga usando TTLs más largos (1-4 horas) y renovando leases en lugar de solicitar nuevas credenciales.
Recursos Relacionados
Manage Application Secrets Securely
How to store, rotate, and inject API keys, database passwords, and certificates without hardcoding them in source code or environment files.
RecipeSecurity Headers
Harden web applications with HTTP security headers: CSP, HSTS, X-Frame-Options, and a thorough security header checklist.
GuideSecurity Best Practices Guide
A thorough guide to application security: authentication, authorization, input validation, secrets management, and common vulnerability prevention.
RecipePrevent SQL Injection with SQLAlchemy Parameterized Queries
Protect Python applications from SQL injection using SQLAlchemy parameterized queries, ORM models, input validation, and query inspection to ensure safe database access
RecipePrevent SQL Injection Attacks
How to write parameterized queries and use ORMs to eliminate SQL injection vulnerabilities across Python, JavaScript, and Java.
PatternMulti-Tenant Data Isolation Pattern
Isolate tenant data in shared infrastructure using row-level security, schema-per-tenant, or database-per-tenant strategies. A pattern for SaaS applications.