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 │
└──────────────────────────────────────────────────────────┘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.
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 againStep 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 usTwo 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.
| Property | Status |
|---|---|
| Key unusable by any human, including the owner | Enforced and verifiable today |
| Key bound to a measured image, not an identity | Enforced today |
| Debug/inspectable boots excluded | Enforced by the attestation condition |
| Every key grant logged and alerted | Enforced today; this alert is still operator-only |
| Running image reports its source commit + hash-chained releases | Enforced today |
| Image independently reproducible from published source | Not yet — reproducible builds + external anchor pending |
| Account-security alerts visible to users | Enforced 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.
- Part 1What we can and cannot read
- Part 2The key that refuses to talk to us (you are here)
- Part 3Where it stops
See it for yourself: explore a sample portfolio. No signup, no bank link.
Try the live demo