Skip to content

feat: export sessions pin a tenant's buckets during a credible exit - #82

Open
parkan wants to merge 2 commits into
fil-forge:mainfrom
parkan:parkan/feat/export-sessions
Open

parkan wants to merge 2 commits into
fil-forge:mainfrom
parkan:parkan/feat/export-sessions

Conversation

@parkan

@parkan parkan commented Sep 23, 2026 •

Copy link
Copy Markdown

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/delete needs only an
empty 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, migration 00009). One row per ceremony: the tenant, the
bucket, 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 aborted is reachable from any open state:

opened -> pop_verified -> pinned -> acked -> manifest_built -> car_ready -> validated -> released

Hilt owns opened, pop_verified, acked and released; the gateway serving the bucket performs
and reports pinned, manifest_built, car_ready and validated, and keys its own pin by the
same session id. Transition is a compare-and-set; a state outside the machine has no edges. Pin
is 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 409 ExportInProgress before anything is torn down -- ahead of
    the 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/delete fails with the named error ExportInProgress, which bucketFailure records
    so 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 are
separate 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

  • Migration number. Rebased onto b5d3b3e, so this is now 00009, behind the IAM stack's
    00006-00008. If anything else claims 00009 first, say so and I will renumber again -- goose
    refuses a migration that arrives out of order, so the number has to be the next free one at merge
    time, not at review time.
  • Session ids come from the caller, so Hilt and the gateway can share one id for the same
    ceremony. Hilt will generate them when the ceremony routes land.
  • Foreign keys. tenant_id references tenant (RESTRICT), so the tenant cascade deletes
    sessions before the row. bucket_id deliberately has none: the record, including its audit log,
    outlives the bucket it exported.
  • The gateway side (export pin in Ingot, hanging the release queue off it) is a separate stack,
    not up yet. It also needs to map this ExportInProgress name to S3 OperationAborted; it cannot
    import the constant until this merges.
  • tenantsvc.New and bucketsvc.New each take one more store, which is most of the diff outside
    pkg/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 top
    of 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, gofmt and go test ./... all pass. Postgres-backed
store 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 itest has not been run against this
branch.

refs https://github.com/fil-one/RFC/blob/main/rfcs/2026-08-credible-exit-under-encryption.md

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
parkan force-pushed the parkan/feat/export-sessions branch from 537ef35 to 5c17c5a Compare October 15, 2026 11:59
@parkan parkan changed the title feat/export sessions feat: export sessions pin a tenant's buckets during a credible exit Oct 15, 2026
@parkan

parkan commented Oct 15, 2026

Copy link
Copy Markdown
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

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