Docs

Add customer-managed keys to your product

This guide adds a Settings → Customer-managed keys page to a product you host, part of Customer-managed keys. The customer connects a key in their own cloud, and your backend encrypts their data with it.

Example UI /Not connected

Customer-managed keys

Settings · Security

Not connected

Use your own encryption key

Encrypt your data in Acme with a key in your AWS KMS. Revoke it at any time to cut off access.

Connect keySend to an admin

The page works like Launch the portal from your product, with one key instead of a deployment:

  1. The customer clicks Connect key in your product.
  2. Your server creates a setup link and sends them to the deployment portal, the branded setup page Alien hosts.
  3. Their admin connects a key in AWS KMS, Google Cloud KMS, or Azure Key Vault.
  4. Your server encrypts and decrypts through the Encrypt/Decrypt API at encryption.alien.dev, and shows the key's state.

Sequence diagram. Lanes: Your backend (your cloud), Alien (encryption.alien.dev), Customer cloud (KMS / Key Vault). Steps: 1 · plaintext + external ID; 2 · unwrap root (cached 5 min); 3 · root key; 4 · ciphertext.

If the customer revokes the key, Alien's next reload of their encryption root fails, within 5 minutes. Encrypt and decrypt then stop working for that customer. See Data and keys for what Alien sees.

Turn on customer-managed keys

This happens once, in the dashboard: open Customer-managed keys in the sidebar, click Enable customer-managed keys, then click Create API key and leave only Customer-managed keys ticked. From the CLI:

alien projects capabilities enable encryption
alien api-keys create --for encryption-gateway --description production

The secret is shown once. Save it as ALIEN_ENCRYPTION_API_KEY on the service that encrypts and decrypts. This key can only encrypt and decrypt: it can't create setup links or read deployments.

Creating setup links and reading the connection need your regular Alien API key (ALIEN_API_KEY), used by the client from Set up a client.

Keep the two keys separate. The encryption key can decrypt your customers' data, so give it only to the service that needs it. The dashboard lets you tick more features on one key, but each one widens what a leaked key exposes.

Add a Connect key button

This runs on your server, in the route your Connect key button calls. Create a setup link with the keys item and send the customer to it. The request is POST /v1/deployment-groups/setup-links:

lib/connect-key.ts
import { alien, PROJECT } from "./alien"

export async function connectKeyLink(customer: { id: string; slug: string }) {
  const link = await alien.setupLinks.create({
    project: PROJECT,
    externalId: customer.id,
    name: customer.slug,
    setupItems: [{ item: "keys", required: true }],
    entryPoint: { item: "keys", provider: "aws" }, // optional: open the AWS step first
  })
  return link.deploymentLink
}

Omit entryPoint for a generic button and let the customer choose a cloud. To allow only AWS, also set deploymentSetupConfig: { policy: { allowedPlatforms: ["aws"] } }. See link options.

Calling it again with the same external ID reuses the customer's record and returns a fresh link.

In the portal, the customer sees Bring your own key: "Choose the cloud key used to protect your data." Their admin then:

  1. Chooses AWS, Google Cloud, or Azure.
  2. Runs a CloudFormation stack (AWS) or Terraform (AWS, Google Cloud, Azure) in their own account.
  3. The stack creates or connects the key and grants Alien permission to use it.

Alien never receives the admin's credentials or the raw key material.

Encrypt and decrypt

This runs on your server, wherever you store or read a protected value. Call POST /v1/encrypt or POST /v1/decrypt on https://encryption.alien.dev with the encryption API key, and send the same external ID in X-Alien-External-ID. Values are base64:

lib/encryption.ts
const GATEWAY = "https://encryption.alien.dev/v1"

async function call(op: "encrypt" | "decrypt", customerId: string, body: object) {
  const res = await fetch(`${GATEWAY}/${op}`, {
    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 (!res.ok) throw new Error(`${op} failed: ${res.status} ${await res.text()}`)
  return res.json() as Promise<{ ciphertext?: string; plaintext?: string }>
}

const key = { keyId: "documents" }

export async function encrypt(customerId: string, value: string) {
  const plaintext = Buffer.from(value).toString("base64")
  const { ciphertext } = await call("encrypt", customerId, { key, plaintext })
  return ciphertext!
}

export async function decrypt(customerId: string, ciphertext: string) {
  const { plaintext } = await call("decrypt", customerId, { key, ciphertext })
  return Buffer.from(plaintext!, "base64").toString()
}

keyId names a cryptographic context inside the customer's connection, such as documents or credentials. Ciphertext is bound to the workspace, project, customer, and keyId, so decrypting with a different value fails.

An optional associatedData (base64) must also match on decrypt. Store the ciphertext with the external ID and keyId you need to decrypt it.

FieldLimit
plaintext1 MiB after base64 decoding
key.keyId1 to 128 UTF-8 bytes
associatedData64 KiB after base64 decoding

To encrypt for one deployment instead of a customer, send X-Alien-Deployment-ID in place of X-Alien-External-ID. Send exactly one of the two.

On your side:

  • Keep the encryption API key in server-side secret storage.
  • Resolve the external ID from the signed-in account on your backend, never from browser input.
  • Pick keyId values deliberately and keep them stable. Changing one makes existing ciphertext undecryptable under the new value.
  • Store ciphertext, not plaintext, and don't log plaintext, decrypted values, or API keys.

Errors

CodeHTTPMeaning
KEY_UNAVAILABLE503The customer's key is disabled or access was removed. Retryable once they restore it.
KEY_NOT_CONFIGURED404This customer hasn't connected a key.
ENCRYPTION_UNAUTHORIZED401The encryption API key is wrong or revoked.
DECRYPTION_FAILED400Wrong ciphertext, keyId, external ID, or associated data.
INVALID_REQUEST400Malformed body or headers, for example a keyId over 128 bytes or both selector headers.
ENCRYPTION_INPUT_TOO_LARGE413Input over 1 MiB.
ENCRYPTION_FAILED500Internal error in Alien.

Decide what your product does on KEY_UNAVAILABLE, for example "Your encryption key is unavailable. Restore access in your cloud account to continue." Don't fall back to unencrypted storage.

Show the connection

This runs on your server, when your settings page loads. Read the customer's state from the project's capability overview, GET /v1/projects/{idOrName}/project-capabilities/overview.

One response covers every customer in the project. It lists one deployment group per customer (Alien's record of that customer) with its external ID. The other settings pages reuse this helper:

lib/capability.ts
import { alien, PROJECT } from "./alien"

type Capability = "keys" | "models" | "buckets" | "remoteSandbox"

// Null until the customer's first setup link creates their group.
export async function capability(customerId: string, name: Capability) {
  const overview = await alien.projects.getCapabilityOverview({ idOrName: PROJECT })
  const group = overview.groups.find((g) => g.externalId === customerId)
  return group?.capabilities[name] ?? null
}

export async function keyStatus(customerId: string) {
  return (await capability(customerId, "keys"))?.state ?? "not-connected"
}

Not connected

The card at the top of this page. Show it when keys.state is not-connected, or capability() returns null because the customer has never opened a setup link.

  • Create the setup link when the customer clicks Connect key, not when the page loads.
  • The person who clicks may not be the cloud admin. Let them send the link to someone else.
  • Encrypt calls return KEY_NOT_CONFIGURED (404) in this state. Don't store the value unencrypted instead.
GET /v1/projects/{idOrName}/project-capabilities/overview
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "not-connected", "deploymentId": null } } }

Setting up

Example UI /Setting up

Customer-managed keys

Settings · Security

Setting up

Finish connecting your key in AWS

Your admin is running the setup in your AWS account. This page updates when the key is connected.

Account
123456789012 · us-east-1
Reopen setupSend to an admin

Show it when keys.state is setting-up: the admin opened the link, and the key isn't connected yet.

  • Reopen setup creates another link with the same external ID. Alien reuses the customer's record.
  • Opening the cloud console proves nothing. Keep this state until the overview says connected.
  • Poll the overview while the page is open, and stop when the customer leaves it.
GET /v1/projects/{idOrName}/project-capabilities/overview
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "setting-up", "deploymentId": "dep_0c29fq4a2yjb7kx3smwdgxlc" } } }

Connected

Example UI /Connected

Customer-managed keys

Settings · Security

Key connected

Your data is encrypted with your key

Acme can read your data only while this key in your AWS KMS is available.

Key
arn:aws:kms:us-east-1:123456789012:key/7f3c…
Account
123456789012 · us-east-1
Change key

Show it when keys.state is connected. Encrypt and decrypt work for this customer.

  • To change clouds or keys, create another link with the same external ID.
  • keys.deploymentId identifies the customer's key connection. An AWS Virtual Key takes it as deployment_id.
  • Tell the customer what disabling or deleting the key does: their data becomes unreadable, and a deleted key can't be recovered.
GET /v1/projects/{idOrName}/project-capabilities/overview
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "connected", "deploymentId": "dep_0c29fq4a2yjb7kx3smwdgxlc", "observation": { "platform": "aws", "location": "us-east-1", "health": "healthy" } } } }

Key unavailable

Example UI /Key unavailable

Customer-managed keys

Settings · Security

Needs attention

Your encryption key is unavailable

Acme can’t read or save your data until access to the key is restored.

Re-enable the key or restore Acme’s access in your AWS KMS. Your data is kept and recovers automatically.

Key
arn:aws:kms:us-east-1:123456789012:key/7f3c…
Reopen setupSend to an admin

Show it when keys.state is needs-attention, or when a request returns KEY_UNAVAILABLE (503).

  • Show this state as soon as a request returns KEY_UNAVAILABLE, even before the overview changes.
  • Requests may keep working for up to 5 minutes after a revoke, while the root is cached. Don't promise instant cut-off.
  • Stored ciphertext stays. When the customer restores access, the same data decrypts again.
  • Don't retry in a tight loop. Back off, and retry when the customer says access is restored.
GET /v1/projects/{idOrName}/project-capabilities/overview
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "needs-attention", "observation": { "platform": "aws", "health": "unhealthy" } } } }

A revoked state means the customer removed the key connection itself. Show Key disconnected, with Connect key to start again.

Test it end to end

Try it as your own test customer before you ship:

alien onboard "Test customer" --external-id org_test --setup-items keys
export CUSTOMER_ID=org_test
alien examples encryption-gateway --operation encrypt --key-id documents

Open the printed link and connect a key from a cloud account you control. alien examples prints a ready-to-run request. Then, for each cloud you offer:

  1. Encrypt a value and decrypt the ciphertext.
  2. Disable the key in the cloud console and wait longer than 5 minutes.
  3. Confirm decrypt returns KEY_UNAVAILABLE and your UI shows it.
  4. Restore the key and confirm decrypt works again.

Inspect requests and usage without exposing values:

alien logs --source encryption-gateway --operation decrypt --since 1h
alien usage encryption --range 24h

Next

On this page