Arquitectura y Principios de un HSM

Un Hardware Security Module es una pieza de hardware dedicada, una caja física o una placa PCIe, cuyo único trabajo es generar, almacenar y usar claves criptográficas sin jamás exponerlas. El material de claves vive dentro de un boundary resistente a manipulación. Si alguien físicamente se mete en el HSM, las claves se destruyen automáticamente (esto se llama zeroización).

La propiedad crítica es que las claves nunca salen del HSM en plaintext. Puede pedirle al HSM que encripte datos con una clave almacenada, desencripte datos, firme un mensaje, verifique una firma, pero no es posible pedirle que te dé la clave en sí. Esto es fundamentalmente diferente de un administrador de secretos como HashiCorp Vault, donde los secretos se almacenan encriptados pero pueden recuperarse en plaintext.

PKCS#11: La Interfaz Universal de HSM

PKCS#11 (también llamado Cryptoki) es la API estándar para interactuar con tokens criptográficos, incluyendo HSMs. Es una API en C de los 90s, y se nota, pero es lo que todo usa.

// pkcs11_example.c - Operaciones básicas de HSM via PKCS#11
#include <pkcs11.h>
#include <stdio.h>

int main() {
 CK_RV rv;
 CK_SESSION_HANDLE session;
 CK_SLOT_ID slot = 0;

 rv = C_Initialize(NULL);
 rv = C_OpenSession(slot, CKF_SERIAL_SESSION | CKF_RW_SESSION,
 NULL, NULL, &session);

 CK_UTF8CHAR pin[] = "crypto-user-pin";
 rv = C_Login(session, CKU_USER, pin, sizeof(pin) - 1);

 // Generar una key AES-256 dentro del HSM
 CK_MECHANISM mechanism = { CKM_AES_KEY_GEN, NULL, 0 };
 CK_ULONG key_length = 32;
 CK_BBOOL true_val = CK_TRUE;
 CK_BBOOL false_val = CK_FALSE;
 CK_UTF8CHAR label[] = "mi-data-encryption-key";

 CK_ATTRIBUTE key_template[] = {
 { CKA_TOKEN, &true_val, sizeof(true_val) },
 { CKA_ENCRYPT, &true_val, sizeof(true_val) },
 { CKA_DECRYPT, &true_val, sizeof(true_val) },
 { CKA_EXTRACTABLE, &false_val, sizeof(false_val) }, // NO se puede exportar
 { CKA_SENSITIVE, &true_val, sizeof(true_val) },
 { CKA_LABEL, label, sizeof(label) - 1 },
 { CKA_VALUE_LEN, &key_length, sizeof(key_length) },
 };

 CK_OBJECT_HANDLE key;
 rv = C_GenerateKey(session, &mechanism, key_template,
 sizeof(key_template) / sizeof(CK_ATTRIBUTE), &key);

 // Encriptar datos usando la key (la key nunca sale del HSM)
 CK_BYTE plaintext[] = "password de la base: hunter2";
 CK_BYTE ciphertext[256];
 CK_ULONG ciphertext_len = sizeof(ciphertext);

 CK_GCM_PARAMS gcm_params = {
 .pIv = (CK_BYTE_PTR)"random-iv-12b",
 .ulIvLen = 12,
 .ulIvBits = 96,
 .pAAD = NULL, .ulAADLen = 0, .ulTagBits = 128,
 };
 CK_MECHANISM enc_mechanism = { CKM_AES_GCM, &gcm_params, sizeof(gcm_params) };

 rv = C_EncryptInit(session, &enc_mechanism, key);
 rv = C_Encrypt(session, plaintext, sizeof(plaintext) - 1,
 ciphertext, &ciphertext_len);

 printf("Encriptado %lu bytes\n", ciphertext_len);

 C_Logout(session);
 C_CloseSession(session);
 C_Finalize(NULL);
 return 0;
}

Observar CKA_EXTRACTABLE = false y CKA_SENSITIVE = true. Estos atributos aseguran que la key jamás puede exportarse del HSM.

Cloud KMS: HSMs como Servicio

AWS KMS y Envelope Encryption

# aws_kms_envelope.py - Envelope encryption con AWS KMS
import boto3
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

kms = boto3.client('kms', region_name='us-east-1')
CMK_KEY_ID = 'arn:aws:kms:us-east-1:123456789:key/mrk-abc123'

def encrypt_data(plaintext: bytes, context: dict) -> dict:
 """
 Envelope encryption:
 1. KMS genera una data key (dentro del HSM)
 2. KMS devuelve data key plaintext + data key encriptada
 3. Usamos la data key plaintext para encriptar localmente
 4. Descartamos la data key plaintext
 5. Guardamos la data key encriptada junto al ciphertext
 """
 response = kms.generate_data_key(
 KeyId=CMK_KEY_ID,
 KeySpec='AES_256',
 EncryptionContext=context # Atada a este contexto
 )

 plaintext_key = response['Plaintext']
 encrypted_key = response['CiphertextBlob']

 nonce = os.urandom(12)
 aesgcm = AESGCM(plaintext_key)
 ciphertext = aesgcm.encrypt(nonce, plaintext, None)

 # CRÍTICO - borrar la key plaintext de memoria
 plaintext_key = b'\x00' * len(plaintext_key)
 del plaintext_key

 return {
 'ciphertext': ciphertext,
 'nonce': nonce,
 'encrypted_data_key': encrypted_key,
 'encryption_context': context,
 }

