carlos / platform Public

objstore: app shop + environment eu and app shop-eu share one bucket and IAM user Issue closed

On branch tenant-iso-objstore

Claimed by claude~paul@keymail.dev 3 weeks ago

Found by the 2026-10-03 tenant-isolation audit of main 78fc8d40. Reproduced with the in-memory provisioner in a scratch copy.

Two different stores can map to one bucket and one IAM user. objstore/walk.go:69-75 names buckets prefix+acct+"-"+app[+"-"+env], and :86-91 names users carlos-store-<acct>-<app>[-<env>]. App names and environment names (appconfig/environments.go:45) may both contain -, so app shop with environment eu gets the same bucket and user as app shop-eu.

Nothing refuses it: EnsureBucket treats BucketAlreadyOwnedByYou as success (aws.go:270-283), EnsureUser adopts the user (aws.go:390-414), ReapKeys deletes the existing key (walk.go:303), and MintKey hands a fresh one to the second caller.

Who can reach it: an Instance-role CI token bound to shop. POST .../instances is open to that role (console/authcli.go:618). With private storage on, the environment comes from the host's first label (console/apiinstances.go:954-958, instanceprivatestore.go:142,147), so creating host eu.<sqid>.<appsdomain> hands the token read/write keys to shop-eu's bucket and revokes shop-eu's own key. ?environment= on store provision reaches the same code (apistore.go:90, apistore_provision.go:58). Purging shop later runs Teardown with the recorded names, which empties and deletes shop-eu's bucket and deletes its user (walk.go:590-632). The per-store lock is keyed on (sqid, app, env), so it doesn't serialise the two.

It stays inside one account: sqids are [a-z]{3,16} with no hyphen.

Same family: handleAPIStoreGrant (apistore.go:782-803) lets an operator adopt any valid bucket name without checking whether another app's store record already names it. A typo gives tenant A a policy on tenant B's bucket. See also #191, where a retried teardown deletes a bucket granted in between.

A fix is going on a branch from this session.

Fixed by tenant-iso-objstore, merged to main as 2129e26 (Fable review plus one re-check, CI green). Production only gets it when a release is cut. Ops note: any colliding pair of stores that already exists becomes un-grantable until one of them is torn down. The branch description says how.