Caution is backed by Y Combinator. Learn more.

All posts

Secrets only verified enclave code can read

Two machines connect through pipes and a transparent chamber containing a glowing pink key.

TL;DR We put upstream Trustee, the key broker from Confidential Containers, in a Caution enclave. An admin verifies the client build, sets which secrets it may read and uploads the policy and secrets. The issuer verifies attestation and issues a five-minute token. The broker checks that token and applies the policy on each secret request.

View the demo code

  1. Where this fits
  2. How it works
  3. Why it holds
  4. What it doesn’t solve yet
  5. Beyond the demo
  6. Verify the broker, not just the client

Applications rely on secrets such as database passwords, API keys, OAuth tokens, signing keys and encryption keys. For an application running in an enclave, those secrets need a secure path in. Plaintext values in deployment configuration are visible to whoever manages it. Secrets baked into an image are readable by whoever has the image. A cloud KMS runs code you can’t check, and whoever controls its key policy can change who gets a secret without anyone seeing.

What you want is a vault that hands a secret only to code it has checked, and that you can check too. We wanted to see how close you get with off-the-shelf parts and nothing trusted but the hardware and the code you can read.

Where this fits

Caution already has a way to get secrets into an enclave: the Key Services. It splits the key that protects an app’s secrets among a quorum of holders, and a threshold of them has to take part before the secrets reach a verified enclave. People stay in the loop, and it is what we recommend today for sensitive workloads.

This demo automates release under a policy controlled by one admin. A quorum could govern policy changes while the broker continues serving approved workloads automatically. That shared control is not implemented here, so treat this as an exploration, not a replacement for the Key Services.

How it works

Two Caution apps run on AWS Nitro Enclaves. The trustee holds the vault. The client is a demo app that asks it for secrets by name.

The trustee runs two programs. Our Nitro issuer checks attestation evidence. The upstream Trustee Key Broker Service (KBS) holds the policy and the secrets. The issuer is the only way in: the KBS listens on loopback only, and TLS ends inside the enclave.

Trustee’s request handling and authorization logic are unchanged. We adjust its build script to embed a deterministic timestamp so the image can be reproduced.

Every call reaches the KBS through the issuer The client enclave sends challenge, token and secret requests to the Nitro issuer inside the trustee enclave. The admin laptop reads the client's attestation and sends policy and secrets to the issuer. The issuer forwards calls to Trustee KBS, which applies the policy and returns the secret encrypted to the client's public key (pk_client). Every call reaches the KBS through the issuer Trustee enclave · $TRUSTEE_DOMAIN Nitro issuer Checks the NSM attestation document of the client Signs a 5-minute ES256 JWT bound to pk_client The only way in: forwards resource calls to the KBS Trustee KBS v0.22.0 Verifies the issuer JWT Applies the rego policy Returns secret as a JWE to pk_client challenge, token, secret policy, secrets reads attestation Client enclave $CLIENT_DOMAIN POST /fetch: one named secret GET /status: SHA-256 only Admin laptop approve-client: verify client, write policy load: upload policy and secrets (admin JWT) TLS pinned to the attested trustee cert
Swipe to explore diagram

The client and the admin both go through the issuer, and only the client enclave can decrypt what the KBS sends back.

How the client gets a secret

A fetch takes three steps:

  1. The client gets a one-time challenge from the issuer and generates a fresh key pair. It asks the Nitro hardware for an attestation document containing the challenge and the client’s public key. The private key stays inside the client enclave.
  2. The issuer checks the document’s AWS signature chain, the challenge and the public key. It answers with a five-minute token that states the client’s measurements, PCR0 to PCR2, and binds them to the client’s public key.
  3. The client shows the token to the KBS. If the admin’s policy lets that build read that secret, the KBS returns it encrypted to the client’s public key. The client decrypts it with the matching private key.

The split of jobs is the point. The issuer only answers “is this a real, fresh Nitro enclave, and what code does it run?” It keeps no allowlist. The KBS answers “may that code read this secret?” using Trustee’s existing passport mode: it trusts the issuer’s tokens through a signing key the issuer hands it at boot.

How the admin approves access

The admin works from a laptop with an admin key.

  1. caution verify rebuilds the client from source and checks its measurements against the live enclave.
  2. make approve-client turns those measurements into a policy.
  3. make load uploads the policy, then the secrets.

Both the policy and the secrets go over TLS pinned to the certificate the trustee attests. The trustee boots with a deny-all policy, so nothing is released before the admin has spoken.

Why it holds

Each answer above comes from one check:

  • Genuine. The document’s certificate chain must end at the AWS Nitro root. That root is compiled into the issuer, so it is part of the trustee’s own measurements.
  • Fresh. A challenge works once, for 60 seconds, so a recorded document is useless.
  • Bound. The document must carry the client’s public key. Only the matching private key, held inside the client enclave, can decrypt the response. A document lifted from the public /attestation endpoint has no public key.
  • Approved. The policy names the build (PCR0 to PCR2), the issuer and each secret. Everything else falls through to deny.
  • Visible. The policy in force is public at /v1/policy, with its hash. Only the admin key can change it, and the admin’s tools talk to the trustee over TLS pinned to the certificate it attests.

What it doesn’t solve yet

Where the code runs. This is the biggest one. The policy pins what the client runs, PCR0 to PCR2, but not which Caution app runs it. Nitro measures that too, in PCR3, as the IAM role of the machine. The repo is public, so anyone can deploy the same client in their own Caution app, and it would pass. Pinning PCR3 needs a source for it we can trust, such as the platform reporting each app’s role. Until then we left it out rather than read it off the network.

One admin key. The admin is one key on one laptop, and whoever holds it decides who reads what. A quorum of keys, as Locksmith uses, is the natural next step.

Memory only. The trustee keeps the policy and the secrets in enclave memory. A reboot wipes them, and the admin uploads them again. Each boot also fetches a new TLS certificate, and Let’s Encrypt allows five a week per name.

Public hashes. The client’s /status shows each secret’s SHA-256 so you can check it. That is only safe for random secrets: never store a guessable password this way.

Beyond the demo

This follows the same pattern as AWS KMS with Nitro: the key service checks attestation, and the key policy decides whether to grant access. Here, the broker and issuer are open source and run together inside a Caution enclave you can verify yourself. The current demo runs on AWS Nitro Enclaves. Our goal is to extend this model to AMD SEV-SNP, Intel TDX and TPM 2.0 as Caution adds support for those platforms.

Verify the broker, not just the client

Attestation tells you what code is running. A key broker decides who gets a secret. Most setups ask you to trust whoever runs the broker to connect the two.

Here you can check that link yourself. The broker runs in an enclave you verify, it only believes evidence the hardware signed, and the rules it applies are public. The code that reads your secret is the code you approved.

Try the demo

Follow the demo at git.caution.co/public/demo-trustee-kbs. The README walks you through setting up both apps, deploying them, approving access, loading secrets and fetching them from the client.

Try requesting a secret before approval, after approving the client build and after changing that build. The expected results are denial, successful release and denial again.