← 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, for this specific step, still an incomplete one: the key-grant alert goes to the operator, which is the person it is meant to constrain. (The separate account-takeover alerts described in Part 3 do now reach the user directly — but that is a different alert for a different path.)

2. You can tie the image to a commit — but not yet independently rebuild it

A digest is only as meaningful as your ability to reproduce it, and this is the part that has moved since we first published. The running image now records the source commit it was built from, and reports it at a public endpoint (/api/health/crypto’s sibling, /api/health/version); the deploy pipeline refuses to build from anything but a committed, pushed source, and writes every release into an append-only, hash-chained log that pins commit to digest.

  git commit                                you / anyone
   a2fddd5c…                                     │
     │  pipeline refuses dirty or                │ GET /api/health/version
     │  unpublished source                       ▼
     ▼                                     { sha: "a2fddd5c…" }
  Cloud Build ──► image ──► digest sha256:e5f3…        ▲
     │  GIT_SHA baked in          │                    │ served by the
     │                            ▼                    │ running enclave
     │                     grant on the KMS key        │
     ▼                                                 │
  append-only, hash-chained release log ───────────────┘
   { seq, commit → digest, prevHash, entryHash }
     │
     ▼   verify-release-chain.mjs:  links check  +  served sha == latest
  ───────────────────────────────────────────────────────────────────
  still missing:  rebuild commit → identical bytes  ·  anchor outside us
Source commit pinned to the running digest and recorded in a hash-chained log. The last mile — rebuilding the commit to identical bytes, and anchoring the log to a witness outside our control — is what is still missing.

Two things are still genuinely not done, and we will not blur them. Our builds are not bit-for-bit reproducible — building the same commit twice does not yet produce identical bytes — and that release log is not yet anchored to a witness outside our control. So today you can read which commit the running image claims and check that we recorded it; you cannot yet rebuild that commit yourself and confirm the bytes match. The correct reading of the attestation chain is now: only one image can decrypt, we can prove which image that is, and we can tell you which commit it came from — not yet that you can independently reproduce that image from source.

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; this alert is still operator-only
Running image reports its source commit + hash-chained releasesEnforced today
Image independently reproducible from published sourceNot yet — reproducible builds + external anchor pending
Account-security alerts visible to usersEnforced today (Part 3); key-grant alert still operator-only

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