Docs

Add BYOK to your product

In this tutorial you add BYOK to an existing application: instead of sensitive fields being encrypted with a key you hold, each customer's data is encrypted under a key that customer owns and can revoke.

We start with individual fields: your backend sends a value to Alien Encryption Gateway before storing it, the gateway encrypts it under a root protected by that customer's AWS KMS, Google Cloud KMS, or Azure Key Vault key, and your database stores the returned ciphertext. At the end we cover the other path — putting a whole AWS resource under the customer's key without touching application code at all.

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:

src/integrations.ts
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-backend

The secret is shown once. Store it as ALIEN_ENCRYPTION_KEY. Never put it in browser code.

Let the customer connect their key

alien onboard "Acme" \
  --external-id org_123 \
  --setup-items keys

The 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 Integrate with your product for the TypeScript SDK and REST API.

Add a small encryption client

src/encryption.ts
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_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:

src/encryption.ts
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:

src/integrations.ts
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:

src/encryption.ts
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")
}
src/integrations.ts
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 1h

Disabling 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.

Encrypt a whole AWS resource instead

The Encrypt/Decrypt API is the right tool for individual fields. For a resource in your own AWS account that AWS already knows how to encrypt — an Aurora database, an S3 bucket, an EBS volume — an Alien Virtual Key gets the same customer control with no application code at all.

Your infrastructure does not move.

An Aurora database in your own AWS account asks AWS KMS for a data key. Because that KMS key is an Alien Virtual Key, KMS sends the already-encrypted data key to Alien over the External Key Store protocol, and Alien wraps it under the key your customer connected — in AWS KMS, Google Cloud KMS, or Azure Key Vault. The resource stays in your account; only the key belongs to the customer, who can disable it at any time.

Run the module below with credentials for your own AWS account. deployment_id names the customer whose key should protect the resource:

module "encryption_key" {
  source  = "pkg.alien.dev/alien/virtual-key/aws"
  version = "0.1.0"

  deployment_id = "dep_example"
  alias         = "acme-database"
}

Then point the resource at the resulting ARN exactly as you would any customer-managed KMS key:

resource "aws_rds_cluster" "acme" {
  cluster_identifier = "acme"
  engine             = "aurora-postgresql"

  storage_encrypted = true
  kms_key_id        = module.encryption_key.kms_key_arn 
}

That is the entire change. The database stays in your account, Aurora encrypts as usual, AWS KMS routes the data key through Alien, and Alien wraps it under the key Acme connected — which can be in their AWS KMS, Google Cloud KMS, or Azure Key Vault. Acme can revoke it at any time without either of you moving infrastructure.

Compare the two paths before choosing:

Encrypt/Decrypt APIAlien Virtual Key
Good forFields inside records you storeWhole AWS resources you run
Application changesEncrypt before write, decrypt after readNone
Who sends data to AlienYour backend, as plaintextAWS KMS, as an encrypted data key
Works withAWS, Google Cloud, Azure resourcesAWS resources only

Both accept a customer key in any of the three clouds.

What you added

Your application still owns its records, its storage, and its AWS account. What changed is who controls the key: each customer's data is now encrypted under a root only their own KMS or Key Vault key can unlock, and they can withdraw that at any time using the key system they already operate.

You have both tools for it now — the Encrypt/Decrypt API for individual fields, and an Alien Virtual Key for a whole AWS resource. Neither one moves your infrastructure.

Continue with Data and keys, or integrate the connection flow into your product.

On this page