Skip to content

Your own encryption key

With your own encryption key, the secrets we keep for your workspace are sealed under a key you control. You can see every time we use it, and if you revoke it or turn it off, nothing sealed under it can be read, by your apps or by us.

It’s an Enterprise feature. Owners set it up in the dashboard, under the workspace’s Settings, in Encryption. Admins can see which key is in use, but only owners can change it. Changing it asks for a sign-in from the last 15 minutes. A CLI session can’t read or change it.

These move under your key:

These stay under ours:

  • Your single sign-on client secret. It’s how owners sign in to fix a key that stopped working, so it can’t depend on that key.
  • The audit log archive, which has a key of its own for each workspace.
  • The passwords of your Postgres connections, and where your log drains send and their credentials.
  • A project you look up to import from another host, for the 30 minutes we keep it.
  • Your apps’ databases, files and code, which Cloudflare encrypts at rest. They aren’t sealed under your key.

When you add your key, we make a new 256-bit data key for your workspace and ask your key to wrap it. Everything in the list above is then sealed under the data key with AES-256-GCM, in one step: nothing is left half moved. We keep the wrapped data key, sealed again under our own key, and never the data key itself.

To read a secret, we ask your key to unwrap the data key, check it is the same key we started with, and keep it in memory for less than a minute. While your apps are busy we ask again a little before that runs out, so they don’t wait on your key. Your key is called about every 40 seconds from each of our servers while your apps are busy. Once they’re idle, it’s called at most once more for each webhook, Slack channel and audit log stream sending what happened last, and then not at all.

Every call says which workspace it’s for, and which data key: AWS gets them as the encryption context, and a key service gets them in the request. A data key opens nothing of any other workspace’s, and each key can be used by one workspace only.

A key service is an https endpoint you run, in front of whatever holds your keys: HashiCorp Vault, Google Cloud KMS, Azure Key Vault or a hardware module. It wraps and unwraps for us, and nothing else.

In Encryption, choose Key service, and enter:

  • URL: where we send each call, like https://keys.example.com/jiayang. It has to be https, on port 443 or 8443, at a public address.
  • Key name: your service’s name for the key, sent with each call, like projects/acme/keys/jiayang. Up to 256 letters, digits and ., _, :, / or -.
  • Signing secret: 32 to 512 characters you make up and give your service too. Your service checks that each call is signed with it.

Each call is a POST with a JSON body:

{
"operation": "wrap",
"key": "projects/acme/keys/jiayang",
"context": { "workspace": "<your workspace's id>", "data_key": "<the data key's id>" },
"plaintext": "<32 bytes, base64>"
}

An unwrap has "operation": "unwrap" and "ciphertext" in place of "plaintext". Answer a wrap with {"ciphertext": "<base64>"} and an unwrap with {"plaintext": "<base64>"}, with status 200. Bind the context to what you wrap, for example as associated data, so a wrapped key only unwraps with the same context. A wrapped key can be up to 4,096 bytes.

The call has a Jiayang-Signature header in the same form as our webhooks: t=<unix time>,v1=<hex>, where the hex is an HMAC-SHA256, under your signing secret, of the time, a ., and the raw body. Refuse a call whose signature doesn’t match, or whose time is more than five minutes from yours.

Any status but 200 is a refusal, a redirect included: we don’t follow it. A call that takes more than 5 seconds counts as unanswered, and so does an answer over 64 KiB.

Answer quickly. When we have no data key in memory, an app’s request waits at most about a second and a half for your key. Past that, a request to a host the app has a secret for is refused with 503 while we keep waiting, and goes through within a second of your key answering.

