Skip to content

Rollback protection gaps: anchor bucket identity not committed in state_root; committed-but-unanchored write window #141

Description

@aruokhai

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:

  1. 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.
  2. 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):

  1. 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.
  2. 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-111Establish builds the version floor from whatever bucket the param names
  • runtime/state_origin.go:208-256stateRoot() pre-image (no anchor bucket)
  • runtime/kvstore.go:742 — commit→anchor ordering and the acknowledged window

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions