# Rules and their gates

**Spinbox is experimental: APIs change and data may be lost.** What is built and what proves it is on [Status](https://spin-stack.github.io/website/reference/status.md); what Spinbox does not do is on [Limitations](https://spin-stack.github.io/website/overview/limitations.md).

> The rules every entry point of Spinbox must keep, and the test or lint that fails the build when one is broken.

<!-- Generated by tools/refgen from spin:docs/security/README.md. Edit the source, not this file. -->

The rules every entry point must keep, from spin's [`docs/security/README.md`](https://github.com/spin-stack/spin/blob/8eee32b9ddc8d535b3300d0da3927a30b33f22c4/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](https://spin-stack.github.io/website/reference/security/known-gaps.md).

| # | 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 `CHECK`s; `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` |
