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
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.
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');
}
} Preguntas frecuentes
¿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.
Recursos Relacionados
Gestionar Secretos de Aplicaciones de Forma Segura
Cómo almacenar, rotar e inyectar API keys, contraseñas de base de datos y certificados sin hardcodearlos en código fuente o archivos de entorno.
RecipeCabeceras de seguridad HTTP
Fortalece aplicaciones web con HTTP security headers: CSP, HSTS, X-Frame-Options y una lista de verificación completa de headers de seguridad.
GuideGuía de Mejores Prácticas de Seguridad
Una Referencia Detallada de seguridad de aplicaciones: autenticación, autorización, validación de inputs, gestión de secretos y prevención de vulnerabilidades comunes.
RecipePreviene inyeccion SQL con consultas parametrizadas en
Protege aplicaciones Python de inyeccion SQL usando consultas parametrizadas de SQLAlchemy, modelos ORM, validacion de input e inspeccion de queries para acceso seguro a base de datos
RecipePrevenir Ataques de Inyección SQL
Cómo escribir queries parametrizadas y usar ORMs para eliminar vulnerabilidades de inyección SQL en Python, JavaScript y Java.
PatternPatrón Multi-Tenant Data Isolation
Aisla datos de tenants en infraestructura compartida usando row-level security, schema-per-tenant o database-per-tenant. Un patrón para aplicaciones SaaS.