Add BYOK to your product
In this tutorial, we are going to add BYOK to an existing application.
Your backend will send a value to Encryption Gateway before storing it. Encryption Gateway encrypts it under a root protected by that customer's AWS KMS, Google Cloud KMS, or Azure Key Vault key. Your database stores the returned ciphertext.
Your backend sends a value and customer ID to Encryption Gateway. The customer's connected KMS or Key Vault key protects the encryption root used for that value.
Your backend does not receive the customer's cloud credentials or raw key material. It does handle the plaintext it sends for encryption and receives after decryption.
Choose one field to protect
Start with a field your application already stores, such as an OAuth refresh token:
await db.integration.create({
data: {
customerId: customer.id,
provider: "github",
refreshToken,
},
})We will replace refreshToken with ciphertext before the record reaches the database.
Enable Encryption Gateway
alien projects capabilities enable encryption
alien api-keys create \
--for encryption-gateway \
--description production-backendThe secret is shown once. Store it as ALIEN_ENCRYPTION_API_KEY. Never put it in browser code.
Let the customer connect their key
alien onboard "Acme" \
--external-id org_123 \
--setup-items keysThe customer opens this link and chooses a key from AWS KMS, Google Cloud KMS, or Azure Key Vault. Alien receives the access needed to protect that customer's encryption root. Your application receives none of the customer's cloud credentials.
In a real product, create the link from your backend when the customer opens your BYOK settings. See Customer setup for the TypeScript SDK and REST API.
Add a small encryption client
const endpoint = "https://encryption.alien.dev/v1"
async function callEncryptionGateway<T>(
path: "encrypt" | "decrypt",
customerId: string,
body: object,
): Promise<T> {
const response = await fetch(`${endpoint}/${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALIEN_ENCRYPTION_API_KEY}`,
"X-Alien-External-ID": customerId,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
if (!response.ok) {
throw new Error(`Encryption Gateway returned ${response.status}`)
}
return response.json() as Promise<T>
}The API key selects your Alien project. X-Alien-External-ID selects the customer's connected key.
Always derive the customer ID from the authenticated server-side account. Do not accept an arbitrary value from browser input.
Encrypt before writing to the database
The API accepts and returns base64:
export async function encryptSecret(customerId: string, value: string) {
const result = await callEncryptionGateway<{ ciphertext: string }>(
"encrypt",
customerId,
{
key: { keyId: "integration-tokens" },
plaintext: Buffer.from(value, "utf8").toString("base64"),
},
)
return result.ciphertext
}Use a stable keyId that describes the data, not the customer. The customer is already selected by the request header. Separate IDs such as integration-tokens, documents, and credentials create separate cryptographic contexts.
Now store the ciphertext:
const encryptedRefreshToken = await encryptSecret(customer.id, refreshToken)
await db.integration.create({
data: {
customerId: customer.id,
provider: "github",
encryptedRefreshToken,
},
})The database no longer receives the plaintext refresh token.
Decrypt when the application needs it
Decrypt with the same customer ID and keyId:
export async function decryptSecret(customerId: string, ciphertext: string) {
const result = await callEncryptionGateway<{ plaintext: string }>(
"decrypt",
customerId,
{
key: { keyId: "integration-tokens" },
ciphertext,
},
)
return Buffer.from(result.plaintext, "base64").toString("utf8")
}const refreshToken = await decryptSecret(
customer.id,
integration.encryptedRefreshToken,
)
await refreshGithubToken(refreshToken)Decrypt fails if the request uses another Alien project, another customer connection, another keyId, or different associated data.
Bind ciphertext to a record
For especially sensitive fields, include associated data that must match at decrypt time. Encode it as base64 just like the plaintext:
const associatedData = Buffer.from(
`integration:${integration.id}:refresh-token`,
"utf8",
).toString("base64")
const encrypted = await callEncryptionGateway<{ ciphertext: string }>(
"encrypt",
customer.id,
{
key: { keyId: "integration-tokens" },
plaintext: Buffer.from(refreshToken).toString("base64"),
associatedData,
},
)Send the exact same associatedData when decrypting. This prevents ciphertext copied from one record or purpose from being decrypted as another.
Test customer control
Disable the test key in the customer's cloud, wait longer than the five-minute encryption-root cache, and try decrypting again. Encryption Gateway should fail when it has to reload the root through the disabled KMS or Key Vault key.
Restore access and verify that decrypt works again:
alien logs --source encryption-gateway \
--operation decrypt \
--since 1hDisabling the cloud key does not delete ciphertext, and access is not guaranteed to stop immediately because a loaded root may remain cached for up to five minutes. Decide how your product behaves while the customer's key is unavailable.
What you added
Your application still owns its records and storage. The sensitive values inside those records are now encrypted under a different customer-controlled root for each customer. Customers can govern access with the KMS or Key Vault system they already operate, while your backend uses one Encrypt/Decrypt API.
Continue with Data and keys, or add the customer setup flow to your product.