AWS KMS keys can’t be used yet: Encryption says so, and until it doesn’t, use a key service in front of your KMS key, or write to support. When they can, you’ll need a symmetric encryption key in AWS KMS, and an IAM role in the same account that we take on to use it. The dashboard shows the two values the role needs: our AWS principal, and your workspace’s id, which is its external id.

  1. Make the role, with this trust policy:

    {
    "Version": "2012-10-17",
    "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "<our principal, from the dashboard>" },
    "Action": "sts:AssumeRole",
    "Condition": { "StringEquals": { "sts:ExternalId": "<your workspace's id>" } }
    }]
    }

    The external id means the role works for your workspace alone. Another workspace naming your role is refused by AWS.

  2. Give the role this permission:

    {
    "Version": "2012-10-17",
    "Statement": [{
    "Effect": "Allow",
    "Action": ["kms:Encrypt", "kms:Decrypt"],
    "Resource": "<your key's ARN>",
    "Condition": { "StringEquals": { "kms:EncryptionContext:jiayang:workspace": "<your workspace's id>" } }
    }]
    }

    If your key policy doesn’t let IAM policies in the account grant use of the key, add the role to the key policy as well.

  3. In Encryption, choose AWS KMS, and enter the key’s ARN and the role’s ARN. Use the key’s ARN, like arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab, not an alias: an alias can be pointed at another key.

When you save, we wrap a new data key with your key and unwrap it again, so a key or a role that can do only one of the two is caught before anything depends on it. Every call shows in your AWS CloudTrail as well as in our log.

To move to another key, choose Rotate and enter the new one. It can be in AWS or a key service, whatever the old one was. We unwrap the data key with your current key, wrap it with the new one and unwrap it again to check. Nothing sealed under the data key changes, so your apps carry on without a gap. Once it’s done, the old key isn’t used again, and you can disable it.

To replace a key service’s signing secret, if it got out, set the new one at your service, choose Rotate, and enter the same URL and key name with the new secret. We ask your service to unwrap the data key with calls signed with the new secret, then wrap it and unwrap it again. The old secret isn’t used, so your service can stop taking it first.

Rotating key material inside AWS KMS, with automatic rotation, needs nothing from you here: AWS keeps the old material to unwrap what it wrapped.

Revoke stops everything sealed under your key within 10 seconds, everywhere: we stop opening anything at once, and your apps’ requests stop carrying secrets once the 10 seconds we keep them for are up. To do the same from your side, disable the key in AWS, take the role’s permission away, or have your key service refuse us. Then it stops within a minute, once the data key we hold in memory runs out.

While your key can’t be used:

  • A request from one of your apps to a host it has a secret for is refused with 503 and a message saying why. It is never sent without the secret. Requests to other hosts go out as usual.
  • Requests to an AI provider with your company key are refused, and cost nothing against the app’s budget.
  • A verified webhook is answered 503, so the sender tries it again later.
  • Your webhooks, Slack channels and audit log streams fail to send. As with any failure, we turn one off after a day of failing.
  • Nothing new can be sealed: saving a secret, an AI key, a webhook or a stream is refused with 503 workspace_key_unavailable.

Restore puts a revoked key back in use. We ask your key to unwrap the data key first, and restore it only if it gives back the same key.

Remove moves everything sealed under your key back under ours, in one step, and your key isn’t used again. Your key has to work for this, since it unwraps the data key one last time. If it’s revoked, restore it first.

If your key is gone for good, nothing sealed under it can be read again, by anyone. Discard deletes the key and everything sealed under it: your apps’ secrets, verified webhooks with their secrets, company AI keys, webhooks and Slack channels, and audit log streams. You type the workspace’s short name to confirm. Then you can set each of them up again, under our key or a new one of yours.

If we hold your workspace’s data for a legal reason, adding a key and discarding one wait until the hold ends.

A hold placed while your key is in use keeps its copies sealed under your key, as they were. We can read them only while your key works: revoke it, disable it or lose it during the hold, and the copies can’t be read, by us or anyone else.

Every call to your key is in your audit log as workspace_key.use, with the operation, why we made it, and how it went. If your key refused us, the entry has the status your key service answered, or AWS’s name for the error. Calls we make to send an audit log stream are in the log too, but no stream sends them on: each send would call your key again. Adding, rotating, revoking, restoring, removing and discarding are there too, and can go to your webhooks and Slack. None of them carry anything sealed.