Enterprise Client-Side Key Management: HSM, BYOK, and Key Lifecycle Governance
A developer's guide to enterprise patterns for client-side key management: integrating HSMs, implementing BYOK policies, and governing key lifecycles at scale without surrendering encryption control.
Enterprise adoption of client-side encryption (CSE) shifts the trust boundary decisively toward the customer. But with that shift comes a new operational burden: managing the cryptographic keys that protect your data—at enterprise scale, across teams, with compliance requirements and auditability demands that never sleep.
This post covers the three pillars of enterprise client-side key management: Hardware Security Module (HSM) integration, Bring Your Own Key (BYOK) policies, and the governance patterns that keep key lifecycles from becoming a liability.
Why Enterprise Key Management Is Different
In a consumer or startup context, a master key might live in a secrets manager and rotate once a year. In an enterprise setting, the calculus changes:
- Regulatory scope: FIPS 140-2/3, Common Criteria, PCI DSS, and SOC 2 Type II all have specific requirements for where and how keys are stored.
- Key proliferation: A single enterprise deployment might maintain thousands of per-tenant encryption keys, each with its own rotation schedule.
- Auditability: Every key access, rotation, and deletion must be logged with tamper-evident trails that satisfy both internal InfoSec and external auditors.
- Separation of duties: The person who provisions keys should not be the same person who can use or export them.
Client-side encryption doesn't eliminate these concerns. It relocates them from the cloud provider's responsibility to yours—which is the point, but also the challenge.
Hardware Security Modules: The Root of Trust
An HSM is a dedicated hardware device (or cloud HSM service) that stores cryptographic keys in tamper-resistant hardware. The key never leaves the HSM in plaintext—operations like signing, encryption, and key derivation happen inside the device.
Cloud HSM Options
| Provider | Service | FIPS 140-2 Level |
|---|---|---|
| AWS | CloudHSM | Level 3 |
| Azure | Dedicated HSM | Level 3 |
| GCP | Cloud HSM (via Key Management) | Level 3 |
| On-premises | Thales Luna, Entrust nShield | Level 3/4 |
Integrating HSMs with Client-Side Encryption
The typical pattern is envelope encryption:
- Generate a Data Encryption Key (DEK) client-side using
crypto.getRandomValues()orSubtleCrypto. - The DEK encrypts the actual payload.
- The DEK is wrapped (encrypted) by a Key Encryption Key (KEK) that lives in the HSM.
- Only the wrapped DEK is stored alongside the ciphertext.
// Simplified envelope encryption with a cloud HSM KEK
import { KMSClient, GenerateDataKeyCommand } from "@aws-sdk/client-kms";
const kms = new KMSClient({ region: "eu-west-1" });
async function encryptWithEnvelope(
plaintextBuffer: ArrayBuffer,
kekArn: string
): Promise<{ ciphertext: ArrayBuffer; wrappedDek: Uint8Array }> {
// Ask the HSM-backed KMS to generate a DEK and return the wrapped version
const { Plaintext: dek, CiphertextBlob: wrappedDek } = await kms.send(
new GenerateDataKeyCommand({
KeyId: kekArn,
KeySpec: "AES_256",
})
);
if (!dek || !wrappedDek) throw new Error("DEK generation failed");
// Import the plaintext DEK into WebCrypto for the actual encryption
const cryptoKey = await crypto.subtle.importKey(
"raw",
dek,
{ name: "AES-GCM" },
false, // non-extractable
["encrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
cryptoKey,
plaintextBuffer
);
// Zero out the plaintext DEK from memory as soon as possible
dek.fill(0);
return {
ciphertext: new Uint8Array([...iv, ...new Uint8Array(ciphertext)]).buffer,
wrappedDek,
};
}
The wrapped DEK can be stored in your database. Decryption requires a Decrypt call to the HSM—which the HSM will only fulfill if the caller has the right IAM policy. The plaintext key material never transits your application server.
BYOK: Giving Enterprises the Keys They Already Expect
BYOK (Bring Your Own Key) lets enterprise customers supply their own root keys rather than trusting your platform's default key hierarchy. It's increasingly a procurement requirement, not a differentiator.
What BYOK Actually Means
True BYOK means the customer's key is the root of the encryption hierarchy for their data. If they revoke access to that key, you cannot decrypt their data—period. This is the assurance they're paying for.
There are weaker interpretations (sometimes called "customer-managed keys" or CMK), where the customer nominates which key in your KMS to use. This provides some auditability benefits but doesn't give the customer the ability to independently verify zero-knowledge properties.
Implementation Patterns
Pattern 1: External KMS wrapping
The customer provides an ARN (AWS), a Key Vault URI (Azure), or a Cloud KMS resource name (GCP). Your platform calls their KMS to wrap DEKs at write time and unwrap at read time. The customer can audit every API call in their cloud provider's logs.
interface ByokConfig {
provider: "aws" | "azure" | "gcp";
keyId: string; // ARN, Key Vault URI, or resource name
tenantId: string; // Your tenant identifier
lastRotatedAt: string; // ISO 8601
}
async function wrapDekWithByok(
dek: Uint8Array,
config: ByokConfig
): Promise<Uint8Array> {
switch (config.provider) {
case "aws":
return wrapWithAwsKms(dek, config.keyId);
case "azure":
return wrapWithAzureKeyVault(dek, config.keyId);
case "gcp":
return wrapWithGcpKms(dek, config.keyId);
}
}
Pattern 2: Customer-provided PKCS#11 or JWK
For highly regulated customers who operate their own HSMs, they may supply a public key (JWK format) for wrapping DEKs. Unwrapping requires them to call their own HSM. Your platform never sees the unwrapping key.
async function wrapDekWithJwk(
dek: Uint8Array,
publicKeyJwk: JsonWebKey
): Promise<ArrayBuffer> {
const publicKey = await crypto.subtle.importKey(
"jwk",
publicKeyJwk,
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["wrapKey"]
);
const rawDekKey = await crypto.subtle.importKey(
"raw",
dek,
{ name: "AES-GCM" },
true, // must be extractable to wrap
["encrypt", "decrypt"]
);
return crypto.subtle.wrapKey("raw", rawDekKey, publicKey, {
name: "RSA-OAEP",
});
}
Key Revocation
A BYOK customer who rotates or deletes their KEK should expect that your platform can no longer decrypt their data. This is a feature. Build it explicitly:
- Surface key status in your admin UI with a health indicator.
- Emit events when key operations fail so the customer's monitoring can catch accidental revocations.
- Document the recovery procedure (re-encrypt using a new KEK) so customers have a path back.
Key Lifecycle Governance
Keys have a lifecycle: generation, active use, rotation, retirement, and deletion. Enterprise governance means tracking all of it, enforcing policies, and producing evidence for auditors.
Rotation Policy Enforcement
Rotation schedules should be enforced programmatically, not by calendar reminders:
interface KeyMetadata {
keyId: string;
tenantId: string;
createdAt: Date;
rotationPeriodDays: number;
lastRotatedAt: Date;
status: "active" | "rotating" | "retired" | "revoked";
}
function isDueForRotation(metadata: KeyMetadata): boolean {
const ageMs = Date.now() - metadata.lastRotatedAt.getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
return ageDays >= metadata.rotationPeriodDays;
}
Rotation for envelope encryption means re-wrapping DEKs with a new KEK—not re-encrypting the underlying data. This makes rotation feasible even with billions of records.
Separation of Duties
Map RBAC roles to key operations:
| Role | Generate | Use (wrap/unwrap) | Rotate | Delete | Audit |
|---|---|---|---|---|---|
| Key Admin | Yes | No | Yes | Yes | Yes |
| Application | No | Yes | No | No | No |
| Auditor | No | No | No | No | Yes |
| Security Officer | No | No | No | Yes | Yes |
Never conflate "can use a key" with "can manage a key." IAM policies on HSMs and cloud KMS should enforce this at the API level, not just in application logic.
Audit Logging
Every key operation should produce an immutable log entry:
interface KeyAuditEvent {
eventId: string; // UUID v4
timestamp: string; // ISO 8601 with milliseconds
tenantId: string;
keyId: string;
operation: "wrap" | "unwrap" | "rotate" | "create" | "delete" | "revoke";
actorId: string; // Service account or user
sourceIp: string;
success: boolean;
errorCode?: string;
}
Ship these to an append-only log store (e.g., CloudTrail, Azure Monitor, or an immutable S3 bucket with Object Lock). This is what auditors look for during a SOC 2 or ISO 27001 assessment.
Putting It Together: A Reference Architecture
A production enterprise CSE setup typically looks like this:
- Tenant onboarding: Customer registers their KEK (external KMS or JWK). Your platform stores only the key reference, never the key.
- Data write: Application generates a DEK, encrypts payload client-side, calls HSM/BYOK to wrap the DEK, stores
{ciphertext, wrappedDek, keyRef, iv}. - Data read: Application fetches
{wrappedDek, keyRef}, calls HSM/BYOK to unwrap, decrypts locally, returns plaintext. - Key rotation: Scheduler identifies due keys, re-wraps DEKs with new KEK, updates metadata, retires old KEK after a grace period.
- Audit pipeline: Every step emits a structured log event to the immutable audit store.
Conclusion
Enterprise client-side key management is not a checkbox—it's an operational commitment. HSM integration guarantees that key material never exists in software; BYOK gives customers the sovereignty they need to trust your platform; lifecycle governance keeps the whole system from drifting into an unauditable state.
The good news: these patterns compose well with standard cloud infrastructure and the WebCrypto API. The investment is upfront engineering and process design, but the result is a system that satisfies even the most demanding enterprise procurement team—and that you can explain clearly to an auditor at 2am.