Cifrado en Reposo: AES-256, KMS y Cifrado Envolvente
Cifra los datos almacenados correctamente: AES-256-GCM, claves gestionadas por KMS, cifrado envolvente, rotación de claves y cifrado a nivel de campo.
Introducción
Un backup de base de datos termina en un bucket S3 público. Un snapshot de EBS se comparte con la cuenta equivocada. Un disco dado de baja sale del data center sin borrar. En todos esos casos el atacante nunca toca tu aplicación: se lleva el almacenamiento en bruto. El cifrado en reposo es lo que convierte ese almacenamiento en algo inútil para él: sin la clave, el ciphertext es ruido.
Lo que no protege es igual de importante saberlo de entrada. Si tu aplicación descifra datos bajo demanda, quien comprometa la app obtiene texto plano. El cifrado en reposo defiende la capa de almacenamiento — discos, snapshots, backups, exportaciones — no la vía de acceso. Encima sigues necesitando autenticación, autorización y defensas contra inyección.
Esta guía cubre las piezas que necesitas en producción: AES-256-GCM como cifrador, un servicio de gestión de claves (KMS) para la custodia, cifrado envolvente para que las llamadas al KMS no te salgan caras, cifrado a nivel de campo para las columnas más sensibles y una estrategia de rotación de claves que no te obligue a reescribir todo después.
Capas de cifrado
“Cifrar los datos” puede significar cuatro cosas distintas según dónde lo apliques. Cada capa defiende contra una amenaza diferente y ninguna sustituye por completo a las demás:
- Nivel de campo es la única capa que oculta datos a alguien con acceso legítimo a la base de datos. Un DBA que ejecuta
SELECT *sobre la tabla users ve ciphertext en las columnas sensibles. - Nivel de base de datos (TDE) hace ilegibles los archivos en disco, pero cualquiera que se autentique ve texto plano: el motor descifra de forma transparente.
- Nivel de volumen y hardware suelen ser gratis (activa el cifrado de EBS, LUKS o confía en los discos autocifrados) y solo cubren el robo físico. Actívalos en todas partes, pero no los tomes como respuesta a la exposición de datos.
La regla práctica: activa las capas baratas de abajo por defecto y gasta tu presupuesto de esfuerzo en el cifrado a nivel de campo para las columnas que realmente dolería perder.
Cuándo usarlo
El cifrado en reposo no es una decisión binaria: es por campo y por almacén. Aplícalo cuando:
- Almacenas datos regulados. Datos de titulares de tarjeta (PCI-DSS), historiales médicos (HIPAA) o datos personales de residentes de la UE (RGPD, art. 32 “medidas técnicas apropiadas”) empujan hacia el cifrado a nivel de campo con claves gestionadas.
- Operas un SaaS multi-tenant. Las claves por tenant permiten crypto-shredding de un inquilino (borras su clave y su ciphertext es irrecuperable) y acotan el radio de daño de una clave comprometida.
- Los backups o exportaciones salen de tu control. Los dumps de base de datos, los archivos de logs y las réplicas en object storage son la vía clásica de fuga. Cifra antes de que salgan, no después.
- El modelo de amenazas incluye insiders. Si “un DBA o un ingeniero de soporte lee tablas de producción” está en tu modelo, el cifrado transparente de base de datos no hace nada: necesitas cifrado a nivel de aplicación.
Y los casos donde aporta poco:
- Campos en los que necesitas buscar, ordenar o hacer join. El ciphertext no se puede indexar de forma útil. O usas un blind index (más abajo) o dejas el campo sin cifrar.
- Datos no sensibles, públicos por diseño. Cifrarlo todo añade sobrecarga de gestión de claves, complica el debugging y difumina qué datos realmente necesitan protección.
- Datos efímeros con TTL corto. Las cachés de sesión y los payloads de colas suelen cargar menos riesgo que el coste de complejidad de cifrarlos — aunque los marcos de compliance pueden exigirlo igualmente, así que revisa tus obligaciones primero.
AES-256-GCM
AES-256 en modo Galois/Counter es la opción por defecto para cifrar datos en reposo. GCM es una construcción AEAD: una sola pasada te da confidencialidad más una etiqueta de autenticación que hace fallar el descifrado si se modificó un solo bit del ciphertext. Compáralo con AES-CBC, que acepta en silencio entrada manipulada salvo que le añadas un HMAC aparte — fuente de ataques reales de bit-flipping y padding oracle.
Tres detalles importan en la práctica:
- La unicidad del IV es obligatoria. Un IV aleatorio de 96 bits (12 bytes) por cada cifrado. Reusar un IV con la misma clave es catastrófico en GCM: rompe la confidencialidad y permite al atacante recuperar la subclave de autenticación para falsificar mensajes.
crypto.randomByteste da un IV con respaldo CSPRNG — no derives IVs de timestamps ni contadores sin pensarlo bien. - La etiqueta de autenticación viaja con el ciphertext. Necesitas
tag(16 bytes) al descifrar, víadecipher.setAuthTag. Guárdala junto al ciphertext y al IV. - AAD vincula el ciphertext a su contexto.
cipher.setAAD()autentica metadatos (tenant ID, nombre de tabla, versión de clave) sin cifrarlos: un atacante no puede mover ciphertext entre filas o tenants sin ser detectado.
// crypto/aesGcm.ts — Cifrado y descifrado AES-256-GCM
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12; // IV de 96 bits para GCM (recomendado)
const TAG_LENGTH = 16; // Etiqueta de autenticación de 128 bits
export interface EncryptedData {
ciphertext: string; // base64
iv: string; // base64
tag: string; // base64
keyVersion: number; // para la rotación de claves
}
export function encrypt(plaintext: string, key: Buffer, keyVersion: number = 1): EncryptedData {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return {
ciphertext: ciphertext.toString('base64'),
iv: iv.toString('base64'),
tag: tag.toString('base64'),
keyVersion,
};
}
export function decrypt(data: EncryptedData, key: Buffer): string {
const decipher = crypto.createDecipheriv(
ALGORITHM,
key,
Buffer.from(data.iv, 'base64'),
);
decipher.setAuthTag(Buffer.from(data.tag, 'base64'));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(data.ciphertext, 'base64')),
decipher.final(),
]);
return plaintext.toString('utf8');
}
Si los nonces te ponen nervioso — por ejemplo, claves cifrando miles de millones de registros donde hasta una colisión de IV aleatorio se vuelve plausible — XChaCha20-Poly1305 (nonce de 192 bits, resistente al mal uso) es la alternativa habitual, disponible vía libsodium en lugar del crypto nativo de Node.
Servicios de gestión de claves (KMS)
El cifrador es la parte fácil; la clave es el problema. Una clave de cifrado guardada en una variable de entorno, un archivo de configuración o la misma base de datos que protege está a un dump de env del compromiso total. Un KMS mantiene la clave dentro de un servicio endurecido y entrega operaciones de cifrado/descifrado en lugar del material de la clave.
Las opciones principales, con las diferencias que de verdad deciden la elección:
| Servicio | Con respaldo HSM | Rotación automática | BYOK | A vigilar |
|---|---|---|---|---|
| AWS KMS | Sí (FIPS 140-2 L3) | Anual, opt-in por clave | Sí | El precio por petición crece con la escala |
| GCP Cloud KMS | Sí (FIPS 140-2 L3; L1 en software) | Periodo configurable | Sí | Key rings separados por región |
| Azure Key Vault | Sí (tier managed HSM) | Política configurable | Sí | El modelo de versionado difiere — planifica el código de rotación |
| HashiCorp Vault | Vía seal HSM PKCS#11 | El transit engine soporta rotación | Sí (import) | Lo operas y parcheas tú — más control, más trabajo de ops |
Los cuatro soportan el mismo patrón central: tu app maneja una clave de datos y el KMS guarda la clave maestra que la protege. La variante de AWS se ve así:
// kms/KmsClient.ts — Interfaz abstracta de KMS
interface KmsClient {
encrypt(dataKey: Buffer): Promise<{ ciphertext: string; keyVersion: number }>;
decrypt(ciphertext: string, keyVersion: number): Promise<Buffer>;
rotateKey(): Promise<void>;
}
// Implementación con AWS KMS
import { KMSClient, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms';
class AwsKmsClient implements KmsClient {
private client = new KMSClient({ region: 'us-east-1' });
private keyId = process.env.KMS_KEY_ID!;
async encrypt(dataKey: Buffer): Promise<{ ciphertext: string; keyVersion: number }> {
const command = new EncryptCommand({
KeyId: this.keyId,
Plaintext: dataKey,
});
const response = await this.client.send(command);
return {
ciphertext: Buffer.from(response.CiphertextBlob!).toString('base64'),
keyVersion: 1, // KMS versiona internamente
};
}
async decrypt(ciphertext: string, _keyVersion: number): Promise<Buffer> {
const command = new DecryptCommand({
CiphertextBlob: Buffer.from(ciphertext, 'base64'),
});
const response = await this.client.send(command);
return Buffer.from(response.Plaintext!);
}
async rotateKey(): Promise<void> {
// AWS KMS rota las claves anualmente si está habilitado
// La rotación manual crea una nueva versión de clave
}
}
Dos cosas a notar en este diseño. Primero, DecryptCommand no necesita el ID de la clave: AWS KMS lo deduce del blob de ciphertext, así que el keyVersion de tu lado es sobre todo contabilidad. Segundo, activar la rotación automática en la clave del KMS rota solo la clave cifradora de claves; tus claves de datos y el ciphertext quedan intactos, que es justo lo que hace tan cómodo el patrón siguiente. Para el ciclo de vida completo de los secretos (políticas de rotación, patrones de acceso, auditoría), la guía de gestión de secretos entra en más detalle.
Cifrado envolvente (envelope encryption)
Llamar a un KMS por red en cada encrypt/decrypt no escala: AWS KMS cobra por petición y aplica cuotas de peticiones por segundo por clave. El cifrado envolvente lo evita repartiendo el trabajo entre dos claves:
- Una clave de cifrado de datos (DEK) — una clave aleatoria de 256 bits generada localmente — hace el trabajo real de AES-256-GCM.
- Una clave cifradora de claves (KEK) — la clave maestra gestionada por el KMS — cifra la propia DEK.
La DEK cifrada viaja con el ciphertext. Descifrar significa una llamada al KMS para desenvolver la DEK y luego criptografía local:
// crypto/EnvelopeEncryption.ts — Patrón de cifrado envolvente
class EnvelopeEncryption {
constructor(private readonly kms: KmsClient) {}
async encrypt(plaintext: string): Promise<EnvelopeEncryptedData> {
// 1. Generar una clave de cifrado de datos (DEK)
const dek = crypto.randomBytes(32); // DEK de 256 bits
// 2. Cifrar los datos con la DEK
const encrypted = encrypt(plaintext, dek);
// 3. Cifrar la DEK con el KMS (KEK)
const encryptedDek = await this.kms.encrypt(dek);
// 4. Poner a cero la DEK en memoria
dek.fill(0);
return {
...encrypted,
encryptedDek: encryptedDek.ciphertext,
dekKeyVersion: encryptedDek.keyVersion,
};
}
async decrypt(data: EnvelopeEncryptedData): Promise<string> {
// 1. Descifrar la DEK usando el KMS
const dek = await this.kms.decrypt(data.encryptedDek, data.dekKeyVersion);
// 2. Descifrar los datos con la DEK
const plaintext = decrypt(
{ ciphertext: data.ciphertext, iv: data.iv, tag: data.tag, keyVersion: data.keyVersion },
dek,
);
// 3. Poner a cero la DEK
dek.fill(0);
return plaintext;
}
}
interface EnvelopeEncryptedData {
ciphertext: string;
iv: string;
tag: string;
keyVersion: number;
encryptedDek: string;
dekKeyVersion: number;
}
La pregunta de granularidad — ¿cuántas DEKs? — es un trade-off real. Una DEK por registro (o por campo) da el aislamiento más fino: filtrar una DEK expone un registro. Una DEK por tenant es más barata de rotar y sigue acotando el radio de daño. Una sola DEK para toda la tabla es lo más simple pero convierte una fuga en exposición total. Para columnas sensibles como el SSN, las DEK por registro justifican las llamadas extra al KMS; para el grueso de datos de un tenant, la opción por tenant suele ser el punto medio correcto.
Cifrado de bases de datos
El cifrado transparente de datos (TDE) — disponible en PostgreSQL vía extensiones y nativo en SQL Server, Oracle y bases de datos gestionadas en la nube — cifra los archivos en disco. Vale la pena activarlo porque es prácticamente gratis, pero recuerda lo que no hace: cualquiera con credenciales válidas ve texto plano, porque el motor descifra las páginas al leerlas. TDE responde a “robaron el directorio de datos”, no a “alguien tiene acceso a la base de datos”.
Para esa segunda amenaza cifras en la capa de aplicación, antes de que los valores lleguen a SQL:
-- Guardar datos cifrados en columnas
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
encrypted_ssn BYTEA, -- número de seguridad social cifrado
encrypted_phone BYTEA, -- teléfono cifrado
encryption_metadata JSONB -- { iv, tag, keyVersion, encryptedDek }
);
-- Insertar datos cifrados
INSERT INTO users (email, encrypted_ssn, encrypted_phone, encryption_metadata)
VALUES (
'user@example.com',
'\x' || encode(encrypted_ssn_bytes, 'hex'),
'\x' || encode(encrypted_phone_bytes, 'hex'),
'{"iv": "...", "tag": "...", "keyVersion": 1, "encryptedDek": "..."}'::jsonb
);
// db/EncryptedField.ts — Cifrado transparente de campos en la capa ORM
class EncryptedField {
constructor(private readonly envelopeEncryption: EnvelopeEncryption) {}
async encryptField(value: string): Promise<string> {
const encrypted = await this.envelopeEncryption.encrypt(value);
return JSON.stringify(encrypted);
}
async decryptField(storedValue: string): Promise<string> {
const data = JSON.parse(storedValue) as EnvelopeEncryptedData;
return this.envelopeEncryption.decrypt(data);
}
}
// Repositorio con cifrado transparente
class UserRepository {
constructor(
private readonly db: Database,
private readonly encryptedField: EncryptedField,
) {}
async createUser(data: CreateUserInput): Promise<User> {
const encryptedSsn = await this.encryptedField.encryptField(data.ssn);
const encryptedPhone = await this.encryptedField.encryptField(data.phone);
const result = await this.db.query(
`INSERT INTO users (email, encrypted_ssn, encrypted_phone, encryption_metadata)
VALUES ($1, $2, $3, $4) RETURNING id, email`,
[data.email, encryptedSsn, encryptedPhone, null],
);
return result.rows[0];
}
async getUser(id: string): Promise<User> {
const result = await this.db.query(
`SELECT * FROM users WHERE id = $1`,
[id],
);
const row = result.rows[0];
if (!row) return null;
return {
id: row.id,
email: row.email,
ssn: await this.encryptedField.decryptField(row.encrypted_ssn),
phone: await this.encryptedField.decryptField(row.encrypted_phone),
};
}
}
Buscar en campos cifrados: blind indexes
En el momento en que cifras una columna pierdes WHERE email = ?. La solución estándar es un blind index: una columna extra con HMAC-SHA256(claveIndice, valorNormalizado). Las búsquedas por igualdad hashean el valor de la query y comparan con la columna índice — la base de datos solo ve hashes que no puede revertir. La pega es real: HMAC es determinista, así que plaintexts idénticos producen índices idénticos y filtran frecuencia (si el 40% de las filas comparte un SSN, eso se ve). Usa blind indexes solo para búsquedas de baja cardinalidad que realmente necesites (email, account_id), nunca para campos de alta entropía, y nunca como sustituto del valor cifrado con AEAD. Rangos, ordenación y LIKE siguen siendo imposibles — diseña tus patrones de acceso alrededor de ese límite. Para una implementación lista para copiar del flujo cifrar/guardar, la receta de cifrado en reposo cubre lo básico.
Rotación de claves
Las claves caducan: algoritmos deprecados, sospecha de exposición o simple política (“rotar anualmente”). Cuánto duele la rotación depende de lo que elegiste antes — una razón más por la que el cifrado envolvente es el default correcto:
- Rotar la KEK es barato. Con la rotación automática de AWS KMS, la clave maestra rota cada año y el ciphertext existente sigue funcionando: el KMS recuerda las versiones antiguas. Re-envolver DEKs bajo una KEK nueva es una operación de metadatos, no una migración de datos.
- Rotar las DEK es lo caro. Re-cifrar datos exige descifrar-con-la-vieja → cifrar-con-la-nueva para cada registro. Por eso el
keyVersionse guarda junto a cada ciphertext: el trabajo de rotación puede encontrar y migrar los registros de la versión antigua de forma incremental.
Un runbook de rotación seguro: escribe los datos nuevos con la clave nueva de inmediato, re-cifra los registros antiguos por lotes (vigila la carga de la base de datos — esto calienta), mantén ambas claves activas hasta completar la migración, verifica que una muestra descifra bien y solo entonces retira la versión vieja. Si tu KMS borra una versión de clave antes de que migren todos los datos, esos datos se pierden para siempre. El runbook de rotación de claves tiene el checklist operativo completo.
// crypto/KeyRotation.ts — Rotar claves de cifrado
class KeyRotationService {
constructor(
private readonly kms: KmsClient,
private readonly db: Database,
) {}
async rotateKeys(): Promise<void> {
// 1. Encontrar todos los registros con la versión antigua
const records = await this.db.query(
`SELECT id, encrypted_data FROM sensitive_data WHERE key_version = $1`,
[1], // versión antigua
);
for (const record of records.rows) {
// 2. Descifrar con la clave antigua
const plaintext = await this.decrypt(record.encrypted_data, 1);
// 3. Re-cifrar con la clave nueva
const reencrypted = await this.encrypt(plaintext, 2); // versión nueva
// 4. Actualizar el registro
await this.db.query(
`UPDATE sensitive_data SET encrypted_data = $1, key_version = $2 WHERE id = $3`,
[reencrypted, 2, record.id],
);
}
console.log(`Rotated ${records.rows.length} records to key version 2`);
}
}
En tablas grandes, procesa este bucle por lotes (miles de filas por transacción, no de una en una) y ejecútalo en ventanas de poco tráfico: la re-cifrado a escala es sobre todo un problema de carga de base de datos, no de criptografía.
Compliance y custodia de claves
El cifrado en reposo aparece en casi todos los marcos de compliance, pero los detalles difieren lo suficiente como para importar:
- PCI-DSS exige cifrado de los datos del titular de tarjeta y controles de gestión de claves: conocimiento repartido y doble control para las claves, rotación documentada. Un KMS gestionado con logs de auditoría cubre de entrada la mayoría de los requisitos de custodia.
- FIPS 140-2/140-3 regula el módulo criptográfico en sí, no tu uso. Los HSM de AWS KMS y GCP Cloud KMS están validados a FIPS 140-2 Nivel 3 — relevante si vendes a gobierno o industrias reguladas donde “¿está validado el módulo cripto?” es una pregunta de procurement.
- HIPAA lista el cifrado como salvaguarda “addressable” — técnicamente opcional, pero una brecha sin cifrar es reportable mientras que perder datos cifrados generalmente no lo es. En la práctica es obligatorio.
- RGPD no nombra algoritmos pero exige “medidas técnicas apropiadas” (art. 32). La palanca interesante es el crypto-shredding para el derecho al olvido: si los datos se cifran con claves por sujeto, borrar la clave es una forma defendible de borrado incluso dentro de backups que no puedes editar quirúrgicamente.
Aplique el marco que aplique, el requisito recurrente es la traza de auditoría: quién usó qué clave, cuándo y sobre qué. Los KMS gestionados registran cada llamada Encrypt/Decrypt (CloudTrail, Cloud Audit Logs, Azure Monitor) — activa esos logs, alerta sobre anomalías y satisfarás la mayoría de las peticiones de evidencia de uso de claves.
Buenas prácticas
- Usa AES-256-GCM para cifrar datos — el cifrado autenticado evita la manipulación.
- Nunca guardes las claves junto a los datos cifrados — usa un KMS.
- Usa cifrado envolvente por rendimiento — las llamadas al KMS son lentas y con cuota, las DEK son locales y rápidas.
- Usa un IV único en cada operación de cifrado — nunca reuses un IV con la misma clave.
- Rota las claves con regularidad — anualmente, o de inmediato tras un incidente sospechado.
- Cifra en varias capas — nivel de campo para columnas sensibles, nivel de volumen para el disco.
- Pon a cero las claves en memoria tras usarlas — mitiga la exposición por dumps de memoria y crash dumps.
- Usa claves separadas por tenant en sistemas multi-tenant — acota el radio de daño y habilita el crypto-shredding.
- Registra el uso de claves en el KMS — consigues la traza de auditoría gratis con CloudTrail o Cloud Audit Logs.
- Prueba el descifrado antes de borrar claves antiguas — una versión eliminada hace su ciphertext irrecuperable, para siempre.
- Prefiere KMS con respaldo HSM — los módulos hardware mantienen la clave maestra fuera de la memoria por completo.
- No cifres datos sobre los que necesitas buscar u ordenar — el ciphertext no se puede indexar; usa un blind index solo cuando la búsqueda por igualdad sea imprescindible.
El cifrado en reposo es un control dentro de una superficie mayor — para el checklist adyacente que cubre auth, transporte y el resto de la postura de seguridad de APIs, mira la checklist de seguridad de APIs — de autenticación a cifrado.
Errores comunes
- Reusar IVs con la misma clave: catastrófico en GCM — rompe la confidencialidad y filtra la subclave de autenticación, permitiendo falsificar ciphertexts. Genera un IV aleatorio para cada cifrado.
- Guardar claves en variables de entorno: las env vars se filtran por crash reports,
/proc, endpoints de debug y logs de CI. Usa un KMS o Vault. - No usar cifrado autenticado: AES-CBC sin HMAC permite ataques de bit-flipping y padding oracle. Usa GCM, o CBC+HMAC si estás atado a una librería sin AEAD.
- Cifrarlo todo: cifrar datos no sensibles suma coste de KMS, complejidad operativa y hace más difícil auditar los datos realmente sensibles. Cifra lo que necesita protección.
- No tener plan de rotación: las claves comprometidas en un incidente quedan válidas para siempre sin versionado ni ruta de migración. Diseña para la rotación desde el día uno.
- Usar una sola clave para todos los tenants: una clave comprometida expone los datos de todos. Las claves por tenant acotan el daño.
- Borrar versiones de clave antiguas demasiado pronto: si algún ciphertext aún referencia la versión retirada, es irrecuperable. Inventaría el uso de
key_versionantes de deshabilitar o borrar claves.
Lecturas adicionales
- NIST SP 800-38D — especificación de GCM — la referencia normativa del modo Galois/Counter, incluidos los requisitos del IV.
- OWASP Cryptographic Storage Cheat Sheet — selección de algoritmos, reglas de almacenamiento de claves y fallos habituales.
- AWS KMS Developer Guide — políticas de claves, cifrado envolvente (
GenerateDataKey), grants y rotación. - Documentación de Google Cloud KMS — key rings, rotación automática y helpers de cifrado envolvente.
- Claves de Azure Key Vault — tipos de clave, política de rotación y managed HSM.
- Código complementario: el repo companion de cifrado en reposo tiene versiones ejecutables de los módulos AES-256-GCM, KMS, cifrado envolvente y rotación de claves de esta página.
Preguntas frecuentes
¿Qué es el cifrado en reposo?
Cifrar los datos donde están almacenados: disco, archivos de base de datos, backups, snapshots. El ciphertext es ilegible sin la clave, y la clave vive aparte, en un KMS. Si el almacenamiento se roba o se filtra, el atacante obtiene ruido en lugar de datos.
¿Qué es el cifrado envolvente?
Un patrón de dos claves: una clave de cifrado de datos (DEK) generada aleatoriamente cifra tus datos en local, y una clave cifradora de claves (KEK) gestionada por el KMS cifra la DEK. Guardas la DEK cifrada con el ciphertext, así que cada descifrado cuesta una llamada al KMS en lugar de mandar todos los datos por el servicio.
¿Qué es AES-256-GCM?
AES con clave de 256 bits en modo Galois/Counter. Además de confidencialidad, GCM produce una etiqueta de autenticación que detecta cualquier manipulación del ciphertext — por eso es el modo recomendado y por eso reusar el IV con la misma clave es un fallo tan grave.
¿Cómo funciona la rotación de claves?
Las escrituras nuevas usan la versión nueva; los datos existentes se descifran con la versión antigua y se re-cifran con la nueva, normalmente por lotes. La clave vieja sigue viva hasta que migra el último registro — por eso el keyVersion se guarda junto a cada ciphertext. Los KMS gestionados rotan la clave maestra automáticamente sin tocar tus claves de datos.
¿Debo cifrar a nivel de base de datos o de aplicación?
Responden a amenazas distintas. TDE protege contra discos y snapshots robados pero muestra texto plano a cualquiera con credenciales de base de datos, incluido un DBA malicioso. El cifrado a nivel de aplicación protege la columna incluso dentro de la base de datos. Usa nivel de campo para las pocas columnas que más dolerían (SSN, datos de salud, credenciales) y TDE como cobertura barata de base.
¿Puedo hacer queries sobre campos cifrados?
No con WHERE, ORDER BY ni LIKE sobre el ciphertext — la búsqueda y la ordenación son imposibles por diseño. Para igualdades puedes añadir una columna blind index (HMAC(clave, valor)), aceptando que filtra la frecuencia de valores. Cualquier cosa más rica — rangos, texto completo — exige técnicas especializadas como searchable encryption o un índice aparte, y normalmente no compensa la complejidad.
Recursos Relacionados
OAuth2 y OIDC: Authorization Code, PKCE, Token Validation
Dominá OAuth2 y OpenID Connect para producción: authorization code flow con PKCE, token validation, refresh tokens, scopes, JWT verification y security best practices.
GuideContent Security Policy: cabeceras CSP, nonces, hashes
Desplegá Content Security Policy sin romper tu sitio: directivas CSP, nonces, hashes, strict-dynamic, reportes e integración por framework.
RecipeEncripción en Reposo para Bases de Datos y Almacenamiento
Cómo encriptar datos sensibles antes de almacenarlos en bases de datos, object storage y backups usando AES-256-GCM, encripción de sobre y servicios de gestión de keys.
DocRunbook de Rotación de Claves
Usa este runbook de rotación de claves para rotarlas sin downtime. Cubre cronogramas, migración dual, verificación y rollback.
RecipeEncripta y Desencripta Datos con AES-GCM en Python
Encripta datos sensibles usando AES-GCM con la librería cryptography. Cubre derivación de claves, generación de nonces, encriptación autenticada y encriptación de archivos.