Patrones de gestión de secretos para aplicaciones nativas de la nube
Gestión robusta de secretos con HashiCorp Vault, AWS Secrets Manager y sealed secrets: rotación, inyección, mínimo privilegio y sin dispersión.

La dispersión de secretos es la crisis de seguridad silenciosa de la mayoría de las organizaciones. Claves de API en variables de entorno, contraseñas de bases de datos en archivos de configuración incluidos en repositorios, tokens compartidos en mensajes de Slack: cada uno de ellos es una superficie de ataque esperando a ser explotada. La gestión moderna de secretos no consiste solo en cifrar valores. Se trata de controlar el acceso, habilitar la rotación, mantener registros de auditoría y evitar que los secretos terminen en lugares donde no deberían estar.
Los patrones que se presentan aquí cubren todo el ciclo de vida: cómo se crean, almacenan, entregan a las aplicaciones, rotan y revocan los secretos, sin introducir tanta complejidad operativa que los equipos terminen evitando el sistema por completo.
El catálogo de antipatrones de secretos
Antes de implementar soluciones, conviene reconocer los patrones que generan esa exposición.
# ❌ Secrets embedded in configuration
# docker-compose.yml
services:
api:
environment:
DB_PASSWORD: "super_secret_password_123"
API_KEY: "sk-live-abc123def456"
JWT_SECRET: "my-jwt-signing-key"
# Visible in process listings, docker inspect,
# version control history, CI/CD logs# ✅ Secrets referenced, not embedded
# docker-compose.yml
services:
api:
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
API_KEY_FILE: /run/secrets/api_key
secrets:
- db_password
- api_key
secrets:
db_password:
external: true
api_key:
external: true
# Secrets injected at runtime, never in config filesEl patrón de inyección basado en archivos (sufijo _FILE) es compatible de forma nativa con muchas imágenes de Docker. La aplicación lee el secreto desde un archivo al iniciar, en lugar de hacerlo desde una variable de entorno, lo que evita su exposición en listados de procesos y volcados de memoria.
Integración con HashiCorp Vault
Vault ofrece secretos dinámicos, rotación automática y políticas de acceso de grano fino. El patrón arquitectónico clave consiste en usar tokens de corta duración con permisos acotados.
// Vault client with token renewal and secret lease management
import Vault from "node-vault";
interface VaultConfig {
endpoint: string;
roleId: string;
secretId: string;
}
class SecretManager {
private client: ReturnType<typeof Vault>;
private leases: Map<string, { leaseId: string; ttl: number }> =
new Map();
constructor(private config: VaultConfig) {
this.client = Vault({
apiVersion: "v1",
endpoint: config.endpoint,
});
}
async authenticate(): Promise<void> {
const result = await this.client.approleLogin({
role_id: this.config.roleId,
secret_id: this.config.secretId,
});
this.client.token = result.auth.client_token;
// Schedule token renewal before expiry
const renewalInterval =
(result.auth.lease_duration * 0.75) * 1000;
setInterval(() => this.renewToken(), renewalInterval);
}
private async renewToken(): Promise<void> {
try {
await this.client.tokenRenewSelf();
} catch {
// Token renewal failed — re-authenticate
await this.authenticate();
}
}
async getDatabaseCredentials(
role: string
): Promise<{ username: string; password: string }> {
const result = await this.client.read(
`database/creds/${role}`
);
// Track lease for renewal
this.leases.set(role, {
leaseId: result.lease_id,
ttl: result.lease_duration,
});
return {
username: result.data.username,
password: result.data.password,
};
}
async getSecret(path: string): Promise<Record<string, string>> {
const result = await this.client.read(
`secret/data/${path}`
);
return result.data.data;
}
async revokeAllLeases(): Promise<void> {
for (const [, lease] of this.leases) {
await this.client.write("sys/leases/revoke", {
lease_id: lease.leaseId,
});
}
this.leases.clear();
}
}
// Usage with automatic credential rotation
const vault = new SecretManager({
endpoint: process.env.VAULT_ADDR ?? "http://vault:8200",
roleId: process.env.VAULT_ROLE_ID ?? "",
secretId: process.env.VAULT_SECRET_ID ?? "",
});
await vault.authenticate();
// Dynamic database credentials — unique per instance,
// automatically expired
const dbCreds = await vault.getDatabaseCredentials(
"api-readonly"
);Las credenciales dinámicas de base de datos son la característica estrella de Vault. Cada instancia de la aplicación obtiene su propio usuario y contraseña con un TTL configurable. Cuando el lease expira, Vault revoca las credenciales a nivel de base de datos. Una credencial comprometida solo es válida durante horas, no para siempre.
Sealed Secrets de Kubernetes
Para los equipos que quieren mantener los secretos en Git sin la sobrecarga operativa de Vault, Sealed Secrets cifra valores que solo el clúster puede descifrar.
# ❌ Kubernetes Secret — base64 encoded, not encrypted
apiVersion: v1
kind: Secret
metadata:
name: api-credentials
data:
api-key: c2stbGl2ZS1hYmMxMjNkZWY0NTY=
# base64 is NOT encryption. Anyone with repo access
# can decode this.# ✅ Sealed Secret — encrypted, safe to commit
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: api-credentials
namespace: production
spec:
encryptedData:
api-key: AgBy3i4OJSWK+PiTySYZZA9r...
# Encrypted with the cluster's public key
# Only the sealed-secrets controller can decrypt
template:
metadata:
name: api-credentials
type: Opaque# Workflow: encrypt locally, commit safely
# Fetch the cluster's public cert
kubeseal --fetch-cert \
--controller-namespace kube-system \
> pub-cert.pem
# Encrypt a secret value
echo -n "sk-live-abc123" | kubeseal \
--raw \
--from-file=/dev/stdin \
--namespace production \
--name api-credentials \
--cert pub-cert.pem
# The output is safe to commit to GitPatrón de inyección de secretos a nivel de aplicación
Independientemente del backend que se utilice, la aplicación debe seguir un patrón coherente para recibir los secretos.
// ❌ Scattered secret access throughout codebase
async function handleRequest(req: Request) {
const apiKey = process.env.STRIPE_API_KEY; // Where does this come from?
const dbUrl = process.env.DATABASE_URL; // Who rotates this?
// Secrets scattered across handler files
}// ✅ Centralized secret provider with lazy loading
interface SecretProvider {
get(key: string): Promise<string>;
getAll(keys: string[]): Promise<Map<string, string>>;
}
class CompositeSecretProvider implements SecretProvider {
constructor(
private providers: SecretProvider[],
) {}
async get(key: string): Promise<string> {
for (const provider of this.providers) {
try {
return await provider.get(key);
} catch {
continue;
}
}
throw new Error(`Secret not found: ${key}`);
}
async getAll(
keys: string[]
): Promise<Map<string, string>> {
const results = new Map<string, string>();
for (const key of keys) {
results.set(key, await this.get(key));
}
return results;
}
}
// File-based provider for Docker/Kubernetes secrets
class FileSecretProvider implements SecretProvider {
constructor(private basePath: string = "/run/secrets") {}
async get(key: string): Promise<string> {
const filePath = `${this.basePath}/${key}`;
const content = await fs.readFile(filePath, "utf-8");
return content.trim();
}
async getAll(
keys: string[]
): Promise<Map<string, string>> {
const results = new Map<string, string>();
for (const key of keys) {
results.set(key, await this.get(key));
}
return results;
}
}
// Environment variable provider as fallback
class EnvSecretProvider implements SecretProvider {
async get(key: string): Promise<string> {
const value = process.env[key];
if (!value) {
throw new Error(`Environment variable ${key} not set`);
}
return value;
}
async getAll(
keys: string[]
): Promise<Map<string, string>> {
const results = new Map<string, string>();
for (const key of keys) {
results.set(key, await this.get(key));
}
return results;
}
}
// Application bootstrap: resolve all secrets once at startup
const secrets = new CompositeSecretProvider([
new FileSecretProvider(),
new EnvSecretProvider(),
]);
const config = {
stripeKey: await secrets.get("STRIPE_API_KEY"),
databaseUrl: await secrets.get("DATABASE_URL"),
jwtSecret: await secrets.get("JWT_SECRET"),
};
// Pass config to services — no secret lookups in handlersRotación de secretos sin tiempo de inactividad
La parte más difícil de la gestión de secretos es la rotación: cambiar un secreto sin interrumpir los servicios en ejecución.
// Dual-read pattern for zero-downtime rotation
interface RotatableSecret {
current: string;
previous: string | null;
rotatedAt: Date;
}
class RotatingApiKeyValidator {
private keys: RotatableSecret;
constructor(keys: RotatableSecret) {
this.keys = keys;
}
validate(providedKey: string): boolean {
// Accept both current and previous during rotation window
if (providedKey === this.keys.current) return true;
if (
this.keys.previous &&
providedKey === this.keys.previous
) {
// Log that a client is still using the old key
console.warn(
"Request using previous API key — client needs update"
);
return true;
}
return false;
}
}
// Rotation procedure:
// 1. Generate new secret, store as "current"
// 2. Move old "current" to "previous"
// 3. Deploy — services accept both keys
// 4. Update all clients to use new key
// 5. After grace period, remove "previous"Conclusiones clave
Los secretos incluidos en variables de entorno, archivos de configuración o código fuente crean superficies de ataque a través de listados de procesos, inspección de contenedores, volcados de memoria e historial de control de versiones: en su lugar, use inyección basada en archivos o referencias a un vault. Los secretos dinámicos de HashiCorp Vault generan credenciales únicas por instancia de aplicación con expiración automática, lo que limita el radio de impacto de un compromiso al TTL de la credencial. Sealed Secrets permite almacenar secretos cifrados en Git de forma segura: solo el controlador sealed-secrets del clúster de Kubernetes puede descifrarlos, lo que habilita flujos de trabajo GitOps sin exponer los valores. Una abstracción centralizada de SecretProvider desacopla la aplicación del backend de almacenamiento de secretos, lo que permite usar secretos basados en archivos en producción y variables de entorno en desarrollo sin cambiar el código. La rotación sin tiempo de inactividad utiliza un patrón de doble lectura: aceptar tanto el valor actual como el anterior del secreto durante un período de gracia, dando tiempo a los clientes para actualizarse mientras se mantiene la continuidad del servicio.


