← All posts

The key that refuses to talk to us

1 August 2026 · 8 min read · TruMint


Part 1 ended on a promise: your data is decryptable only inside a running production service. That is easy to say and hard to mean. Most systems that claim it are one IAM binding away from being false, because the key is granted to a service account — and a service account is just a name that an administrator can attach to anything, including a shell.

This post is about replacing that name with a measurement, and then being precise about the gap that remains.

The problem with granting a key to an identity

The usual design gives the production service account permission to use the key. It is a real improvement over a key file on disk, and it fails in a specific way: anyone who can create a VM with that service account attached, or mint a token for it, inherits the key. The permission describes who is asking, and “who” is administratively assignable.

What we want is a permission that describes what code is asking — one that a person cannot satisfy by pointing a credential at a different program.

Attestation: proving what is running

Production runs as a single container image inside a Confidential VM using AMD SEV, on Google Confidential Space. Memory is encrypted by the CPU; the host cannot read it. At boot the platform measures what it launched and issues a signed attestation token containing, among other claims, the SHA-256 digest of the container image.

That token is exchanged for cloud credentials through a workload identity pool whose condition we pin: the assertion must come from Confidential Space and carry the hardened support attributes, which excludes debug images where an operator could attach and inspect memory. The resulting principal is not a service account. It is the image digest itself.

  ┌──────────────────────────────────────────────────────────┐
  │  AMD SEV Confidential VM · memory encrypted by the CPU    │
  │                                                          │
  │   container image  sha256:65862c0b…                       │
  │        │                                                 │
  │        ▼                                                 │
  │   platform measures the launch                           │
  └────────┬─────────────────────────────────────────────────┘
           │  signed attestation token
           │    swname            = CONFIDENTIAL_SPACE
           │    support_attributes ∋ STABLE      (no debug boot)
           │    image_digest      = sha256:65862c0b…
           ▼
  ┌──────────────────────────────────────────────────────────┐
  │  Workload Identity Pool  ·  attributeCondition enforced   │
  └────────┬─────────────────────────────────────────────────┘
           │  principal = …/attribute.image_digest/sha256:65862c0b…
           ▼
  ┌──────────────────────────────────────────────────────────┐
  │  Cloud KMS — key "field-kek", europe-west2                │
  │  IAM allow list has exactly ONE member: that digest       │
  └──────────────────────────────────────────────────────────┘
The chain from silicon to key. The KMS binding names a digest, not an identity — so it cannot be re-pointed at a shell.

Locking ourselves out

Binding the key to a digest is only half the job. On a normal cloud project the owner role implicitly grants cryptographic operations — so the founder account could have used the key regardless of who else was on the allow list. Basic roles cannot be trimmed.

The fix is an organisation-level deny policy: deny the KMS use permissions to everyone, with a single exception for the attested workload pool. Deny beats allow, and the policy sits above the project, so a project administrator cannot remove it.

We check this rather than assume it
Running an encrypt with the owner account returns PERMISSION_DENIED: cloudkms.cryptoKeyVersions.useToEncrypt. The same account cannot even list the deny policy that stops it. If either of those ever starts working, something has changed that should not have.

What a deploy has to do, and why it is awkward

If the key is bound to a digest, then shipping any code change — a copy tweak, a bug fix — produces a new digest that the key has never heard of. Deployment therefore includes a key re-grant, and that is deliberately the most sensitive step in the pipeline.

  1  build image                     → sha256:NEW
  2  grant NEW digest on field-kek     ← alerts fire on this IAM change
  3  boot new enclave beside the old   (both serve traffic — no downtime)
  4  probe NEW enclave directly:  GET /api/health/crypto
        └─ performs a real KMS unwrap + cipher round-trip
        └─ not healthy ⇒ roll back the grant, keep the old enclave, stop
  5  retire the old enclave
  6  revoke every digest except NEW    ← key answers exactly one image again
Blue/green with the key grant inside the loop. The health gate probes the NEW enclave directly, because both serve the same tunnel and a probe through the front door could pass on the strength of the old one.

Step 4 is the one that earns its keep. A root-level health check returns 200 from a service whose decryption is completely broken; only a probe that actually unwraps a key tells you the new image can do the job. Step 6 matters because a key that accumulates old digests is a key that answers several images, and the guarantee decays quietly.

Alerting on the sensitive step

Any IAM change on the key ring, and any mutation of the attestation pool, raises an alert. That is what makes the arrangement checkable rather than merely configured: the moment someone grants the key to a new digest, the fact is recorded and pushed.

What this does not prove

Here is the part that vendor architecture posts tend to leave out, and it is the reason we split this series into three.

1. We choose which digest gets the key

The deny policy stops us using the key. It does not stop us granting it. A determined operator could build an image that copies plaintext somewhere and grant that image the key — the attestation would be perfectly valid, because attestation proves which bytes are running, not that those bytes are trustworthy.

What stands in the way today is that the grant is a logged, alerted IAM change. That is a real control and an incomplete one: the alert currently goes to the operator, which is the person it is meant to constrain.

2. You cannot yet check that the digest matches our published source

A digest is only as meaningful as your ability to reproduce it. Our builds are not reproducible: the image records no source commit, and building the same code twice does not reliably produce identical bytes. So while we can tell you the running digest, you cannot independently derive it from source and compare.

This is the honest limit of the current design, and it is on the roadmap rather than done. Until it lands, the correct reading of the attestation chain is: only one image can decrypt, and we can prove which image that is — not that image is the code we published.

PropertyStatus
Key unusable by any human, including the ownerEnforced and verifiable today
Key bound to a measured image, not an identityEnforced today
Debug/inspectable boots excludedEnforced by the attestation condition
Every key grant logged and alertedEnforced today; alert routing is operator-only
Running image reproducible from published sourceNot yet — roadmap
Alerts visible to users, not just to usNot yet — roadmap

Part 3 leaves the key custody question entirely and deals with the paths that go around it: the server at runtime, the AI features, and the administrator who can still take over an account.

A three-part series on what TruMint can see
  1. Part 1What we can and cannot read
  2. Part 2The key that refuses to talk to us (you are here)
  3. Part 3Where it stops

See it for yourself: explore a sample portfolio. No signup, no bank link.

Try the live demo