Known gaps
The open items of Spinbox’s security model, from spin’s docs/security/README.md at 8eee32b9d. The numbers are its rules.
What no line keeps yet. Each is a design change, not a patch, which is why it is here.
- A team’s network policy reaches what its admins say. Where an installation sets
team_network_policies, any team’s admin - and every person administers their personal team - can let that team’s workspaces reach any destination past the node drops, whatever the installation’s own policies allow. The setting is the whole control; there is no ceiling a team’s policy is held under. - I12 through a proxy holds as the proxy does. The proxy is asked to connect to the address that was checked, and one that connects elsewhere is the operator’s choice, as the proxy is. The storage endpoint an admin sets is deliberately exempt: an S3 inside the installation’s own network is the normal deployment.
- A name a policy allows is held to what DNS and the client say. An address serving many names
is open to all of them once one is allowed; a rule that checks SNI (
netpolicy.Rule.VerifySNI) narrows that for a client that names the host it wants, and passes a tenant that names an allowed host to reach another behind the same address (domain fronting). Only a brokered rule that injects, whose proxy holds each request’sHostto the name, keeps a tenant to it. - Every host writes
layers/. A layer is content-addressed, create-only and locked, and opened only through a manifest its writer signed, so a host can add layers nothing reads - and pay for them - and change none. Under the volumes it writes only its own subtree of each, never the grant or the descriptor (bucketpolicy.For), and nothing there is served without its holder’s signature and grant. A host a volume left keeps that credential for up to a quarter of an hour, which the store cannot end early: it reads the volume as it held it - and a snapshot asked for before the move, which the new holder seals under the version it was asked at - and adds commits and HEADs under its own subtree that no restore takes, each counted as it is passed over and every request in the bucket’s access log (I8). - A key a host was given opens what it sealed until the volume is re-keyed. A volume’s key never turns, and an honest host forgets it when the volume leaves; a taken one keeps it. Revoking a host marks every volume it was granted for a re-key, which runs once the workspace is stopped: until then the layers that key opens are open to whoever holds it, and the re-key deletes the volume’s snapshots with it, being exactly what that key opens.
- The storage layer holds nothing against a control plane that is taken. It mints every credential and grants every key: one that is taken clones any snapshot to a workspace of its own on an attested runner, and comes in with a keyring of its own.
- A move between a detach and its attach is the old host’s. A grant is made over the HEAD the
volume’s history reaches when the control plane places it, so what the host it left wrote in
that window - seconds, when the move is one
RecoverWorkspace- is history the new holder starts from. - I14 and I15 in a development installation. A developer’s runner is not root: its QEMU and its qemu-img run as itself, with nothing of root’s to keep from them, and a kernel without core scheduling runs machines side by side on one core’s siblings. Such an installation places on any host; only a production one requires what each host says it keeps.
- What a host says it keeps is the host’s word. On AWS that is the word of a runner build the host proved it booted (I13), on the image whose kernel has all three; a self-hosted host’s is believed as its registration token is.
- I16 tells a store’s clients apart by port, not by what each asks. Neither Quickwit nor the metric store’s writes take a key, so the kernel refuses a connection by the caller’s group, and a client let through a port has all of it: the collector reaches the whole of Quickwit’s gRPC, which carries Quickwit’s own services beside the logs it is sent, and Grafana can write samples as well as read them. Root on the control plane’s machine can drop the table; that root is the installation already.
- I17 and I18 hold against the control plane and the operator, not against the tenant, nor a guest. Root in a guest is anybody the access lets in - its owner and each guest alike: it can rewrite the drop-in and the supervisor’s answers. Sealing keeps everyone else out; it does not keep the people let in from the machine they were let into.
- A taken control plane can start a person’s keyring anew. It does through the account what a
person who lost every key does (
StartKeyringAnew), and a keyring started anew is somebody no access or roster signed before names - so it gets into none of their workspaces and none of their teams of more than one - but it can make new workspaces that answer to it, and sign the person’s own team of one again (roster.Roster.Start), whose identity then names those workspaces. The CLI and the terminal show a keyring started anew before it is signed; nothing tells a person their account was used to start one. - A guest is let into the guest by the same certificate as the owner. The host decides who
logs in, not what a login may do: a guest is root, and changes nothing the control plane holds -
middleware.guestMay- but can change the machine itself. Sharing a workspace is trusting the guest with it. - A control plane can withhold the owner’s newest access. It carries every one, so one taken
before an access reaches the bucket can keep it from the bucket and the host - a guest taken out
logs in until the host reads it - and answer the owner’s client with an older one. An access the
bucket has is past withholding: the host reads it at every login. A client signs nothing over an
access behind the newest it signed itself (
~/.spin/words, the terminal origin’s storage); a client that signed nothing yet of the workspace has no newest to hold it to. - The egress root is trusted on first use. An owner’s client pins the first root an
installation offers (
~/.spin/egress-roots, the terminal origin’s storage), so a control plane that is taken can offer its own to a client that never pinned one. A person closes that by comparing the fingerprint withspin-controlplane egress-rooton the control plane’s machine. A guest whose access names no root, or another, trusts none, and its brokered hosts fail TLS: denial, not a read. - I21 holds against a confused deputy, not a taken control plane. The control plane tags the
session, so one that is taken tags it with any team, and it holds every value it reads in the
clear, as it does a sealed one. The tag is only as good as who may tag a secret: a principal of
the account that can tag one
spin-team-idhands it to that team. - The terminal’s code is trusted as the installation’s Terraform wrote it. Whoever can write
term.<domain>’s bucket, which the boundary denies every installation role, serves the code that holds a session and asks the passkey. It is the release’s signed artifact, pinned by version. - I19 holds the identity key to the image a host booted, not to the host. Every host shares the key, because the published key set is one key: a runner taken at run time signs for any subject. A self-hosted installation has no key service, and its control plane keeps the key it grants.
- A keyring and a roster are trusted from their first version. A version is signed by a key the one before trusts, and kept under the bucket’s lock in GOVERNANCE, which the account’s root can lift. A user or a team made while the control plane is taken starts a chain whose first version the control plane can sign - and a team it makes is its own, which nothing trusts.
- The domain an identity is named under is the control plane’s word. It comes with the host’s
policy, and a taken control plane can change it - which a relying party refuses, since the
issuer it names publishes another key set: denial, not a widening. An owner who names
"*"in their word trusts the installation’s list of audiences, which a taken control plane writes. A member of a parent team is no member of a team below it: a roster names a team’s own members.
Generated by task reference from spin/docs/security/README.md at 8eee32b9d.
Documents Spinbox v20261019.02. This page as Markdown, for a model or a terminal.

