Public Access
Move the keys when a membership changes, not just the flag
Adding somebody to a team granted them nothing readable and removing them
rotated nothing. Both were honest — the interface said so in as many words — and
both left the actual work to a button somebody had to remember to press, on a
machine that happened to hold the key. Adding now wraps every team vault this
machine can open to the new member, and removing revokes their grants and moves
each of those vaults to a fresh key that goes to whoever is left.
The rotation is where the design had to be decided rather than written. A vault
key is per generation and an item carries the generation it was sealed under, so
advancing the vault and withdrawing the old grants would make everything already
stored unreadable to everybody, including whoever pressed the button. So earlier
grants are kept: a member holds one per generation, /me serves them as
PriorKeyWraps, and VaultKeyring holds a key per generation — the newest for
writing, the item's own for reading, chosen per item on every read path. Sharing
issues one grant per generation held, because a recipient handed only the current
key would open the vault to find most of it undecryptable; revocation takes every
generation, because leaving the history behind leaves them able to read
everything written before the rotation.
The bump itself is one server transaction. POST /vaults/{id}/rekey must name
exactly current + 1 and the vault's xmin token makes that binding, so two admins
rotating at once do not both walk away believing they succeeded — the second is
refused and told to read the vault again. The server contributes the moment and
no cryptography: it cannot generate the key, cannot tell that the one it is
handed differs from the old one, and checks that the caller held the old one the
only way it can, by requiring a live grant at the current generation.
What this does not do is re-encrypt what is already stored, and the product says
so rather than the reassuring version: everything written from the rotation
onwards is unreadable to the person who left, and nothing about the past changes.
That half is deferred and is safe to add incrementally precisely because a vault
at mixed generations stays readable. ADR 0010 records the alternatives — revoking
the old grants, chaining each key under its successor, re-sealing every item in
one request against a server that caps a push at 500 operations — and why each
was rejected.
Two things fell out of the change rather than being asked for. The grant listing
would have shown a member once per generation, so it now returns one row per
holder carrying the best key they hold, which is what makes a row below the
vault's generation mean "still owed the new key". And MarkUnreadable gives up the
write target as well as reporting: a client whose vault was rotated elsewhere
would otherwise have gone on sealing items under its superseded key — readable to
its author, unreadable to everybody else, with nothing to show for it.
This commit is contained in:
@@ -285,8 +285,8 @@ rather than only in the ID token or the userinfo response.
|
||||
become owner and you become an admin, in a single transaction. Not two role changes — promoting first
|
||||
leaves the team owned twice, demoting first leaves it owned by nobody, and there is nobody with the
|
||||
authority to finish a transfer that stopped in the middle. You are demoted rather than removed, so you keep
|
||||
your vault key grants; removing you would revoke them and flag every team vault for rekey, and somebody
|
||||
handing over a team is usually staying in it.
|
||||
your vault key grants; removing you would revoke them and rotate every team vault, and somebody handing
|
||||
over a team is usually staying in it.
|
||||
|
||||
**Archiving a team is refused while it owns a vault, and that is a limit rather than a rough edge.** A team
|
||||
vault is readable *because* of membership, so archiving a team that still owned vaults would take them away
|
||||
@@ -299,10 +299,19 @@ still holding.
|
||||
|
||||
Four limits, stated rather than discovered:
|
||||
|
||||
- **Removing a member is not retroactive.** It revokes their grants and flags the team's vaults for rekey,
|
||||
and blocks future reads. Everything they already pulled is on their machine. Rotate the SSH credentials
|
||||
- **Removing a member is not retroactive.** It revokes their grants, rotates every team vault your machine
|
||||
can open, and hands each new key to the members who are left — so nothing written from that point on is
|
||||
readable to them. Everything they already pulled is still on their machine. Rotate the SSH credentials
|
||||
that matter — that is the actual remediation, and it is why there is no button labelled anything stronger.
|
||||
- **The rekey is flagged, never performed.** See the milestone note above.
|
||||
- **A rotation re-keys the vault, not what is already in it.** Existing items stay sealed under the
|
||||
generation they were written with, and everybody still in the team keeps those keys as well as the new
|
||||
one — which is what stops a rotation making the vault's own history unreadable. It also means somebody
|
||||
who left with a copy of the old key could still open old ciphertext they later got hold of. Re-sealing
|
||||
stored items under the new key is the remaining half; see [ADR 0010](docs/adr/0010-vault-key-rotation.md).
|
||||
- **Adding a member shares the vaults you can open, including their history.** Membership is still one act
|
||||
and a key is still another — nothing changed about that — but the client now performs the second one for
|
||||
you, wrapping every generation it holds so the new member can read the vault back to its first item. A
|
||||
vault your machine holds no key to is skipped and says so; somebody who holds it has to share that one.
|
||||
- **Host key trust stays in your personal vault.** A pin approved for a team's host is recorded and used
|
||||
from your own vault, not the team's, so a teammate cannot pre-approve a fingerprint that your client will
|
||||
then trust silently for a host you defined. The cost is that each member approves a team host's key once
|
||||
@@ -570,7 +579,7 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
directories, an interrupted **upload** starts again rather than resuming (an object cannot be written from
|
||||
the middle), and a rename is a copy and a delete rather than one atomic operation. Downloads do resume — a
|
||||
ranged GET is part of the protocol, which is the one place a bucket beats SFTP.
|
||||
- **M3 — teams**, sharing, ACLs. *Done, except rekey.* Teams with roles, a public-key directory, the
|
||||
- **M3 — teams**, sharing, ACLs. *Done.* Teams with roles, a public-key directory, the
|
||||
append-only key log served for clients to verify against, team-owned vaults, and vault key grants
|
||||
wrapped by a client and stored opaquely by the server. `VaultAccessService` now resolves team
|
||||
membership to permissions, so a viewer may pull and may not push; the desktop client reads and syncs
|
||||
@@ -582,20 +591,24 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
knowing before you rely on it; the reasoning is in
|
||||
[ADR 0009](docs/adr/0009-team-access-model.md).
|
||||
|
||||
**What is deliberately not here: the rekey itself.** Removing a member revokes their grants and flags
|
||||
every team vault `RekeyRequired`, and nothing acts on that flag. A rekey re-wraps every item's data key
|
||||
under a fresh vault key and can only be performed by a client that holds the current one; that is M5's
|
||||
key rotation. Until it lands the flag is what the interface reads to say a rotation is owed, which is
|
||||
more honest than a button that only appears to do it.
|
||||
**Membership changes now move the keys, not just the flag.** Adding somebody wraps every team vault the
|
||||
adding machine can open to them — every generation of each, so they can read the vault's history and not
|
||||
only what happens next. Removing somebody revokes their grants, advances each vault it can open to a
|
||||
fresh key generation in one server transaction, and wraps that key to the members who remain. What a
|
||||
rotation buys is exact: everything written from then on is unreadable to the person who left. Items
|
||||
already stored keep the generation they were sealed under and are not re-encrypted — that half is still
|
||||
outstanding, and it is safe to add later precisely because a vault at mixed generations stays readable.
|
||||
See [ADR 0010](docs/adr/0010-vault-key-rotation.md).
|
||||
|
||||
**Ownership transfer is here, and it is one write rather than two.** The member you name becomes owner
|
||||
and you become an admin, in a single transaction — because ownership is sole, so promoting first leaves
|
||||
the team owned twice and demoting first leaves it owned by nobody, and there is nobody left with the
|
||||
authority to finish a transfer that stopped in the middle. Nothing else is touched: you keep your vault
|
||||
key grants, because removing the outgoing owner would revoke them and flag every team vault for rekey,
|
||||
which is a much larger act than the one being asked for.
|
||||
key grants, because removing the outgoing owner would revoke them and rotate every team vault, which is a
|
||||
much larger act than the one being asked for.
|
||||
- **M4 — hardening and ops**, packaging, self-hosting guide.
|
||||
- **M5 — multi-provider OIDC**, key rotation, per-item content keys.
|
||||
- **M5 — multi-provider OIDC**, identity key rotation, re-sealing a rotated vault's stored items,
|
||||
per-item content keys.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
Reference in New Issue
Block a user