bastion 0.58.1
This commit is contained in:
+18
-12
@@ -1,6 +1,7 @@
|
||||
# How each claim is tested
|
||||
|
||||
The claims are the points of the [security model](security-model.md).
|
||||
The claims are the points of the [security model](security-model.md), and
|
||||
some further ones that page does not list.
|
||||
|
||||
Each row names a test in `client-go/` that fails if the claim it sits under
|
||||
stops being true. Where a claim, or part of one, has no such test, the row
|
||||
@@ -16,7 +17,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `service_reach_test.go`: `TestReachGrantSyncAndTheForwardWall` | Installing a reach grant is refused without an approval, for another host, or for another machine. `serviceReachAllowed`, the reach check a slot port runs, then admits the granted host to the granted server only; another server, another host or a revoked grant is refused. The check is called directly, not through the slot listener. |
|
||||
| | `claims_unix_test.go`: `TestEachInferenceForwardAdmitsOnlyThePairingThatHoldsInference` | Through each forward's accept loop on an in-memory tunnel: a pairing without `inference` never reaches the upstream, and granting the family lets the next connection through, with no restart. |
|
||||
| | `claims_unix_test.go`: `TestTheServiceSlotAdmitsOnlyWithTheFamilyAndAReachGrant` | Through the slot listener: a connection is refused with a reach grant but no `services` family, and with the family but no reach grant for that server; with both it reaches the server's socket. |
|
||||
| | `claims_static_test.go`: `TestOnlyTheGatedForwardsListenInsideTheTunnel` | Every listener on the tunnel's network stack is one of the four gated forwards, each at one call site; only they are handed the stack, and it is otherwise only dialled out of. |
|
||||
| | `claims_static_test.go`: `TestOnlyTheGatedForwardsListenInsideTheTunnel` | Every listener on the tunnel's network stack is one of the five gated forwards (three inference forwards, the service slot and coder runs), each at one call site; only they are handed the stack, and it is otherwise only dialled out of. |
|
||||
| | not covered by a test | The reach check's own comparison of a stored grant's machine (`service_reach.go`, `serviceReachAllowed`). |
|
||||
| Scoped tunnel | `claims_tunnel_test.go`: `TestTheTunnelReachesOnlyThePairedHost` | A WireGuard device configured the way a pairing configures it holds one peer, whose only allowed address is the host's /128, before and after an endpoint rotation. |
|
||||
| | `claims_tunnel_test.go`: `TestATunnelAddressThatIsNotOneHostIsRefused` | A tunnel address that is a prefix, empty, zoned, unspecified, multicast or not an address is refused at tunnel start and at a rotation, and the device is left with no peer. |
|
||||
@@ -55,7 +56,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| No MCP endpoint | `claims_static_test.go`: `TestNoRouteOpOrDependencySpeaksMCP` | Every HTTP route on every listener is in a fixed table and none names MCP; no management op, import or module names MCP; no string in the code is a URL or route with an `/mcp` path, or names JSON-RPC. The agent clients' tool-server config files are named only in `mcpconfig.go`, which reads them, and nothing in the config reader imports or can reach a dialler, an HTTP client, a websocket or a process start. |
|
||||
| | `claims_static_test.go`: `TestMCPReaderCheckCatchesADialAndAStrayBasename` | The check above reports a config reader that starts a process, imports `net/http` or reaches a dialler through another function, a Windows reader that starts a process, opens a socket, loads a DLL by name or opens a remote registry, and a second file that names a client's config file, the servers key, an `/mcp` URL or an `/mcp` route. |
|
||||
| Grants are verified here | `anchor_test.go`: `TestPairingGrantSyncWalls` | A pairing grant signed by another key, for another machine, naming another anchor, or expired is refused, and the pairing stays pending. |
|
||||
| | `fuzz_grants_test.go`: `FuzzAChangedSignedByteNeverVerifies` | For every kind of signed record, changing any byte of the record or its signature makes it fail to verify. |
|
||||
| | `fuzz_grants_test.go`: `FuzzAChangedSignedByteNeverVerifies` | For each of nine kinds of signed record (pairing grant, pairing revoke, anchor consent, management grant, store grant, move plan, reach grant, op run and op grant), changing any byte of the record or its signature makes it fail to verify. |
|
||||
| | `session_test.go`: `TestSessionPinsTheHostKeyWithoutAStoreGrant` | The first key offered is pinned; a different key later is refused and the pin is kept. |
|
||||
| | `anchor_test.go`: `TestAnchorVerifyKeyPinsFromDCNAndRefusesRepin` | The anchor's verify key cannot be re-pinned to a different key. |
|
||||
| | `session_test.go`: `TestSessionRefusedWithoutGrantIs126` | An op the grant both allows and denies, or a grant that denies `*`, is refused. |
|
||||
@@ -72,7 +73,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `fuzz_observe_test.go`: `FuzzUnitNameIsSystemdsAlphabet` | Whatever the input, a unit name that passes is in systemd's unit-name alphabet, at most 255 characters, ends in a unit type, does not start with `-`, `.` or `@`, and is the input itself or the input with `.service`. |
|
||||
| | `claims_unix_test.go`: `TestTheAllowlistedOpIgnoresTheCallersArguments` | `uptime`, sent through the management lane with hostile arguments, runs its program with none. |
|
||||
| | `claims_static_test.go`: `TestTheAllowlistedOpRunsItsTableArgvOnly` | The allowlisted table is exactly `uptime` with no arguments, and the one command the lane starts is built from the table's argv alone. |
|
||||
| | `claims_static_test.go`: `TestFileHandlersStartNoShell` | Nothing a context, store or backup handler can reach runs a shell; the one process any of them can start is the local search agent's Python, which `dcn_grant_sync` brings up. |
|
||||
| | `claims_static_test.go`: `TestFileHandlersStartNoShell` | Leaving text extraction (`ctx_extract`) aside, nothing a context, store or backup handler can reach runs a shell, and the one process any of them can start is the local search agent's Python, which `dcn_grant_sync` brings up. |
|
||||
| Approval on the machine, once enrolled | `stepup_test.go`: `TestStepupVerifyBurnAndApprovals` | With no factor enrolled the gate passes; once enrolled, a code is used once, and an approval passes only for the exact terms it was issued for, and only once. |
|
||||
| | `modelswap_test.go`: `TestAnApprovalIsSingleUse` | A spent approval does not fund a second model download. |
|
||||
| | `modelswap_test.go`: `TestUnenrolledMachineCannotHaveAModelPulled` | With no factor enrolled, a model pull is refused. |
|
||||
@@ -166,7 +167,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `service_reach_test.go`: `TestReachGrantSyncAndTheForwardWall` | Installing a reach grant is refused without an approval, for another host, or for another machine. The reach check then admits the granted host to the granted server only, and a revoked grant admits nothing. Called directly, not through the slot listener. |
|
||||
| | `claims_unix_test.go`: `TestTheServiceSlotAdmitsOnlyWithTheFamilyAndAReachGrant` | Through the slot listener: each connection is checked, and one without a reach grant for that server, or without the `services` family, never reaches the server's socket. |
|
||||
| | not covered by a test | The reach check's own comparison of a stored grant's machine. |
|
||||
| Egress for someone else reaches the public internet, never this machine | `coders_linux_test.go`: `TestCoderEgressRefusesEveryInternalClass` | A coder run's proxy refuses every internal class (loopback, RFC 1918, CGNAT, link-local and metadata, ULA, IPv4-mapped, IPv4-compatible, IPv4-translated, NAT64, 6to4, unspecified, 0/8, 198.18/15, 240/4, multicast, fec0::/10), by address and by a name with one such answer among public ones, and `localhost` names and names that do not resolve. |
|
||||
| Egress for someone else reaches the public internet, and nothing on this machine or your network that you did not name | `coders_linux_test.go`: `TestCoderEgressRefusesEveryInternalClass` | A coder run's proxy refuses every internal class (loopback, RFC 1918, CGNAT, link-local and metadata, ULA, IPv4-mapped, IPv4-compatible, IPv4-translated, NAT64, 6to4, unspecified, 0/8, 198.18/15, 240/4, multicast, fec0::/10), by address and by a name with one such answer among public ones, and `localhost` names and names that do not resolve. |
|
||||
| | `coders_linux_test.go`: `TestCoderEgressRefusesThisMachinesOwnPublicAddress` | A public address on this machine's interface is refused in every spelling (IPv4-mapped, NAT64, 6to4) and through a name; a neighbour on the same subnet passes; `allow_lan` is the one exception. |
|
||||
| | `coders_linux_test.go`: `TestCoderEgressLocalAddressesAreReread` | An address the machine gains after start is refused; interfaces that cannot be read refuse. |
|
||||
| | `coders_linux_test.go`: `TestCoderEgressDialsTheVettedAddressAndHonoursAllowLAN` | The proxy returns the vetted addresses, IPv4 first, never the name; a LAN host passes only when named in `allow_lan`, and an entry with a port only on that port. |
|
||||
@@ -181,9 +182,9 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `egress_network_test.go`: `TestLocalEgressRefusesTheLANAndTheMesh` | On a local machine the egress lane refuses the network around it as an anchored one does: private, ULA, CGNAT and benchmarking addresses, a NAT64 address for one, and a name with any one such answer, each answered 403 through the lane with nothing dialled; a public name passes at its vetted address. |
|
||||
| | `ownaddr_inside_linux_test.go`: `TestNetworksInsideThisMachineAreThisMachine` | In a private network namespace with two bridges whose only port is a veth (one with an IPv4 and an IPv6 range, one with a public range), a bridge whose port is a dummy, a bridge with no port, a veth routed to directly, a range routed through a veth with this machine's own address on it as the gateway, a veth whose only default route is in another table, a bridge over a VLAN, a route over a dummy link, the network on three veths that carry this machine's main-table default routes (an IPv4 multipath default over two, an IPv6 default over the third: this machine as a container), addresses through those defaults' gateways and a route through a gateway on the dummy-port bridge (a LAN behind a router VM): the real route lookup reads the first seven as this machine and the rest as leaving; a local machine's egress lane refuses the inside addresses, by address and by name, as this machine, refuses the private addresses beyond them as its network, and dials the public ones; a coder run's proxy refuses the public range on an inside network and dials a public address through the gateway; the backup lanes allow the IPv4 and IPv6 addresses through the gateway. |
|
||||
| | `ownaddr_inside_linux_test.go`: `TestInsideMachineReadsTheLinkAndABridgesPorts` | A veth, and a bridge whose every port is a veth, a tap or a dummy (or that has none), keep a packet on this machine; a bridge with a network card, a VLAN or a bridge among its ports, a network card, loopback, a VPN, a tunnel and a macvlan do not. |
|
||||
| | `egress_local_test.go`: `TestLocalEgressLaneNeverDialsThisMachine` | Through the lane on a local machine with a real dialer: a request for a loopback listener gets 403 over CONNECT and plain HTTP and never reaches it; a LAN name is dialled at its vetted address; a dual-stack name falls back to its second address. |
|
||||
| | `egress_local_test.go`: `TestLocalEgressLaneNeverDialsThisMachine` | Through the lane on a local machine with a real dialer: a request for a loopback listener gets 403 over CONNECT and plain HTTP and never reaches it; a public name is dialled at its vetted address; a dual-stack name falls back to its second address. |
|
||||
| | `egress_dial_test.go`: `TestDialEachDoesNotWaitOutAnAddressThatGoesNowhere` | The dial both egress lanes share tries only the vetted addresses, in order; an address that never answers holds up the next for well under its share of the budget, and is cancelled once the next answers. |
|
||||
| | `egress_relay_test.go`: `TestARefusingHostCannotSpinTheEgressWorkers` | A Host that answers every parked egress worker with a job this machine refuses (a request for `localhost`, a line that is no request) gets at most four workers a second back, not a dial loop, and nothing is dialled out. |
|
||||
| | `egress_relay_test.go`: `TestARefusingHostCannotSpinTheEgressWorkers` | A Host that answers every parked egress worker with a job this machine refuses (a request for `localhost`, a line that is no request) gets at most 16 workers back in two seconds, not a dial loop, and nothing is dialled out. |
|
||||
| | `egress_relay_test.go`: `TestEgressWorkersParkAgainAtOnceAfterAJobTheyTook` | A worker that took a job parks again at once: 40 accepted jobs through the four workers are each answered 200 well inside the 10 s one wait per job would take. |
|
||||
| | `egress_local_test.go`: `TestLocalModelsSwitchLeavesNoEgressRouteToOllama` | On a local machine with `models` switched off for the host, the ollama forward filters, and the egress lane refuses `/api/pull` to ollama on loopback, `localhost` and this machine's LAN address without a dial. |
|
||||
| | `egress_local_test.go`: `TestAnchoredEgressStillRefusesTheLAN` | An anchored machine's egress lane still refuses the LAN addresses a local machine reaches, with its own wording, and passes the public internet. |
|
||||
@@ -203,7 +204,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| A pairing code pins the Host's CA by its fingerprint | `pair_code_linux_test.go`: `TestAPairingCodePinsTheHostCAByFingerprint` | A pairing code whose enroll port presents no CA, another CA, a CA under another fingerprint, a leaf another CA signed, or a leaf without the name `host.cseries.internal` is refused before the token is spent, and nothing is saved. |
|
||||
| | `pair_code_linux_test.go`: `TestAPairingCodePairsAsTheHostCAFileDoes` | A good code makes the same TCP pairing `--redeem-url` and `--host-ca` make, with that CA pinned; the same code again is "already paired" (exit 3) and changes nothing; a Host of the same name under another CA is a plain refusal. |
|
||||
| | `pair_code_linux_test.go`: `TestAPairingCodeIsReadStrictly` | A code that is garbled, another version, or has an extra, missing or malformed field is refused, and no refusal repeats the token. |
|
||||
| | `pair_code_linux_test.go`: `TestACodeTheHostMadeDecodesAndPins` | A code made by the C-Series Host's own `pairing_code` decodes to its address and token, and its fingerprint is the one `caSHA256` computes from the same CA. |
|
||||
| | `pair_code_linux_test.go`: `TestACodeTheHostMadeDecodesAndPins` | A recorded code from the C-Series Host's `pairing_code` decodes to its address and token, and its fingerprint is the one `caSHA256` computes from the same CA. |
|
||||
| | `pair_code_linux_test.go`: `TestAPairingCodeWithAKeyGivenTwiceIsRefused` | A code that names a field twice is refused. |
|
||||
| | `pair_code_linux_test.go`: `TestThePinnedChainTakesOnlyAValidCAWithTheFingerprint` | A certificate with the fingerprint that is not a CA, an expired CA, or a chain of the leaf alone is refused; the pinned CA after other certificates is the one kept. Called directly, not through TLS. |
|
||||
| | `pair_code_linux_test.go`: `TestHostCAFileWithASameNamedHostUnderAnotherCAIsNotAlreadyPaired` | With `--host-ca` too, a Host of the same name under another CA is a plain refusal, not exit 3, and its token is not spent; the same Host under its own CA is exit 3. |
|
||||
@@ -236,10 +237,10 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `rootexec_test.go`: `TestRootExecStopsWhenAWallFallsMidRun` | Through the handler: a command already running is killed within one recheck when its grant is revoked or expires, `session` is switched off here, or the pairing is quarantined, and answered with 126 and a J that says why; while every wall holds, the recheck leaves it alone. |
|
||||
| | `rootexec_test.go`: `TestRootExecDeadlineKillsAChildThatIgnoresSIGTERM` | Through the handler with the real `/bin/sh`: at the deadline a child that ignores SIGTERM, holding the output open or not, is gone when the answer arrives, within the deadline and the kill grace. Every signal goes through a fence that admits only the process group the test started. |
|
||||
| | `rootexec_test.go`: `TestRootExecBadCommandAndTranscriptFailureAreAudited` | A bad command, a bad session id, and a transcript that cannot be opened each leave an audit row with the command (or its size and sha256) and exit 1; none starts. |
|
||||
| | `rootexec_watch_linux_test.go`: `TestRootExecWatchKernel` | In a private network namespace, through the handler with the real `/bin/sh`, nftables and a cgroup: a command's connections to outside addresses (IPv4 and IPv6, TCP and UDP) are reported in `untrusted_connections`; loopback, the machine's own address and listed platform ranges are not, though they stay in the audit row marked own; a grandchild's connections are caught; what a command reached before its deadline killed it is reported; no table or cgroup is left after any of them, and one left by a dead bastion is removed by the next sweep, with any process in it moved to its parent. |
|
||||
| | `rootexec_watch_linux_test.go`: `TestRootExecWatchKernel` | In a private network namespace, through the handler with the real `/bin/sh`, nftables and a cgroup: a command's connections to outside addresses (IPv4 and IPv6, TCP and UDP) are reported in `untrusted_connections`; loopback (except a port listed as a relay), the machine's own address and listed platform ranges are not, though they stay in the audit row marked own; a grandchild's connections are caught; what a command reached before its deadline killed it is reported; no table or cgroup is left after any of them, and one left by a dead bastion is removed by the next sweep, with any process in it moved to its parent. |
|
||||
| | `rootexec_watch_test.go`: `TestRootExecJCarriesOnlyOutsideConnections` | The J frame lists only the destinations outside the platform list (addresses, CIDRs and resolved names), the stop row lists every one and counts the outside ones, the transcript records them; with only loopback and own hosts the field is absent. |
|
||||
| | `rootexec_watch_test.go`: `TestRootExecUnwatchedCountsAsOutside` | A command whose connections could not be watched is reported with one untrusted entry that says why. |
|
||||
| | `rootexec_watch_test.go`: `TestRootExecOwnHostsList` | No list means only loopback is own; a broken list or a bad entry reads as empty; a name that does not resolve adds nothing; the host's list is capped and the rest counted. |
|
||||
| | `rootexec_watch_test.go`: `TestRootExecOwnHostsList` | No list means only loopback is own; a broken list or a bad entry reads as empty; a name that does not resolve adds nothing; the outside connections reported are capped and the rest counted. |
|
||||
| | `claims_unix_test.go`: `TestAMemberWithoutTheSessionFamilyGetsNoTerminal` | Through the terminal handler, on an anchored machine: a host other than the anchor without the `session` family is refused and leaves no transcript; once the family is granted the same approval opens the terminal. |
|
||||
| | `claims_unix_test.go`: `TestAnAnchoredTerminalApprovalOpensOneSessionOnce` | Through the handler: an anchored machine's terminal approval is refused for another session, opens its own, and is refused when used again. |
|
||||
| | `claims_unix_test.go`: `TestARootTerminalNeedsTheBastionToRunAsRoot` | Through the handler: with a factor enrolled and an approval for that session, a bastion not running as root refuses a root terminal, and the same approval opens it once the bastion runs as root. |
|
||||
@@ -253,7 +254,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| On macOS and Windows, a networked bastion is the system's service | `sysservice_test.go`: `TestLaunchdPlistRunsTheDaemonAsRootAtBootAndKeepsItAlive` | The launchd job is well-formed and runs the program as root at boot, with root's home, and launchd restarts it whenever it exits. |
|
||||
| | `sysservice_test.go`: `TestBinaryChainMustBeRootOnly` | On macOS the service is refused for a program a non-root user owns, that a group or everyone can write, that is a symlink, or that sits under such a directory. Run against a stand-in for the disk, not on a Mac. |
|
||||
| | `sysservice_test.go`: `TestWindowsServiceBinaryMustLiveUnderProgramFiles` | On Windows the service is refused for a program outside Program Files, including the per-user install folder. |
|
||||
| | `sysservice_test.go`: `TestServiceGuardRefusesAUserspaceBastionBesideTheService` | With the service installed, `pair`, `run` and `status` from an account without machine authority are refused; an administrator, and a machine with no service, are not. |
|
||||
| | `sysservice_test.go`: `TestServiceGuardRefusesAUserspaceBastionBesideTheService` | With the service installed, `pair`, `run`, `status` and `switch` from an account without machine authority are refused; an administrator, and a machine with no service, are not. |
|
||||
| | `sysservice_test.go`: `TestServiceModeOpensNothingOnTheDesktop` | As a Windows service, the tray, the blocking dialog and the detached relaunch after an update each return before doing anything. Read from the source, not run. |
|
||||
| | `runlock_unix_test.go`: `TestARunBesideTheMacServiceIsRefusedBeforeItStarts` | With the macOS daemon installed, a second `run` is refused while the first holds the lock in the state folder, and may run once it is gone; with no daemon nothing is locked. Run against the real lock on Linux; the macOS wiring (`runguard_darwin.go`) is not run off a Mac. |
|
||||
| | not covered by a test | That launchd and the Windows service manager actually restart the bastion after it exits (`sysservice_darwin.go`, `sysservice_windows.go`); neither runs off its own platform. |
|
||||
@@ -299,7 +300,12 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `backuproot_test.go`: `TestBackupRootsAreEachHostsOwn` | A backup op is refused while the host's backup folder is the same as, inside, or contains another paired host's, also through a link. |
|
||||
| | `backuproot_test.go`: `TestBackupRootNeverOverlapsTheBastionsOwnState` | A backup folder that is or contains the bastion's configuration folder, keys or backup records, or sits in the configuration folder outside `backups/`, is refused, and none of them can be shared; the default folder is accepted. |
|
||||
| No delete op on shared stores | `dcn_plan_test.go`: `TestDcnPlanCheckApplyVerify` | A move plan that escapes the store, targets an existing path, or touches an excluded file is refused whole; the op list carries the plan op and no `dcn_delete` or `dcn_remove`. |
|
||||
| | `fuzz_paths_unix_test.go`: `FuzzMovePlanStaysInsideTheRoot` | Any move the plan check passes stays inside the store, targets a path that does not exist yet, and avoids excluded paths. |
|
||||
| | `fuzz_paths_unix_test.go`: `FuzzMovePlanStaysInsideTheRoot` | Any move the plan check passes stays inside the store, targets a path that does not exist yet, and avoids excluded paths and the search agent's folder. |
|
||||
| A shared folder serves only what its grant leaves in | `dcn_private_test.go`: `TestDcnTheDefaultGcloudExcludeHoldsOnEveryOp` | Through list, stat and read_range on a store granted with the Host's default excludes: nothing in `.config/gcloud`, at the top of the store or inside `home/`, is listed, stated or read, as written, with whitespace, through a `..` or through a link; a move out of or into it is refused; `.config/gcloudx` and the rest of `.config` still read. |
|
||||
| | `dcn_private_test.go`: `TestDcnAMultiFolderExcludeMatchesThatRunAnywhere` | A pattern with a `/` matches that run of folders at any depth, with names compared as Linux, macOS and Windows compare them (case, and Windows' trailing dots and spaces); a near miss (`.config/gcloudx`, `.configx/gcloud`, the two folders apart or in the other order) is not excluded; slashes at either end or doubled name the same run; one-folder patterns match as before. Judged per platform from any OS. |
|
||||
| | `fuzz_paths_unix_test.go`: `FuzzAnExcludedPathIsExcludedHoweverItIsSpelled` | Any path the sandbox accepts and the excludes pass opens neither an excluded name nor the search agent's folder, by its spelling or through a link inside the store. |
|
||||
| | `dcn_private_test.go`: `TestDcnTheAgentsFolderIsNeverServed` | With a grant that excludes nothing: no path in `.agent` at the top of the store, or in one inside a folder of it, is listed, stated or read, as written, with whitespace, through a `..`, or through a link to the folder or to a file in it; a move out of or into one is refused; the bastion's own report of a plan it applied, through the real plan op, is still written to `.agent/plans`. |
|
||||
| | `dcn_private_test.go`: `TestDcnTheAgentsFolderIsMatchedHoweverTheNameIsCased` | `.agent` is refused in any case on every OS, and on Windows with trailing dots, spaces or a stream name; `.agentx`, `agent` and `.agent-old` are not. Judged per platform from any OS. |
|
||||
| Verified releases | `release_sign_test.go`: `TestATamperedManifestIsRefused` | A manifest changed after signing is refused. |
|
||||
| | `release_sign_test.go`: `TestAManifestSignedByAnotherKeyIsRefused` | A manifest signed by a key the build does not pin is refused. |
|
||||
| | `release_sign_test.go`: `TestAnUnsignedManifestIsNotRead` | A manifest with no signature reaches neither the updater nor the local page. |
|
||||
|
||||
Reference in New Issue
Block a user