Repository navigation
Conversation
Hilt's record of a credible-exit ceremony (fil-one/RFC fil-forge#24, Tier 1): a customer exporting one bucket to a key they hold. Hilt owns the session and the customer-facing steps (opened, pop_verified, acked, released); the gateway serving the bucket reports the steps it performs (pinned, manifest_built, car_ready, validated) and keeps its own pin keyed by the same id. States only move forward, with abort from any open state; Pin records the bucket root the export is built from.
An open export session still needs the tenant's keys and the bucket's
space, and deletion destroys both. DELETE /tenants/{id} now answers 409
while the tenant has an open session, before anything is torn down, and
deletes the tenant's closed sessions in its cascade. /s3/bucket/delete
fails with ExportInProgress while the bucket has an open session: a
session opens here before the gateway pins anything, so the gateway's
own pin cannot be relied on to refuse the delete.
parkan
force-pushed
the
parkan/feat/export-sessions
branch
from
October 15, 2026 11:59
537ef35 to
5c17c5a
Compare
Author
|
@alanshaw I can't ask for reviews here but mind giving this a look if you have a sec? just the hilt part of fil-one/RFC#24 rebased and should be green the important surface is gateway pin |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
RFC #24
(credible exit under encryption, merged) puts Hilt in charge of the exit ceremony: the customer
opens it against the Tenant API, proves possession of the receiving key, acknowledges the bucket
root the gateway pinned, and takes delivery of the exit CAR. Nothing about that exists yet, and the
first thing it needs is somewhere to record that an exit is in flight -- because while one is, the
tenant's keys and the bucket's space are exactly what must not be destroyed.
Today
DELETE /tenants/{id}needs only a disabled tenant, and/s3/bucket/deleteneeds only anempty space. Either one during an export destroys the CEKs the key manifest still has to re-wrap,
or the closure the CAR is built from.
What changes
The store (
pkg/store/exportsession, migration00009). One row per ceremony: the tenant, thebucket, the customer's X25519 receiving key, the state, the pinned root, and an append-only jsonb
audit log. Memory and Postgres implementations in lockstep, exercised by one backend-parametrized
suite.
States move forward only, and
abortedis reachable from any open state:Hilt owns
opened,pop_verified,ackedandreleased; the gateway serving the bucket performsand reports
pinned,manifest_built,car_readyandvalidated, and keys its own pin by thesame session id.
Transitionis a compare-and-set; a state outside the machine has no edges.Pinis its own call because it records the root the customer acknowledges and the export is built from.
The gates. A tenant or bucket with an open session cannot be deleted:
DELETE /tenants/{id}answers 409ExportInProgressbefore anything is torn down -- ahead ofthe did:plc deactivation, which is the irreversible step -- and its cascade now also deletes the
tenant's closed sessions. With feat: return error codes from the Tenant API #76 merged, the body carries
{"code": "ExportInProgress", ...}./s3/bucket/deletefails with the named errorExportInProgress, whichbucketFailurerecordsso the name reaches the caller. Hilt has to carry this one: a session opens here before the
gateway has pinned anything, so the gateway's own pin cannot refuse the delete on its own.
Scope
This is the pin and nothing else. The ceremony surface (the Tenant API exit routes and the
/s3/exit/*commands the gateway polls), the Option A key release and the tenant-path rewrap areseparate changes: the first two are waiting on decisions from @alanshaw and @hannahhoward -- the
ceremony's shape and command names, and the custody exception for loading a tenant's private wrap
key into Hilt's memory, which RFC #24 requires be stated explicitly before that code is written.
Most of the store's methods have no production caller yet; the deletion gates are the first, and
the ceremony is what drives the transitions.
Notes for reviewers
b5d3b3e, so this is now00009, behind the IAM stack's00006-00008. If anything else claims00009first, say so and I will renumber again -- gooserefuses a migration that arrives out of order, so the number has to be the next free one at merge
time, not at review time.
ceremony. Hilt will generate them when the ceremony routes land.
tenant_idreferencestenant(RESTRICT), so the tenant cascade deletessessions before the row.
bucket_iddeliberately has none: the record, including its audit log,outlives the bucket it exported.
not up yet. It also needs to map this
ExportInProgressname to S3OperationAborted; it cannotimport the constant until this merges.
tenantsvc.Newandbucketsvc.Neweach take one more store, which is most of the diff outsidepkg/store/exportsession. The IAM stack, feat(api): revoke every key's delegations before removing a tenant #80 and feat(bucketpolicy): make the bucket policy an AWS bucket policy document #96 have all landed and this is rebased on topof them; the tenant gate sits ahead of feat(api): revoke every key's delegations before removing a tenant #80's revoke-then-delete cascade, since it has to refuse
before the first irreversible step.
Testing
go build,go vet,go vet -tags itest,gofmtandgo test ./...all pass. Postgres-backedstore tests run through testcontainers, against a rootless podman socket here; they skip when
nothing answers the Docker API, as the other store suites do. The state machine, both stores'
parity and both deletion gates have unit coverage.
make itesthas not been run against thisbranch.
refs https://github.com/fil-one/RFC/blob/main/rfcs/2026-08-credible-exit-under-encryption.md