def decrypt_data(envelope: dict) -> bytes:
 response = kms.decrypt(
 CiphertextBlob=envelope['encrypted_data_key'],
 EncryptionContext=envelope['encryption_context']
 )
 plaintext_key = response['Plaintext']

 aesgcm = AESGCM(plaintext_key)
 plaintext = aesgcm.decrypt(
 envelope['nonce'], envelope['ciphertext'], None
 )

 plaintext_key = b'\x00' * len(plaintext_key)
 del plaintext_key
 return plaintext

context = {'service': 'payment-api', 'environment': 'production'}
envelope = encrypt_data(b'database_password=super_secret_123', context)
recovered = decrypt_data(envelope)

El parámetro EncryptionContext se suele pasar por alto pero es crítico para la seguridad. Ata la data key encriptada a un contexto específico. Si alguien intenta desencriptar la data key con un contexto diferente, KMS rechaza.

Rotación de Keys que No Rompe Nada

# key_rotation.py
import boto3
from datetime import datetime, timedelta

kms = boto3.client('kms')

def setup_automatic_rotation(key_id: str):
 kms.enable_key_rotation(KeyId=key_id)
 status = kms.get_key_rotation_status(KeyId=key_id)
 print(f"Rotación habilitada: {status['KeyRotationEnabled']}")

def manual_rotation_with_alias(old_key_id: str, alias_name: str):
 """Rotación manual usando aliases, útil para rotar más seguido que anualmente."""
 new_key = kms.create_key(
 Description=f'Key rotada - {datetime.now().isoformat()}',
 KeyUsage='ENCRYPT_DECRYPT',
 Origin='AWS_KMS',
 Tags=[
 {'TagKey': 'RotatedFrom', 'TagValue': old_key_id},
 {'TagKey': 'RotationDate', 'TagValue': datetime.now().isoformat()},
 ]
 )
 new_key_id = new_key['KeyMetadata']['KeyId']

 # Actualizar alias para apuntar a la nueva key
 kms.update_alias(AliasName=alias_name, TargetKeyId=new_key_id)

 # LA KEY VIEJA PERMANECE ACTIVA para desencriptación
 # Este es el insight clave: no re-encriptás datos existentes
 print(f"Alias {alias_name} ahora apunta a {new_key_id}")
 print(f"Key vieja {old_key_id} sigue activa para desencriptación")

La Arquitectura Completa de Gestión de Secretos

┌─────────────────────────────────────────────────────┐
│ Capa de Aplicación │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Payment │ │ Auth │ │ Notification │ │
│ │ Service │ │ Service │ │ Service │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼──────────────────▼────────┐ │
│ │ HashiCorp Vault │ │
│ │ - Credenciales dinámicas de base de datos │ │
│ │ - Emisión de certificados PKI │ │
│ │ - Encriptación transit (delega a KMS) │ │
│ │ - Secretos con leases y auto-rotación │ │
│ └────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────────┐ │
│ │ AWS KMS │ │
│ │ - Auto-unseal de Vault │ │
│ │ - Envelope encryption para transit engine │ │
│ │ - S3 SSE-KMS para audit logs │ │
│ │ - Master key para TDE de RDS │ │
│ └────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────────┐ │
│ │ AWS CloudHSM (FIPS 140-2 L3) │ │
│ │ - Custom KMS key store │ │
│ │ - Gestión de keys PCI DSS │ │
│ │ - Keys de firma de código │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
# vault_config.hcl
storage "raft" {
 path = "/opt/vault/data"
 node_id = "vault-1"

 retry_join {
 leader_api_addr = "https://vault-2.internal:8200"
 }
 retry_join {
 leader_api_addr = "https://vault-3.internal:8200"
 }
}

listener "tcp" {
 address = "0.0.0.0:8200"
 tls_cert_file = "/opt/vault/tls/vault.crt"
 tls_key_file = "/opt/vault/tls/vault.key"
}

seal "awskms" {
 region = "us-east-1"
 kms_key_id = "mrk-abc123"
}

La arquitectura de tres niveles es deliberada:

  1. Vault maneja secretos dinámicos (credenciales de base de datos que rotan cada hora), PKI, y entrega de secretos a las aplicaciones.
  2. KMS maneja el auto-unseal de Vault y provee la encriptación de respaldo para el transit engine de Vault.
  3. CloudHSM respalda el custom key store de KMS para claves de encriptación reguladas por PCI.

Qué Hice Mal

Dos cosas me afectaron en este proyecto:

Primero, subestimé el costo de CloudHSM. A aproximadamente $1.50/hora por HSM (y se necesita al menos dos para HA), son más de $2,000/mes solo por los HSMs. Para la mayoría de las cargas de trabajo, KMS estándar a $1/key/mes más $0.03/10,000 llamadas API es más que suficiente.

Segundo, el ecosistema de tooling de PKCS#11 es doloroso. Las librerías son viejas, la documentación asume que se ejecuta Solaris, y depurar errores de sesión requiere leer hex dumps. Si es posible quedarte en la capa de API del cloud KMS, hacelo.

El archivo .env ya no existe. Las contraseñas de base de datos rotan cada hora. El audit trail muestra exactamente quién accedió a qué key, cuándo. ¿Y la siguiente auditoría de seguridad? Pasó sin hallazgos. Eso se sintió bien.