Rules and their gates
The rules every entry point must keep, from spin’s docs/security/README.md at 8eee32b9d. Code cites a rule’s number at the line that keeps it. Gate is what fails a build when the rule is broken; none marks a rule kept only by each place remembering it. What is still open is on Known gaps.
| # | Rule | Gate |
|---|---|---|
| I1 | Every procedure and route behind auth names its scope class. | TestEveryProcedureHasAScope |
| I2 | Every id taken from a request is authorized for the caller before anything is read or written with it. | TestEveryHandlerResolvesTheWorkspaceItActsOn, TestEveryProcedureUsesTheMechanismItNames |
| I3 | “Not yours” is answered as “not found”, and ownership is checked before state. | the same |
| I4 | Membership and role are asked at the time of the action, not only when the resource was made. | TestOnlyTheListedProceduresSkipTheTeam, TestAnOwnerRemovedFromTheTeamCannotUseItsWorkspace, TestAPortOfATeamsWorkspaceIsReachedOnlyByWhoIsInTheTeam, TestAStreamIntoATeamsWorkspaceEndsWithItsViewersMembership - the gate reads handlers that resolve a workspace, and a port’s decision is not one, so a new way in that decides for itself is held by a test of its own and not by the gate |
| I5 | A cookie-authenticated request that changes state or opens a socket names the dashboard’s origin (or none) - or the browser terminal’s, for the calls it makes alone. | TestAPageOnAnotherSiteCannotSpendTheVisitorsCookie, TestEveryHTTPRouteIsMountedBehindAuth, TestOnlyTheTerminalIsTheTerminalsOrigin, TestEveryTerminalRouteIsOneServed |
| I6 | A credential is minted from crypto/rand, stored only hashed or sealed with its resource as AAD, compared in constant time, and revoked with what issued it. A guest holds none to the control plane: its host carries its supervisor’s stream under the host’s own (I8), and revoking the host cuts every stream it carries. | TestAGuestsStreamIsCarriedByItsHostAndCutWithIt; otherwise none |
| I7 | Bytes a guest controls are bounded (size, count, time) and made valid (UTF-8, paths) where they cross into the host. | the CHECKs; TestWhatAGuestWritesIsBoundedBeforeItIsARow |
| I8 | A host is trusted only for what is placed on it, and says only what a host may say - its guests’ supervisors included, whose streams it carries under its own token. | runnerMessageBinding and its two tests; TestAHostCarriesOnlyTheSupervisorsOfWorkspacesPlacedOnIt, TestWhatReachesTheControlPlaneIsTheSupervisorsCall; TestAHostsMintedCredentialWritesOnlyItsOwnSubtree, against the store; TestAHostsCredentialsLastAQuarterOfAnHour, TestAKeyForgottenIsAskedForAgainAndRefused, TestARevokedHostsVolumesArePendingRekey, TestARekeyedVolumeOpensWithItsOwnKeyAlone, TestAHeadWrittenByAHostThatLeftIsPassedOverAndSaid, the DST’s a-share-opens-only-its-snapshot and a-rekeyed-volume-depends-on-no-other-key; TestAMovedVolumeIsRestoredFromItsHoldersHistory, TestAMoveGrantsTheVolumeOverItsHistory, the DST’s a-moved-volume-restores-its-new-holders-history, the storage lane’s TestAMoveFencesTheHostTheVolumeLeft |
| I9 | Object-store bytes are checked against the database-anchored digest before use; a shared key is read at its published version. | TestALayerWrittenOverIsStillReadAsPublished |
| I10 | An effect outside the database happens before the row that names it is forgotten. | none |
| I11 | Every third-party action is pinned by commit. | task lint:workflows |
| I12 | A URL a non-operator can set is refused when it resolves into the node’s networks. | TestAConnectionToThisNodeIsRefusedAsItIsMade, TestARedirectIsHeldToTheRule, TestAProxiedRequestGoesToTheAddressItWasCheckedAt, TestAnIssuersEndpointsAreHeldAsTheIssuerIs |
| I13 | A component that speaks for another principal proves it is that component. | TestOnlyTheProxyAsksForABrowser, TestAProxyIsGivenItsTokenForWhatItProves, TestAProxysProofIsGivenATokenOnce, TestAnOperatorGivesTheProxyItsTokenFromTheCommandLine, TestAMachineJoinsByWhoItIsOnlyAsANewHostOfANamedRole, TestAHostIsTheBuildItJoinedAs, TestAControlPlaneRetiresTheHostsOfBuildsItNoLongerServes, TestARetiredHostIsRevokedOnceItsWorkspacesAreSuspended, TestARetiredHostThatKeepsItsWorkspacesIsRevokedAtItsDeadline, TestARetiringHostIsGivenOnlyWhatItsDrainTakes, TestAHostOfARetiredBuildRunsNoWorkspace |
| I14 | A machine’s QEMU reaches only what it is handed, and holds only what it is bounded to - a core included. | TestAProductionInstallationPlacesOnlyOnIsolatedHosts, TestEveryIsolationIsAskedOnce, TestAHostSaysItKeepsWhatItCanGiveAMachine, TestSMTTurnedOnGivesTheMachinesCoresOfTheirOwn, TestAHostThatNoLongerKeepsItsMachinesApartIsDrained; the VM lane’s TestAMachinesQEMUReachesNothingOnTheHostOrAnotherMachine, TestAGuestAnswersOnlyTheRunner, TestAMachineIsBornWithinItsBounds; TestAMachineWithAUserOfItsOwnIsLaunchedApart, TestARunnersMachineMergesNoPagesAcrossMachines |
| I15 | A program that parses a tenant’s bytes on the host opens only the files of its operation. | TestOnlyARootRunnerOnANewLandlockSaysItHoldsAProgramToItsFiles, TestAProductionInstallationPlacesOnlyOnIsolatedHosts, TestQemuImgDoesItsWorkWithOnlyItsFiles, TestAProgramReadsOnlyWhatItIsGiven, TestAProgramWritesOnlyWhatItIsGiven, TestAProgramMakesNoSocket; the VM lane’s TestARootRunnersProgramHoldsNothingOfRoots; the storage fakes refuse a file a run was not given, as the kernel does |
| I16 | A store on the control plane’s machine is reached by the installation’s administrators only through the control plane, and by its processes only for what each is. | TestOnlyTheControlPlaneSaysWhoTheAdministratorIs, TestGrafanaShowsTheStoreToTheAdministratorItIsTold, TestNothingWithoutTheStoresKeyTakesItsMetrics, TestTheStoresRunWhileTheirControlPlaneDoes (each account to each port, against the kernel), TestTheStoresGuardNamesEveryPortAndOnlyAccountsTheImageHas; the dashboard lane’s Monitoring access and Admin Metrics |
| I17 | Only the keys of the people a workspace’s access names - its owner and the guests they invited - log in to it, each with the installation’s certificate over it; the control plane holds no key that logs in and is never an SSH client of a guest (ADR-0030). | the sshd lanes’ TestSSHDLetsInOnlyACertificateOverAKeyTheHostAdmits, TestThePasskeyTerminalLogsInToTheClaimedGuest; TestAPreparedRootLetsInOnlyByItsHostsWord, TestALoginIsWhatTheHostSaysOfTheKey, TestALoginIsWhatTheNewestAccessSaysOfTheKey, TestAWordAdmitsTheKeysOfThePeopleItNames, TestTheNewestKeptWordIsTheHighestItsOwnerSigns, TestAMachineIsGivenItsOwnWorkspacesNewestAccess, TestTheControlPlaneKeepsOnlyTheOwnersNewerAccess, TestAGuestIsLetInByTheAccessThatNamesThem, TestATunnelCertifiesOnlyAKeyOfSomebodyTheAccessNames, TestOnlyTheListedProceduresAdmitAGuest, TestAKeyringStartedAnewIsAnotherChain; the VM lane’s TestOnlyThePeopleTheAccessNamesLogInToAWorkspace, TestAGuestTakenOutStaysOutOfAWorkspaceRestoredToBeforeIt, TestTheOwnerLogsInToACloneOfTheirOwnSnapshot; TestAGuestIsConnectedToOnlyByAClaimThatHolds, TestASessionIsRefusedBeforeTheRelayIsDialled |
| I19 | A workspace’s identity is signed by the host that runs it, for the machine that asked, named only as its owner’s access says - its team, owner, image and parent, fixed by the first - held to the owner’s keyring and the team’s roster, which people sign, and given only for an audience the access names; the control plane holds no key that signs one (ADR-0030). | TestATokenIsTheAskingMachinesWorkspace, TestATokenIsForAnAudienceItsOwnerNamed, TestAWordGivesTheIdentityForTheAudiencesItNames, TestARunIsHeldToTheFirstKeyItPresents, TestAMachineIsNamedAsItsAccessSays, TestAnOwnerWhoseKeyringStartedAnewNamesNoMachine, TestABookSaysWhoIsAMemberByTheKeyringTheRosterNames, TestAKeyringIsAChainItsOwnKeysSigned, TestARosterIsAChainItsOwnersSigned, TestOnlyATeamOfOneStartsAnew, TestASignatureIsKeptOnlyAsProposedByWhoMaySignIt, TestAKeyringStartsAnewWhenItsPersonAsks, TestATeamOfMoreThanOneIsSignedAgainByAnotherOwner, TestAHostOpensTheKeyThroughAnAttestedDecrypt, TestAHostWritesOnlyItsOwnIdentityRecords; the VM lane’s TestAWorkspacesIdentityIsTheHostsSignatureOfWhatItsOwnerSigned |
| I18 | What a workspace applies of its owner’s settings, what of it leaves it, and which egress root it trusts are its access’s: settings only as approved by their digest, the journal and the console only with consent, the root only as named by its digest (ADR-0030). | TestOnlySettingsTheOwnerApprovedAreApplied, TestTheJournalIsSentOnlyWithTheOwnersConsent, TestTheEgressRootIsTrustedOnlyByTheOwnersWord, TestAWordNamesOnlyTheEgressRootThisMachinePinned, TestTheConsoleLeavesTheHostOnlyAsTheNewestAccessSays, TestAConsoleLeavesTheHostOnlyWithItsOwnersConsent |
| I20 | No commit holds a credential, in any file. | task lint:secrets |
| I21 | A team’s secret kept as a reference is read only as the team whose secret it is, so a team’s admin cannot name another team’s secret, or the installation’s. | TestASecretIsReadInASessionTaggedWithItsTeam, TestASecretSetAsAReferenceIsReadAsTheTeamBeforeItIsKept, TestASecretKeptAsAReferenceIsReadAsTheTeamThatKeepsIt; spin-terraform-aws’s posture run a_teams_secret_is_read_only_as_the_team |
| I22 | A header the egress proxy sets for a workspace goes on no request its rule’s scope - methods, a path prefix by whole segments - leaves out, and a host that cannot hold a scope is not given one. | TestAnInjectionIsSetOnlyOnTheRequestsItsScopeNames, TestARequestOutsideAnInjectionsScopeIsRefused, TestAScopedInjectionGoesOnlyToAHostThatHoldsIt, TestAnInjectionCrossesTheWireWhole |
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.

