Quantum-Safe Cloud -- Parte 2

QuantumVault: Secretos que No Se Pueden Robar

#security #post-quantum #quantumvault #secrets #azure #dotnet

El Problema

Pregúntale a un desarrollador dónde está la contraseña de su base de datos. La mayoría de las veces la respuesta honesta es una de estas:

  • appsettings.json subido al repositorio
  • Un archivo .env que “todo el equipo tiene”
  • Una variable de entorno configurada a mano en cada servidor
  • Azure Key Vault — pero la clave de acceso al Key Vault está en appsettings.json

Esto no es un problema raro. Es la forma por defecto en que la mayoría de aplicaciones manejan secretos. Y crea un tipo de riesgo que es difícil de ver hasta que algo sale mal: los secretos se extienden a sitios que no controlas, gente que ya dejó el equipo todavía los tiene, nunca se rotan, y cuando hay una brecha no sabes qué secretos quedaron expuestos.

La solución clásica es un vault: un lugar centralizado para almacenar secretos, con control de acceso, logs de auditoría y rotación. Azure Key Vault es un buen ejemplo. Es mucho mejor que un archivo .env.

Pero Azure Key Vault tiene un problema que cubrimos en el artículo 1: las claves que protegen tus secretos son RSA o ECC por defecto. El vault es seguro contra los ataques de hoy. No es seguro contra harvest-now-decrypt-later. Un atacante que grabe tu tráfico de Key Vault hoy y espere a tener un ordenador cuántico puede eventualmente desenvolver cada secreto que hayas almacenado.

QuantumVault resuelve ambos problemas a la vez: gestión centralizada de secretos con key wrapping ML-KEM. Los secretos se cifran con claves post-cuánticas generadas desde hardware QRNG. La arquitectura es sólida hoy, y sigue siendo sólida cuando lleguen los ordenadores cuánticos.

La Solución

QuantumVault es un servicio de gestión de secretos. Almacena un secreto, recibe un ID. Recupéralo por ID con tu API key. Ese es el modelo básico — igual que Key Vault.

Lo que cambia por dentro:

  1. Generación de claves QRNG: cada DEK (Data Encryption Key) de cada tenant se genera desde hardware real de números aleatorios cuánticos. No /dev/urandom. No un PRNG por software.
  2. Envelope encryption con ML-KEM: tus secretos se cifran con AES-256-GCM. La clave AES se envuelve con ML-KEM-768. La clave envuelta es lo que se almacena. Aunque un atacante consiga la base de datos, no puede desenvolver sin la clave privada ML-KEM.
  3. Aislamiento por tenant: cada tenant tiene su propio DEK. Una brecha en las claves de un tenant no expone los secretos de otro.
  4. Soporte BYOK: si tu política de compliance requiere que seas dueño de tus propias claves, puedes traer tu propio par de claves ML-KEM e importarlo. quantumAPI nunca ve tu clave privada.
  5. Logs de auditoría: cada lectura, escritura y borrado se registra con timestamp e identidad de la API key.

La API es lo bastante simple para usarla desde un script de shell o desde un servicio .NET.

Ejecución

Vamos a hacer tres cosas:

  1. Almacenar secretos en QuantumVault y recuperarlos en una app .NET
  2. Reemplazar las referencias a Azure Key Vault por QuantumVault
  3. Exportar una clave generada por QuantumVault a Azure Key Vault (BYOK) para cargas de trabajo que deben quedarse en Azure

Paso 1: Almacenar un secreto

Si todavía no has instalado el CLI qapi, el Artículo 01 lo cubre. Una vez configurado:

qapi secrets create users-api-db-connection \
  "Server=db.internal;Database=users;User Id=api;Password=..."

Respuesta:

✓ Secret 'users-api-db-connection' created successfully

El CLI asigna un ID al secreto. Ejecuta qapi secrets list para verlo — guardarás ese ID en la configuración de tu app, no el valor.

Paso 2: Recuperar un secreto

qapi secrets get <secret-id> --show

Respuesta:

{
  "id": "7c3a1f9d-4e2b-8a6c-0f5d-3b9e1c7a4f2d",
  "name": "users-api-db-connection",
  "value": "Server=db.internal;Database=users;User Id=api;Password=..."
}

