Per-Tenant Key Derivation
Mindful Auth uses per-tenant key derivation with HKDF (HMAC-based Key Derivation Function). Each tenant gets a unique encryption key derived from:
tenant_key = HKDF(master_key, tenant_salt, "mindful-auth-tenant-key")This provides improved security isolation between tenants while maintaining a single master secret.
What is a Salt?
Section titled “What is a Salt?”A salt is random data added to cryptographic operations to ensure uniqueness. Think of it like adding a unique ingredient to each recipe:
- Without salt: Same input → same output (predictable)
- With salt: Same input + different salt → different output (unique per tenant)
Example:
Master Key alone: "secret123" → Same derived key for all tenantsMaster Key + Salt A: "secret123" + "xyz..." → Unique key for Tenant AMaster Key + Salt B: "secret123" + "abc..." → Unique key for Tenant BKey properties:
- Salts are random (generated once per tenant during onboarding)
- Salts are not secret (stored openly alongside encrypted data)
- Salts ensure unique keys even when master key is shared
- An attacker needs both master key AND salt to derive a tenant’s key
Security Benefits
Section titled “Security Benefits”- Each tenant has a unique encryption key
- Compromise of one tenant’s encrypted data doesn’t directly reveal other tenants’ keys
- Attacker needs both master key AND tenant salt to derive any specific tenant’s key
Limitations
Section titled “Limitations”- Master key compromise still affects all tenants
- Attacker with master key + tenant salts can derive tenant keys
- Tenant salts are not secret (stored alongside encrypted data in Cloudflare KV and Tape admin workspace)
Technical Architecture
Section titled “Technical Architecture”Key Derivation
Section titled “Key Derivation”// Unique key per tenant using HKDFkey = HKDF(master_key, tenant_salt, "mindful-auth-tenant-key", SHA-256)Ciphertext Format
Section titled “Ciphertext Format”All encrypted data follows the format:
[iv(12 bytes)] + [ciphertext]- iv: 12-byte random initialization vector (AES-GCM)
- ciphertext: AES-GCM encrypted data with authentication tag
Storage
Section titled “Storage”- tenantSalt: 32 random bytes, base64 encoded
- Stored in:
- Cloudflare KV:
tenantConfig:{hostname}→tenantSaltfield - Tape: Hostnames app →
tenant_saltfield
- Cloudflare KV:
Implementation Details
Section titled “Implementation Details”// Generate new per-tenant saltgenerateTenantSalt() → string (base64)
// Encrypt with per-tenant keyencryptTenantSecret(plaintext, env, tenantSaltB64) → ciphertext
// Decrypt with per-tenant keydecryptTenantSecret(ciphertextB64, env, tenantSaltB64) → plaintextEncrypted Fields Per Tenant
Section titled “Encrypted Fields Per Tenant”| Field | Storage Location | Description |
|---|---|---|
encrypted2faKey | Cloudflare KV + Tape | Tenant’s 2FA encryption key |
encryptedInternalApiKey | Cloudflare KV + Tape | API key for lock/unlock endpoint |
encryptedTurnstileSecretKey | Cloudflare KV + Tape | Cloudflare Turnstile secret |
Backend API Key | Cloudflare KV + Tape | encryptedTapeApiKey or encryptedD1ApiToken |
Onboarding Flow (New Tenants)
Section titled “Onboarding Flow (New Tenants)”All tenants automatically use per-tenant key derivation:
-
Generate tenant salt:
const tenantSalt = generateTenantSalt(); -
Encrypt all secrets with salt:
const encrypted2faKey = await encryptTenantSecret(twoFAKey, env, tenantSalt);const encryptedInternalApiKey = await encryptTenantSecret(internalApiKey, env, tenantSalt);const encryptedTurnstileSecretKey = await encryptTenantSecret(turnstileSecretKey, env, tenantSalt); -
Store salt alongside encrypted data:
// Cloudflare KV storageawait env.KV_HOSTNAMES.put(`tenantConfig:${hostname}`, JSON.stringify({...config,tenantSalt,encrypted2faKey,encryptedInternalApiKey,encryptedTurnstileSecretKey}));
Encryption Key Reuse (Shared App IDs)
Section titled “Encryption Key Reuse (Shared App IDs)”When multiple hostnames share the same backend app ID:
- The first hostname’s
tenantSaltis reused - All encrypted keys are copied from the existing hostname
- This ensures users can authenticate across all portals with consistent encryption
// In onboardHostname.jsif (existingHostnameWithSameAppId) { // Reuse salt and encrypted keys from existing hostname tenantSalt = existingConfig.tenantSalt; encrypted2faKey = existingConfig.encrypted2faKey; encryptedInternalApiKey = existingConfig.encryptedInternalApiKey;}Decryption Flow
Section titled “Decryption Flow”Storage Strategy: CloudflareKV for Speed, Tape for Audit Trail
Section titled “Storage Strategy: CloudflareKV for Speed, Tape for Audit Trail”-
Cloudflare KV storage: Fast in-memory cache used during authentication flows
- Tenant config lookups happen via
getTenantConfig() - All encrypted secrets are stored and retrieved from Cloudflare KV
- Used for every login, password reset, 2FA operation
- Purpose: Sub-millisecond response times
- Tenant config lookups happen via
-
Tape storage (Hostnames app): Persistent source of truth for admin operations
- Stores same encrypted data as Cloudflare KV (synchronized during onboarding)
- Used by admin endpoints (getTenantHostname, updateHostname, etc.)
- Provides audit trail and administrative visibility
- Purpose: Long-term persistence, admin console access
Key point: Runtime authentication flows ALWAYS read from Cloudflare KV, not Tape. Cloudflare KV contains the encrypted data needed for decryption during auth operations.
Security Considerations
Section titled “Security Considerations”Tenant Salt is NOT Secret
Section titled “Tenant Salt is NOT Secret”The tenant salt enables key derivation but doesn’t provide security on its own. An attacker with:
- Only tenant salt: Cannot derive key (needs master key)
- Only master key: Cannot derive key (needs tenant salt)
- Both: Can derive tenant key
Per-Tenant Isolation
Section titled “Per-Tenant Isolation”Each tenant’s encrypted data is isolated at the key level:
- Tenant A’s salt + master key → Tenant A’s key
- Tenant B’s salt + master key → Tenant B’s key
An attacker with Tenant A’s encrypted data and salt still cannot decrypt Tenant B’s data (different salt = different key).
Cloudflare KV Security
Section titled “Cloudflare KV Security”What is Cloudflare KV?
Section titled “What is Cloudflare KV?”Cloudflare KV is a globally distributed, low-latency key-value store that replicates data across Cloudflare’s edge network. It’s designed for fast reads/writes with eventual consistency.
Security Properties
Section titled “Security Properties”Encryption at rest:
- All data stored in Cloudflare KV is encrypted by Cloudflare using industry-standard encryption
- Encryption keys are managed by Cloudflare’s infrastructure
Access control:
- KV namespaces are isolated per Cloudflare Worker project
- Access requires valid authentication to the Cloudflare account
- No direct public access to KV data (only through Worker code)
Why it’s safe for encrypted secrets:
- Your encrypted data is protected by two layers of encryption:
- Your application layer: Encrypted with per-tenant derived keys (HKDF + AES-GCM)
- Cloudflare layer: Encrypted by Cloudflare at rest
- Even if Cloudflare infrastructure is compromised, the data is still encrypted with your keys
- The master key is never stored in KV (kept in Worker environment secrets)
- Tenant salts are non-secret; their exposure doesn’t compromise encryption
Threat model:
- ✅ Protected against: Cloudflare infrastructure compromise, data breaches at storage level, network interception
- ✅ Protected against: Unauthorized access to KV (requires account authentication)
- ⚠️ Not protected against: Master key compromise (attacker can decrypt all tenant data)
Separation of Secrets
Section titled “Separation of Secrets”- Master key: Stored in Cloudflare Worker environment secrets (separate from KV)
- Encrypted data: Stored in Cloudflare KV (with public visibility acceptable since encrypted)
- Tenant salts: Stored with encrypted data in Cloudflare KV (non-secret by design)
This separation ensures that even if someone gains read access to Cloudflare KV, they cannot decrypt tenant secrets without the master key.
Master Key Protection
Section titled “Master Key Protection”The master key is stored in Cloudflare Worker environment variables/secrets, which are separate from KV and provide stronger protection:
Storage location:
- Master key never leaves Cloudflare’s infrastructure
- Stored as an encrypted environment variable in Cloudflare’s secure vault
- Only loaded into Worker memory at runtime (not persisted to disk or KV)
Access difficulty (why it’s hard to compromise):
-
❌ Not accessible via KV API - Environment secrets are completely separate from KV storage
-
❌ Not accessible via Worker code - You can read your own environment secrets, but an attacker would need to:
- Compromise the Cloudflare account (requires stealing 2FA credentials + password)
- Gain administrative access to the Worker project
- Deploy malicious code that exfiltrates the secret
- Or compromise Cloudflare’s infrastructure directly (extremely difficult)
-
❌ Not accessible via network interception - The secret never travels over the network; it’s injected directly into the Worker runtime
-
❌ Not accessible via Worker logs
Attack chain required: To compromise the master key, an attacker would need:
- Valid Cloudflare account credentials (email + password + 2FA bypass)
- Permission to modify the Worker code
- Ability to deploy new code (requires another 2FA step)
- AND either:
- Read the environment variable (if Cloudflare API allows it, which it doesn’t by default for security)
- Or modify the code to exfiltrate the secret
Industry comparison: This is equivalent to how major cloud providers (AWS, Azure, GCP) protect secrets:
- AWS Secrets Manager: Secrets stored in encrypted vault, not in code or logs
- Azure Key Vault: Encryption keys never leave the secure enclave
- Cloudflare: Environment secrets stored separately with restricted access
Risk assessment:
- 🔴 High risk if: Attacker compromises Cloudflare account + 2FA
- 🟡 Medium risk if: Attacker gains code deployment access (still need account + 2FA to do this)
- 🟢 Low risk from: KV data breach, network interception, log leaks