How it works
MarkdownSealing a secret
Sealing turns a key into a blob that only your enclave can open, for the apps you name and the destination you choose.
Three ways to seal#
The CLI. Use this one. It checks the enclave itself and reads the secret without echoing it. Piping it in works too.
$seal secret --to https://api.openai.com/v1 --app checkout > openai.blobThe SDK. For scripts that rotate keys. It reads SEAL_ENDPOINT and SEAL_TENANT like your app, and needs no app identity.
import os, seal
blob = seal.seal(os.environ["OPENAI_API_KEY"], to="https://api.openai.com/v1", apps=["checkout"])import { Client } from "@sealzone/sdk";
const seal = await Client.fromEnv();
const blob = await seal.seal(process.env.OPENAI_API_KEY!, { to: "https://api.openai.com/v1", apps: ["checkout"] });client, err := seal.FromEnv(ctx)
if err != nil {
return err
}
blob, err := client.Seal(ctx, []byte(os.Getenv("OPENAI_API_KEY")), "https://api.openai.com/v1", []string{"checkout"})let seal = seal::Client::from_env().await?;
let key = std::env::var("OPENAI_API_KEY")?;
let blob = seal.seal(key.as_bytes(), "https://api.openai.com/v1", ["checkout"]).await?;The console's Seal page. Handy for a test key. The page's code comes from Seal and sees the secret as you type it, so use the CLI for production keys.
What happens when you seal#
The secret is encrypted on your machine, for an enclave your machine has just checked.
The CLI and SDK refuse a blob that doesn't name your tenant and root id or carry exactly your policy. A blob opens in every replica of your tenant, whoever runs it.
Anyone who can reach your enclave can seal for your tenant. That gains them nothing. A blob holds only the secret its sealer typed in. Sealing does need your root id, which seal use and SEAL_TENANT=acme@<root id> provide.
Options#
Options are written into the blob in the clear, and the enclave enforces them on every request. To change one, seal a new blob.
--to--app--headerauthorization.--format{key}. Default Bearer {key} for authorization.--methodsGET,POST.--deny--expires--pinsha256/<base64>. Pin a CA's key so renewals don't break it.--sealed-only--receipt-fileThe SDKs take the same options under matching names, like sealed_only in Python or seal.WithHeader in Go.
Seal refuses unencrypted destinations, a password inside the URL, and query strings. A key sent in a body or query string isn't supported. APIs and databases lists what each upstream allows.
A session can do anything the credential allows. Give each key the least permissions you can, and --deny the API's key-management paths so nobody mints a new key through the enclave.
Examples#
An HTTPS API with an x-api-key header, two methods, its key paths denied and an expiry:
$seal secret --to https://api.example.com/v2 --app checkout --header x-api-key --methods GET,POST --deny /v2/keys --expires 2027-01-01 > example.blobPostgres over TLS. The user, host and database go in --to. You type only the password.
$seal secret --to postgres://[email protected]:5432/prod --app checkout > db.blobRedis over TLS, port 6380, database 0:
$seal secret --to rediss://cache.example.com:6380/0 --app checkout > redis.blobKeeping blobs#
Put the blob where the secret used to be, under a name like OPENAI_API_BLOB. A database row, secrets manager or CI variable works too. Seal keeps no copy, so if you lose one, seal again.
To rotate, seal the new value, swap the blob and revoke the old key at the provider. No switch kills a single blob, so set --expires on blobs you hand out. See Rotating and revoking.
Secrets must be at least 16 bytes, so masking can't be used to test guesses. Postgres passwords must also be printable ASCII. See Limits.