<!-- Canonical: https://lucioduran.com/es/blog/hardware-security-modules-hsm-cloud-kms
Language: es
Content-SHA256: cb14b4129d7c196090f7406be8afca566ace1e6c146e1ed24cfa31692a653e87
-->

Security

# HSMs y Cloud KMS: PKCS#11, Envelope Encryption y Arquitectura Real de Gestión de Secretos

2026-01-257 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/hardware-security-modules-hsm-cloud-kms>)

## 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](<https://www.vaultproject.io/>), 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.

```c
// 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

```python
# 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

```python
# 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 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```

```hcl
# 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.

## Preguntas frecuentes

### ¿Necesito un HSM dedicado o el cloud KMS es suficiente?

Cloud KMS (AWS KMS, Azure Key Vault, GCP Cloud KMS) es suficiente para el 95% de las cargas de trabajo. Necesitás un HSM dedicado (CloudHSM, Azure Dedicated HSM) solo cuando requisitos regulatorios exigen hardware single-tenant, cuando se necesita operaciones criptográficas custom no soportadas por KMS, o cuando se necesita acceso directo PKCS#11/JCE.

### ¿Cuál es la diferencia entre una CMK y una data key?

Una Customer Master Key (CMK) vive dentro del KMS/HSM y nunca sale. Una data key es generada por KMS, usada para encriptar tus datos reales, y después la data key misma es encriptada por la CMK. Este approach de dos niveles es envelope encryption.

### ¿Cada cuánto debería rotar las claves de encriptación?

Rotar CMKs anualmente como baseline. AWS KMS soporta rotación automática anual. Las data keys pueden rotarse más seguido si es necesario. Lo importante es que la rotación sea automática y no requiera re-encriptar datos existentes cuando se utiliza envelope encryption.

## Tips

1. **Tip:** Nunca exportes master keys de un HSM. Si tu arquitectura requiere que la master key salga del boundary del HSM, tu arquitectura está mal. Utilizar ` envelope encryption ` en su lugar.
2. **Tip:** AWS KMS ` GenerateDataKey ` devuelve tanto la versión plaintext como ciphertext de la data key. Utilizar el plaintext para encriptar, descartalo inmediatamente de memoria, guardá solo el ciphertext junto con los datos encriptados.
3. **Tip:** Key rotation no significa re-encriptar todo. Con envelope encryption, solo rotás la KEK, las data keys existentes siguen válidas porque están encriptadas con la versión vieja de la KEK, que KMS todavía sabe desencriptar.
4. **Tip:** Siempre colocar un ` DeletionWindowInDays ` de al menos 14 días para eliminación de keys en KMS. Borrar una key KMS accidentalmente es permanente y catastrófico, ese es 14 días salvaron a más de un equipo que conozco.

- hsm
- kms
- pkcs11
- envelope-encryption
- key-rotation
- aws-cloudhsm
- secrets-management

[Volver al Blog](<https://lucioduran.com/es/blog>)