El flag --show es necesario para incluir el valor en la salida. Sin él solo recibes metadata — sin exposición accidental en los logs.

Paso 3: Integración con .NET

Instala el SDK:

dotnet add package QuantumAPI.Client

Añade un QuantumVaultSecretProvider a tu app ASP.NET Core:

// Program.cs
builder.Services.AddQuantumApiClient(options =>
{
    options.ApiKey = builder.Configuration["QuantumApi:ApiKey"]!;
});

builder.Services.AddScoped<ISecretProvider, QuantumVaultSecretProvider>();
// QuantumVaultSecretProvider.cs
public class QuantumVaultSecretProvider : ISecretProvider
{
    private readonly QuantumApiClient _client;

    public QuantumVaultSecretProvider(QuantumApiClient client)
    {
        _client = client;
    }

    public async Task<string> GetSecretAsync(string secretId)
    {
        var secret = await _client.Vault.GetSecretAsync(secretId);
        return secret.Value;
    }
}

Úsalo en tu DbContextFactory o connection factory:

// UsersDbContextFactory.cs
public class UsersDbContextFactory
{
    private readonly ISecretProvider _secrets;

    public UsersDbContextFactory(ISecretProvider secrets)
    {
        _secrets = secrets;
    }

    public async Task<UsersDbContext> CreateAsync()
    {
        var connectionString = await _secrets.GetSecretAsync(
            Environment.GetEnvironmentVariable("DB_SECRET_ID")!);

        var options = new DbContextOptionsBuilder<UsersDbContext>()
            .UseNpgsql(connectionString)
            .Options;

        return new UsersDbContext(options);
    }
}

Tu aplicación ahora tiene cero connection strings en código o archivos de configuración. Lo único en tu configuración es el DB_SECRET_ID (solo un ID, no un secreto) y la API key de quantumAPI.

Paso 4: Configuración con secret IDs (no valores secretos)

Actualiza appsettings.json para referenciar IDs, no valores:

{
  "QuantumApi": {
    "ApiKey": "" // ← loaded from environment variable only, never in appsettings
  },
  "Secrets": {
    "DbConnectionId": "7c3a1f9d-4e2b-8a6c-0f5d-3b9e1c7a4f2d",
    "JwtSecretId": "2a8f4c1e-9b3d-7f5a-1e4c-6d0b8e3f2a9c"
  }
}

El valor de QuantumApi:ApiKey viene de una variable de entorno:

export QUANTUMAPI__APIKEY=qid_your_api_key_here

En Azure DevOps o Kubernetes, esto se configura con un variable group o un K8s Secret — y es el único secreto que todavía necesitas gestionar manualmente. Todo lo demás está en QuantumVault.

Paso 5: Bring Your Own Key a Azure Key Vault

Si tu política de compliance requiere que las claves vivan dentro de Azure Key Vault, puedes exportar una clave generada por QuantumVault a Azure. El material de la clave fue generado con QRNG, así que mantiene su origen quantum-safe incluso dentro de Azure KV.

Primero, genera una clave ML-KEM-768 en QuantumVault usando QRNG:

qapi keys generate azure-export-key \
  --algorithm ML-KEM-768 \
  --purpose encryption

La clave se crea dentro de QuantumVault. Apunta el key ID de la salida. Para exportar la clave pública (para verificar operaciones desde Azure):

qapi keys export <key-id> --format pem --output quantum-public.pem

Para la importación BYOK completa — donde el material de la clave privada va a Azure Key Vault — usa el flujo de exportación BYOK del dashboard de QuantumAPI, que genera la clave en un HSM FIPS 140-2 y la envuelve usando el formato de importación de Azure Key Vault. La exportación del CLI de arriba es solo para la clave pública; el material de la clave privada nunca sale del HSM sin cifrar.

Lo que ajustamos

El tratamiento ATLAS para el alcance de este artículo:

[A] ARCHITECT
  Move all secrets out of appsettings.json and env vars.
  Single source of truth: QuantumVault.
  Only QuantumAPI key stays in environment — everything else is an ID.
  Out of scope: key rotation automation (Article 5), multi-region (future).

