Quantum-Safe Cloud -- Part 2

QuantumVault: Secrets That Can't Be Stolen

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

The Problem

Ask a developer where their database password lives. Most of the time the honest answer is one of these:

  • appsettings.json checked into the repository
  • A .env file that “everyone on the team has”
  • An environment variable set manually on each server
  • Azure Key Vault — but the Key Vault access key is in appsettings.json

This is not a niche problem. It’s the default way most applications handle secrets. And it creates a specific kind of risk that’s hard to see until something goes wrong: secrets spread to places you don’t control, people who’ve left the team still have them, they never get rotated, and when there’s a breach you don’t know which secrets were exposed.

The classical solution is a vault: a centralised place to store secrets, with access control, audit logs, and rotation. Azure Key Vault is a good example. It’s much better than an .env file.

But Azure Key Vault has a problem we covered in article 1: the keys wrapping your secrets are RSA or ECC by default. The vault is secure against today’s attacks. It’s not secure against harvest-now-decrypt-later. An attacker who records your Key Vault traffic today and waits for a quantum computer can eventually unwrap every secret you’ve ever stored.

QuantumVault solves both problems at once: centralised secret management with ML-KEM key wrapping. Secrets are encrypted with post-quantum keys generated from QRNG hardware. The architecture is sound today, and stays sound when quantum computers arrive.

The Solution

QuantumVault is a secrets management service. Store a secret, get back an ID. Retrieve by ID with your API key. That’s the basic model — same as Key Vault.

What’s different under the hood:

  1. QRNG key generation: every tenant’s Data Encryption Key (DEK) is generated from real quantum random number hardware. Not /dev/urandom. Not a software PRNG.
  2. ML-KEM envelope encryption: your secrets are encrypted with AES-256-GCM. The AES key is wrapped with ML-KEM-768. The wrapped key is what gets stored. Even if an attacker gets the database, they can’t unwrap without the ML-KEM private key.
  3. Per-tenant isolation: every tenant has their own DEK. A breach in one tenant’s keys doesn’t expose another tenant’s secrets.
  4. BYOK support: if your compliance policy requires owning your own keys, you can bring your own ML-KEM key pair and import it. quantumAPI never sees your private key.
  5. Audit logs: every read, write, and delete is logged with timestamp and API key identity.

The API is simple enough to use from a shell script or from a .NET service.

Execute

We’ll do three things:

  1. Store secrets in QuantumVault and retrieve them in a .NET app
  2. Replace Azure Key Vault references with QuantumVault
  3. Export a QuantumVault-generated key to Azure Key Vault (BYOK) for workloads that must stay in Azure

Step 1: Store a secret

If you haven’t installed the qapi CLI yet, Article 01 covers it. Once it’s set up:

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

Response:

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

The CLI assigns an ID to the secret. Run qapi secrets list to see it — you’ll store that ID in your app config, not the value.

Step 2: Retrieve a secret

qapi secrets get <secret-id> --show

Response:

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

The --show flag is required to include the value in the output. Without it you get only metadata — no accidental exposure in logs.

Step 3: .NET integration

Install the SDK:

dotnet add package QuantumAPI.Client

Add a QuantumVaultSecretProvider to your ASP.NET Core app:

// 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;
    }
}

Use it in your DbContextFactory or 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);
    }
}

Your application now has zero connection strings in code or config files. The only thing in your configuration is the DB_SECRET_ID (just an ID, not a secret) and the quantumAPI key.

Step 4: Configuration with secret IDs (not secret values)

Update appsettings.json to reference IDs, not values:

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

The QuantumApi:ApiKey value comes from an environment variable:

export QUANTUMAPI__APIKEY=qid_your_api_key_here

In Azure DevOps or Kubernetes, this is set via a variable group or a K8s Secret — and it’s the only secret you still need to manage manually. Everything else is in QuantumVault.

Step 5: Bring Your Own Key to Azure Key Vault

If your compliance policy requires keys to live inside Azure Key Vault, you can export a QuantumVault-generated key to Azure. The key material was generated with QRNG, so it keeps its quantum-safe origin even inside Azure KV.

First, generate an ML-KEM-768 key in QuantumVault using QRNG:

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

The key is created inside QuantumVault. Note the key ID from the output. To export the public key (for verifying operations from Azure):

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

For full BYOK import — where the private key material goes into Azure Key Vault — use the QuantumAPI dashboard’s BYOK export flow, which generates the key in an FIPS 140-2 HSM and wraps it using Azure Key Vault’s import format. The CLI export above is for the public key only; private key material never leaves the HSM unencrypted.

What we adjusted

The ATLAS treatment for this article’s scope:

[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

Three things we adjusted in practice:

1. Startup vs. per-request resolution. Fetching secrets per-request adds latency. For connection strings, resolve once at startup and cache. For truly sensitive values (tokens, payment keys), resolve per-request to respect rotation.

2. Secret ID in appsettings vs. in code. We put the secret ID in appsettings.json (not hardcoded). This lets you point the same codebase at different secrets in different environments (dev/staging/prod) by changing config, not code.

3. The quantumAPI key itself. The only secret that can’t live in QuantumVault is the QuantumAPI key (you need it to access QuantumVault). This is your bootstrap secret. Treat it like a root credential — Azure DevOps variable group with Key Vault backing, or a Kubernetes Secret with RBAC-restricted access.

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

Challenge

Before article 3, try this: take the users API from the ATLAS+GOTCHA series and move its database connection string into QuantumVault. You’ll need to:

  1. Create the secret in your QuantumVault dashboard
  2. Add QuantumAPI.Client to the project
  3. Change builder.Configuration.GetConnectionString("DefaultConnection") to await secretProvider.GetSecretAsync(secretId)

It takes about 30 minutes. When you’re done, do grep -r "password" --include="*.json" on the project. You should get zero results.

In article 3, we go further: not just secrets, but actual data encryption. User records in the database — email, phone number, address — encrypted at the field level with QuantumAPI EaaS before they ever touch the database.

If this series helps you, consider buying me a coffee.

Comments

Loading comments...