Security
Claims
Each row of the claims table quotes one security promise Brig makes and names the tests that defend it.
The quotes and section names come from docs/security.md in the Brig repository. Security on this site covers the same facts.
Automated check #
script/check-claims.sh reads the table on every pull request. It fails in two cases:
- a quoted sentence is gone from the page
- a named test no longer exists
make claims runs the same check after its self-test.
Row format #
A row has three cells: claim, section, and defences.
Claim. The claim cell quotes the page. The check matches only the part in double quotes. A note in parentheses after the quote says which part of the sentence the row covers.
Quote the whole sentence, through its period. A qualifier added to the sentence on the page then ends the match. The match ignores line breaks and runs of spaces.
Section. The section cell names the heading above the sentence. The quote must be under that heading or under one nested in it.
Table shape. Every line after the table's delimiter row is a row, up to the first blank line, with or without its outer pipes. These fail the check:
- a line in the table that is not three cells
- a pipe line outside the table
Defence tokens #
Each defence in the last column is one token.
| Token | What it names | How the check finds it |
|---|---|---|
go:TestName |
a Go test | func TestName( in a _test.go file |
smoke:<text> |
an assertion in script/smoke.sh |
its ok "<text>" line |
vm:<check> |
a check in script/claims-vm.sh |
its vm_check <check> line |
Any other token, a row with none, or text beside the tokens fails the check.
VM checks #
A vm check needs a booted sandbox, and CI has no runtime. CI resolves a vm row by name and lists it as not yet run.
| Command | What it tests |
|---|---|
make claims-vm |
builds brig from this checkout and runs the checks against that binary. It runs where hull or nerdctl is on PATH, and skips where neither is. |
script/claims-vm.sh |
run by hand, tests the brig on PATH unless BRIG names another |
script/claims-vm.sh --self-test |
runs in CI, as described below |
Run make claims-vm before a merge that touches the run path.
The self-test answers every check from a fake guest that leaks one thing at a time. Each check must fail on the leak it tests for. A few checks also run their real probes through a stub brig on the host, so a probe with no answer also fails.
The self-test proves that each check judges an answer correctly. Only a booted sandbox proves that the guest leaks nothing.
Claims table #
| Claim | Section | Defended by |
|---|---|---|
| "Beyond those, the guest does not have your keychain, your SSH agent, your secret manager, or any other directory on the host." (your keychain) | The boundary | go:TestTheRunPathReadsNoKeychain vm:keychain-not-reachable vm:secret-service-not-reachable |
| "Beyond those, the guest does not have your keychain, your SSH agent, your secret manager, or any other directory on the host." (your secret manager) | The boundary | go:TestTheRunPathCannotReachTheImporter go:TestUnresolvedReferencesAreRejectedButOrdinaryURLsAreNot |
| "Beyond those, the guest does not have your keychain, your SSH agent, your secret manager, or any other directory on the host." (your SSH agent) | The boundary | vm:ssh-agent-not-forwarded vm:no-agent-socket |
| "Beyond those, the guest does not have your keychain, your SSH agent, your secret manager, or any other directory on the host." (any other host directory) | The boundary | vm:other-host-directory |
| "Forwarded values go into the runtime process's own environment, and only the variable name appears on its command line." | Not in argv | go:TestSplitEnvKeepsValuesOutOfArgv go:TestRunArgsKeepsSecretValuesOutOfArgv smoke:credential values reach the runtime, but never through argv smoke:argv names the variables only |
| "Nothing is written into the guest home from the host for this." | Credentials | smoke:no credential is written into the workspace |
"So every host-side read and write Brig makes inside the guest home goes through an os.Root opened on it." |
Writing into the workspace | go:TestMarkerWriteRefusesAPlantedSymlink go:TestSetupGitRefusesASymlinkedGitconfig |
"A variable on the profile's deny list is refused, with the reason." |
Credentials | go:TestDenyAppliesToRefdValues go:TestOffSpellingsDoNotForwardADeniedCredential smoke:the metered key is refused, and says why smoke:a denied key never reaches the guest env line of a run smoke:a denied key's value never reaches argv, even under BRIG_ENV_ARGV=1 |
| "Inside Brig, the guest has your guest home mounted as its home, read-write." | The boundary | smoke:the workspace is mounted as the guest home vm:guest-home-read-write |
"Name a project on the run line and that project is a second host directory, also mounted read-write, at /work/<name>." |
The boundary | smoke:the project is mounted at /work/<basename> vm:project-at-work |
| "The guest gets only the credentials you deliver to it." | What the agent can reach | smoke:an undeclared ambient variable and its value reach no runtime argv or env line smoke:an undeclared ambient variable stays out under an override too smoke:the declared credential name reaches the guest |
"--home pointed at a symlink is refused for the same reason, with the same kind of message, and is fixed by naming the real directory." |
Writing into the workspace | go:TestWorkspaceStillRefusesASymlinkAtTheWorkspace go:TestSymlinkedWorkspaceRootIsRefused go:TestWorkspaceRefusesASymlinkedParentComponent |
"The one case with no innocent reading is an image sitting under our registry whose signature does not verify. That is the case that stops." (an image under ghcr.io/brig-sh/) |
Guest images | smoke:a bad signature on our own image is reported smoke:a bad signature stops the boot (exit 5) with no terminal to ask go:TestVerifyRefusesAFailedSignatureWithNoTerminal go:TestVerifyRefusesAFailedSignatureWithStdinOnDevNull |
"A scheme:// value read from the environment is refused as an unresolved secret-manager reference." |
Credentials | smoke:a secret-manager reference is not forwarded go:TestUnresolvedReferencesAreRejectedButOrdinaryURLsAreNot go:TestEnvRefsStillGetTheUnresolvedRefGuard |
| "The object cosign checked is the object that runs, and the success line names the digest rather than the tag it came from." (the object that runs) | The digest, not the tag | go:TestVerifyResolvesVerifiesAndPinsAMatchingDigest go:TestRunArgsBootsThePinnedDigest go:TestNerdctlBootsTheVerifiedDigest smoke:the verified digest is what hull was told to boot |