[T] TRACE
  App starts → reads QuantumApi:ApiKey from environment
  → QuantumApiClient initialised
  → First DB operation → GetSecretAsync(DbConnectionId)
  → QuantumVault returns decrypted connection string
  → Npgsql uses it → DB connection established

[L] LINK
  App → QuantumVault: HTTPS, X-Api-Key header, TLS 1.3
  QuantumVault → PostgreSQL (internal): ML-KEM envelope encryption at rest

[A] ASSEMBLE
  1. Add QuantumAPI.Client NuGet
  2. Register client + ISecretProvider
  3. Replace direct connection string reads with GetSecretAsync calls
  4. Store secrets in QuantumVault dashboard
  5. Remove connection strings from all config files

[S] STRESS-TEST
  What if QuantumVault is unreachable at startup?
    → Add retry policy (Polly, 3 retries with exponential backoff)
    → Consider caching decrypted values in memory for the process lifetime
  What if the API key is wrong?
    → 401 from QuantumVault → app fails fast with a clear error

Tres cosas que ajustamos en la práctica:

1. Resolución al arranque vs. por request. Obtener secretos por cada request añade latencia. Para connection strings, resuelve una vez al arranque y cachea. Para valores realmente sensibles (tokens, claves de pago), resuelve por request para respetar la rotación.

2. Secret ID en appsettings vs. en código. Pusimos el secret ID en appsettings.json (no hardcoded). Esto te permite apuntar el mismo código a diferentes secretos en diferentes entornos (dev/staging/prod) cambiando configuración, no código.

3. La API key de quantumAPI en sí. El único secreto que no puede vivir en QuantumVault es la API key de QuantumAPI (la necesitas para acceder a QuantumVault). Este es tu secreto de bootstrap. Trátalo como una credencial root — variable group de Azure DevOps con respaldo de Key Vault, o un Kubernetes Secret con acceso restringido por RBAC.

Template

=== QUANTUMVAULT MIGRATION CHECKLIST ===

INVENTORY
[ ] List every secret in your application:
    - Database connection strings
    - JWT signing secrets
    - API keys for third-party services
    - OAuth2 client secrets
    - Encryption keys

CLASSIFY
[ ] For each secret: what algorithm protects it today?
    RSA-wrapped → migrate to QuantumVault (ML-KEM wrapping)
    Plaintext in config → migrate to QuantumVault immediately
    AES-wrapped locally → assess (AES-256 is quantum-resistant, but where's the AES key?)

MIGRATE
[ ] Create secret in QuantumVault via API or dashboard
[ ] Replace config value with secret ID (not the secret itself)
[ ] Update application code to call GetSecretAsync
[ ] Remove old secret from config files
[ ] Rotate the old secret (the one that was in plaintext is now compromised)

BOOTSTRAP
[ ] QuantumAPI key: store in Azure DevOps variable group or K8s Secret
[ ] Restrict access: only the service identity that needs it
[ ] Set up rotation reminder (or automate — Article 5)

VERIFY
[ ] pnpm build succeeds with no secrets in config
[ ] git grep -r "password\|secret\|connectionstring" --include="*.json" → 0 results
[ ] App starts and connects successfully using QuantumVault-resolved secrets
[ ] Audit log in QuantumVault dashboard shows your app's reads

Reto

Antes del artículo 3, prueba esto: coge la API de usuarios de la serie ATLAS+GOTCHA y mueve su connection string de base de datos a QuantumVault. Necesitarás:

  1. Crear el secreto en tu dashboard de QuantumVault
  2. Añadir QuantumAPI.Client al proyecto
  3. Cambiar builder.Configuration.GetConnectionString("DefaultConnection") por await secretProvider.GetSecretAsync(secretId)

Lleva unos 30 minutos. Cuando termines, haz grep -r "password" --include="*.json" en el proyecto. Deberías tener cero resultados.

En el artículo 3, vamos más allá: no solo secretos, sino cifrado de datos reales. Registros de usuarios en la base de datos — email, teléfono, dirección — cifrados a nivel de campo con QuantumAPI EaaS antes de que toquen la base de datos.

Si esta serie te resulta útil, considera invitarme a un café.

Comments

Loading comments...