Summary (Branch tls_key)
Two gaps in the freshness-anchor design allow rollback in specific scenarios, despite the anchor mechanism working as intended against the host:
- The anchor bucket identity is not committed in the attested
state_root, so a principal with SSM write access (deployer/admin/root — not the host) can repoint AnchorBucketName at an empty bucket and cause a silent, undetectable rollback to genesis.
- A crash between the DynamoDB commit and
anchor.Record leaves the newest version unanchored, so that one increment can be rolled back undetected until the key is written again.
1. Anchor bucket pointer is root-repointable (silent rollback to genesis)
NewFreshnessAnchor reads the bucket name from the SSM param /<deployment>/<app>/AnchorBucketName at every boot (runtime/anchor.go:64, runtime/anchor.go:308). Meanwhile stateRoot() commits the KMS key ID, static-secret ciphertext hashes, and DEK ciphertext hash — but not the anchor bucket name (runtime/state_origin.go:208-256, stateRootInputV1).
Consequence: on a StartStateResume boot the state-origin receipt verifies cleanly even if AnchorBucketName now points at a fresh, empty (but compliance-locked) bucket. Establish then finds no anchored keys, builds an empty versionFloor, and the enclave happily accepts arbitrarily old DynamoDB state — indistinguishable from a legitimate first boot.
The host cannot do this (read-only IAM on the param), so the live threat is a principal with broader SSM write: deployer, admin, account root, or future IAM drift. This is asymmetric with the rest of the design: ENCLAVE_KMS_KEY_LOCKED strict mode denies PutKeyPolicy even to root, yet the freshness anchor's pointer remains root-repointable.
Fix: fold the anchor bucket identity — {bucket name, owning account ID, region} — into stateRootInputV1 so that a repoint changes the recomputed state root and receipt verification fails closed on resume. Concretely:
- Add an
ssmArtifactV1{Name: anchorBucketParam(), Value: bucket} entry (plus account/region, e.g. as additional committed artifacts) in stateRoot().
- On resume,
verifyStateOriginReceipt then inherently pins the bucket; NewFreshnessAnchor should read the bucket from the same verified value (or verify-after-read) rather than trusting the live SSM param independently.
2. Committed-but-unanchored write window
kvStore.write orders DynamoDB commit → anchor.Record (runtime/kvstore.go:742), deliberately, so that anchored ≤ live always holds (the inverse ordering would false-halt the enclave on benign crashes). The comment acknowledges: "a failure here leaves the write committed but unanchored."
Consequence: if the enclave crashes (or S3 fails) between the commit and the anchor write, the newest version of that key is floored only at the previous anchored version. That single increment can be rolled back undetected until the next write to the same key re-anchors it. The writing client saw an error so shouldn't assume durability — but subsequent readers will serve the un-floored value as fresh.
Fix options (in preference order):
- Write-ahead intent + reconcile: record an anchor intent (key + next version) before the DynamoDB commit; on boot/periodically, reconcile intents — if live ≥ intent version, finish the anchor; if live < intent, it's the benign-crash case (commit never landed), drop the intent. This closes the window without the false-halt failure mode.
- Serve reads only up to the anchored version: readers treat the anchored version as the visibility horizon; a committed-but-unanchored head is re-anchored (or rejected) on first read. Simpler, but turns an S3 outage into a read availability loss.
References
runtime/anchor.go:64 — bucket read from SSM at boot
runtime/anchor.go:84-111 — Establish builds the version floor from whatever bucket the param names
runtime/state_origin.go:208-256 — stateRoot() pre-image (no anchor bucket)
runtime/kvstore.go:742 — commit→anchor ordering and the acknowledged window
Summary (Branch tls_key)
Two gaps in the freshness-anchor design allow rollback in specific scenarios, despite the anchor mechanism working as intended against the host:
state_root, so a principal with SSM write access (deployer/admin/root — not the host) can repointAnchorBucketNameat an empty bucket and cause a silent, undetectable rollback to genesis.anchor.Recordleaves the newest version unanchored, so that one increment can be rolled back undetected until the key is written again.1. Anchor bucket pointer is root-repointable (silent rollback to genesis)
NewFreshnessAnchorreads the bucket name from the SSM param/<deployment>/<app>/AnchorBucketNameat every boot (runtime/anchor.go:64,runtime/anchor.go:308). MeanwhilestateRoot()commits the KMS key ID, static-secret ciphertext hashes, and DEK ciphertext hash — but not the anchor bucket name (runtime/state_origin.go:208-256,stateRootInputV1).Consequence: on a
StartStateResumeboot the state-origin receipt verifies cleanly even ifAnchorBucketNamenow points at a fresh, empty (but compliance-locked) bucket.Establishthen finds no anchored keys, builds an emptyversionFloor, and the enclave happily accepts arbitrarily old DynamoDB state — indistinguishable from a legitimate first boot.The host cannot do this (read-only IAM on the param), so the live threat is a principal with broader SSM write: deployer, admin, account root, or future IAM drift. This is asymmetric with the rest of the design:
ENCLAVE_KMS_KEY_LOCKEDstrict mode deniesPutKeyPolicyeven to root, yet the freshness anchor's pointer remains root-repointable.Fix: fold the anchor bucket identity —
{bucket name, owning account ID, region}— intostateRootInputV1so that a repoint changes the recomputed state root and receipt verification fails closed on resume. Concretely:ssmArtifactV1{Name: anchorBucketParam(), Value: bucket}entry (plus account/region, e.g. as additional committed artifacts) instateRoot().verifyStateOriginReceiptthen inherently pins the bucket;NewFreshnessAnchorshould read the bucket from the same verified value (or verify-after-read) rather than trusting the live SSM param independently.2. Committed-but-unanchored write window
kvStore.writeorders DynamoDB commit →anchor.Record(runtime/kvstore.go:742), deliberately, so thatanchored ≤ livealways holds (the inverse ordering would false-halt the enclave on benign crashes). The comment acknowledges: "a failure here leaves the write committed but unanchored."Consequence: if the enclave crashes (or S3 fails) between the commit and the anchor write, the newest version of that key is floored only at the previous anchored version. That single increment can be rolled back undetected until the next write to the same key re-anchors it. The writing client saw an error so shouldn't assume durability — but subsequent readers will serve the un-floored value as fresh.
Fix options (in preference order):
References
runtime/anchor.go:64— bucket read from SSM at bootruntime/anchor.go:84-111—Establishbuilds the version floor from whatever bucket the param namesruntime/state_origin.go:208-256—stateRoot()pre-image (no anchor bucket)runtime/kvstore.go:742— commit→anchor ordering and the acknowledged window