Skip to content

feat(TC-887): seal KV blocks in the S3 store (S3c, stacked on #333) - #336

Draft
samgbafa wants to merge 4 commits into
skgbafa/tc-887-s3b-fs-sealedfrom
skgbafa/tc-887-s3c-s3-sealed
Draft

samgbafa wants to merge 4 commits into
skgbafa/tc-887-s3b-fs-sealedfrom
skgbafa/tc-887-s3c-s3-sealed

Conversation

@samgbafa

@samgbafa samgbafa commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

TC-887, slice S3 part c: the S3 block store seals KV blocks. Stacked: S1 #327 → #329 (S3a) → #333 (S3b, FileSystem) → this PR. Review only the top four commits (c36b098 S3 sealing, f87eb1c double-remove test and escrow doc, 3268564 review fixes, f5cb0b0 review deltas). Off by default: with TINYCLOUD_AT_REST__MODE unset or off, the store behaves as before.

What changed

  • tinycloud-node-server/src/storage/s3.rs
  • storage/mod.rs: attach_at_rest attaches the runtime to the S3 store as well as the FileSystem store, and declares the seal path for both. docs/at-rest-escrow.md says both stores seal.
  • storage/size.rs test: a double remove floors plaintext sizes at zero under both the tag and the legacy entry.
  • tests/at_rest_s3_store.rs: a new test against a real S3 API. It is ignored by default, and the harness CI job runs it against its MinIO in a new step after the RED checks.

How it was tested

  • tests/at_rest_s3_store.rs against the harness MinIO (2/2 pass):
    • Round trip: plaintext lengths 0, 17, one segment, one segment + 1, and 3 segments + 9. Each is stored only under its sealed name, as TCE1 at the exact ciphertext length, and reads back with Content::len equal to the plaintext length.
    • Ranges: every range across the segment edges matches the plaintext slice. A range past the end returns 416 with the plaintext total.
    • Quota: plaintext quota, and the boot scan on a freshly opened store.
    • Modes: a legacy object written in mode off stays readable in encrypt mode. While locked, writes and sealed reads return 503 and legacy reads are still served. Strict mode refuses legacy objects and serves sealed ones.
    • Tamper and remove: flipping one byte of a sealed object fails closed with InvalidData, before any plaintext is returned. Removing the block then makes it unreadable.
  • cargo test -p tinycloud-node --lib storage:: 27/27 pass; did_resolution_single_entry 10/10 pass. cargo clippy -p tinycloud-node --tests and cargo fmt --check are clean.
  • The full suite runs on CI (on a CI-only branch, since stacked PRs don't get CI); the run is linked in a comment below.

Review fixes (security 3c9d19cf, correctness d6a8c93b), commit 3268564

  • Range reads pinned by ETag (correctness 1, security N3): the segment GET sends If-Match with the header GET's ETag. If the object was replaced, removed or shrunk in between, the read starts over once, then answers 503. It never falls back to the legacy name once a sealed header was read, and never gives a false integrity 500.
  • contains checks only the name a write would use (INTERFACES §7). The S3 store implements prepares/prepare_read and uses LoggedDecrypt.
  • New MinIO tests in tests/at_rest_s3_store.rs (6/6 pass locally against the harness MinIO):
    • temp-file staging with From/Suffix ranges at segment edges;
    • off after encrypt, and off with locked keys (503);
    • strict with locked keys (503);
    • contains per §7;
    • a concurrent-replace test: 200 alternating re-PUTs of two valid encryptions during range reads, with no wrong bytes and no integrity error.

Delta round (6d578564, 3f0b1aec), commit f5cb0b0

@samgbafa

Copy link
Copy Markdown
Contributor Author

CI (stacked PR, so it ran on a CI-only branch): run https://github.com/TinyCloudLabs/tinycloud-node/actions/runs/39027726967 on ci/tc-887-s3c (f71b24f = d8dddd3 + the workflow_dispatch trigger). Every job passed. The harness job's new step ran at_rest_s3_store against MinIO: 2/2 passed.

samgbafa added a commit that referenced this pull request Oct 17, 2026
…e, bounded generations, required mirrors

Review a7e06ef6 on #335 (Naomi's round-5 rulings):
- Escrow-none runs: any escrow bundle or record object is RED. Every
  mirror string (exempt fields, commitments, holder names alone, in pairs
  and in order, the armored share text and its payload, record fields)
  must not carry a secret the harness derived or unwrapped (root, W,
  ARK_g, KEK_g, chain/canary/MAC keys, NK, the DEK tag key, any DEK) in
  hex, base64, base64url or base32, at any alignment.
- Escrow holder names are left out of the 16+16 split check (they are
  checked exactly by the schema).
- ARK generations above 9 are refused outright and a gap is one finding;
  no loop is bounded by a number read from a name.
- Encrypt/strict mode: an ARK mirror must exist and every block must be
  sealed-named, its NK space tag a harness space whose DEK mirror
  authenticates.
- Raw-key backstop also decodes base32 and any short label: prefix and
  flags anything decoding to more than 32 bytes.
- INTERFACES status note names #336.
Self-tests for each rule.
samgbafa added a commit that referenced this pull request Oct 17, 2026
S3BlockStore::remove called GetObjectAttributes without an attribute
list, which S3 and MinIO reject, so every S3 block delete errored.
Use HeadObject for the size and existence check, as #336 does. A
missing object is still Ok(None), and the quota drops by the stored
length. Adds a MinIO-backed test run in the at-rest harness CI job.
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3c-s3-sealed branch from d8dddd3 to 79878f5 Compare October 17, 2026 12:30
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3b-fs-sealed branch from cc0e223 to 91a2a57 Compare October 17, 2026 12:30
samgbafa added a commit that referenced this pull request Oct 17, 2026
…e, bounded generations, required mirrors (#338)

Review a7e06ef6 on #335 (Naomi's round-5 rulings):
- Escrow-none runs: any escrow bundle or record object is RED. Every
  mirror string (exempt fields, commitments, holder names alone, in pairs
  and in order, the armored share text and its payload, record fields)
  must not carry a secret the harness derived or unwrapped (root, W,
  ARK_g, KEK_g, chain/canary/MAC keys, NK, the DEK tag key, any DEK) in
  hex, base64, base64url or base32, at any alignment.
- Escrow holder names are left out of the 16+16 split check (they are
  checked exactly by the schema).
- ARK generations above 9 are refused outright and a gap is one finding;
  no loop is bounded by a number read from a name.
- Encrypt/strict mode: an ARK mirror must exist and every block must be
  sealed-named, its NK space tag a harness space whose DEK mirror
  authenticates.
- Raw-key backstop also decodes base32 and any short label: prefix and
  flags anything decoding to more than 32 bytes.
- INTERFACES status note names #336.
Self-tests for each rule.
@samgbafa

Copy link
Copy Markdown
Contributor Author

Rebased onto S1 round 3 (#327 daef3cf). Exact-head CI on 79878f5 (dispatched with workflow_dispatch on the PR branch): run https://github.com/TinyCloudLabs/tinycloud-node/actions/runs/39032820048, all jobs green.

@samgbafa

Copy link
Copy Markdown
Contributor Author

REQUEST_CHANGES: see TC-887 (head 79878f55a7a25672757a49e62c6151979033b275)

@samgbafa

Copy link
Copy Markdown
Contributor Author

REQUEST_CHANGES: see TC-887 (head 79878f5)

@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3c-s3-sealed branch from 79878f5 to 6b01237 Compare October 17, 2026 15:25
@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-887 (head 6b01237ed6d22b41f0b04d5f6b5b91ef53d4152c)

@samgbafa

Copy link
Copy Markdown
Contributor Author

Review round (6b01237): exact-head CI run https://github.com/TinyCloudLabs/tinycloud-node/actions/runs/39045005162 — all jobs green, including the 6 MinIO tests in the harness job.

@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-887 (head 6b01237)

@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3c-s3-sealed branch from 6b01237 to 98efe3e Compare October 17, 2026 16:52
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3b-fs-sealed branch from efa5bbb to 4dbf283 Compare October 17, 2026 16:52
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3c-s3-sealed branch from 98efe3e to dc5e68f Compare October 17, 2026 17:33
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3b-fs-sealed branch from 4dbf283 to 8ba1e92 Compare October 17, 2026 17:33
@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-887 (head dc5e68f)

@samgbafa

Copy link
Copy Markdown
Contributor Author

Rebased onto S1 #327 029c703. Exact-head CI on dc5e68f: run https://github.com/TinyCloudLabs/tinycloud-node/actions/runs/39054266209, all jobs green.

With encryption at rest on, the S3 block store writes new blocks as TCE1
objects under their sealed names (v1/<space tag>/<object id>) and reads
them back through the authenticated decrypting reader. Range reads fetch
the header, then only the segments that cover the range. Legacy objects
stay readable in encrypt mode and are refused in strict mode. While keys
are locked, writes and sealed reads fail with the at-rest 503. Quotas
count plaintext bytes, sealed objects by space tag. Removing a block
removes its sealed copy, or else the legacy object.

storage::attach_at_rest attaches the runtime to both stores. The object
size check uses HeadObject: GetObjectAttributes without an attribute
list is rejected by S3 and MinIO.

tests/at_rest_s3_store.rs runs against the at-rest harness MinIO
(ignored by default; the harness CI job runs it).
…ad; contains per §7; prepare hooks; more S3 tests (S3 review)
…t test asserts no aborted body; changelog (S3 review deltas)
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3b-fs-sealed branch from 8ba1e92 to a6201d0 Compare October 17, 2026 18:48
@samgbafa
samgbafa force-pushed the skgbafa/tc-887-s3c-s3-sealed branch from dc5e68f to f5cb0b0 Compare October 17, 2026 18:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant