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
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.
The page works like Launch the portal from your product, with one key instead of a deployment:
- The customer clicks Connect key in your product.
- Your server creates a setup link and sends them to the deployment portal, the branded setup page Alien hosts.
- Their admin connects a key in AWS KMS, Google Cloud KMS, or Azure Key Vault.
- 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 productionThe 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:
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:
- Chooses AWS, Google Cloud, or Azure.
- Runs a CloudFormation stack (AWS) or Terraform (AWS, Google Cloud, Azure) in their own account.
- 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:
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.
| Field | Limit |
|---|---|
plaintext | 1 MiB after base64 decoding |
key.keyId | 1 to 128 UTF-8 bytes |
associatedData | 64 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
keyIdvalues 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
| Code | HTTP | Meaning |
|---|---|---|
KEY_UNAVAILABLE | 503 | The customer's key is disabled or access was removed. Retryable once they restore it. |
KEY_NOT_CONFIGURED | 404 | This customer hasn't connected a key. |
ENCRYPTION_UNAUTHORIZED | 401 | The encryption API key is wrong or revoked. |
DECRYPTION_FAILED | 400 | Wrong ciphertext, keyId, external ID, or associated data. |
INVALID_REQUEST | 400 | Malformed body or headers, for example a keyId over 128 bytes or both selector headers. |
ENCRYPTION_INPUT_TOO_LARGE | 413 | Input over 1 MiB. |
ENCRYPTION_FAILED | 500 | Internal 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:
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.
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "not-connected", "deploymentId": null } } }Setting up
Example UI /Setting up
Customer-managed keys
Settings · Security
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
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.
{ "externalId": "customer-1", "capabilities": { "keys": { "enabled": true, "state": "setting-up", "deploymentId": "dep_0c29fq4a2yjb7kx3smwdgxlc" } } }Connected
Example UI /Connected
Customer-managed keys
Settings · Security
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
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.deploymentIdidentifies the customer's key connection. An AWS Virtual Key takes it asdeployment_id.- Tell the customer what disabling or deleting the key does: their data becomes unreadable, and a deleted key can't be recovered.
{ "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
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…
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.
{ "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 documentsOpen 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:
- Encrypt a value and decrypt the ciphertext.
- Disable the key in the cloud console and wait longer than 5 minutes.
- Confirm decrypt returns
KEY_UNAVAILABLEand your UI shows it. - 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 24hNext
- AWS Virtual Keys: encrypt S3, Aurora, or EBS in your account with the customer's key
- Data and keys: plaintext, cryptographic context, and revocation
- Local AI models: the next setting, on the same pattern