bastion 0.58.0
This commit is contained in:
@@ -62,6 +62,14 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `localdeny_test.go`: `TestTheLocalSwitchCanOnlyEverRemoveReach` | Switching a capability off on this machine never turns a refusal into an allow. |
|
||||
| Fixed argv | `observe_ops_test.go`: `TestObserveArgsNeverReachArgv` | Shell metacharacters and flags sent as arguments to every observe op are refused or never reach a command line, and each op runs only its pinned argv. Linux only. |
|
||||
| | `contextextract_test.go`: `TestCtxExtractRefusesAnUnknownTool` | Text extraction runs only tool names from its fixed table. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveJournalRunsOnlyItsPinnedArgv` | `observe:journal` runs `systemctl list-units`, `systemctl list-unit-files` and one pinned `journalctl`; the unit reaches `journalctl` only as `--unit=` with a name one of those lists holds, and a unit this machine does not have, or knows only as not found, is refused before `journalctl` runs. Linux only. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveLoginsRunsItsPinnedArgv` | `observe:logins` runs one pinned `journalctl` whose only caller value is a whole number of hours; without access to the system journal it runs nothing and its counts read -1, never 0. Linux only. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveLoginsCountsOnlyRootsSSHDLines` | Every match `observe:logins` gives `journalctl` also needs `_UID=0`, so a line another account wrote under a process named sshd, or from a shell left in ssh.service, is never counted. Linux only. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveFoldersReadsOnlyTheFolderItStatd` | A folder swapped for a link between the walk's stat and its open, or reached through a folder above it that was, is not read and is listed as changed. Linux only. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveFoldersStartsNoWalkPastTheStuckOnes` | Past four walks still inside a read that has not returned, `observe:folders` answers at once and starts no walk; a walk that comes back frees its place. Linux only. |
|
||||
| | `observe_reads_test.go`: `TestObserveReadsRefuseBadValuesWithTheRule` | A relative folder path, one with `..` or under `/proc`, a unit name outside systemd's alphabet, shaped like a flag or naming a template, and a depth, window or port out of range are refused at exit 2 with the rule broken, without repeating the value. |
|
||||
| | `fuzz_observe_test.go`: `FuzzFolderPathIsAbsoluteCleanAndOffTheKernelTrees` | Whatever the input, a folder path that passes is absolute and clean, holds no `..` and no control character, and is outside `/proc`, `/sys` and `/dev`. |
|
||||
| | `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. |
|
||||
@@ -101,6 +109,24 @@ says so. Those parts are enforced only by the code as written.
|
||||
| | `ops_action_linux_test.go`: `TestActionApplyOfAnotherChangeListRunsNothing` | An apply whose change list differs from the approved one is refused before the program starts. Skipped where action ops cannot run. |
|
||||
| | `ops_action_test.go`: `TestActionManifestRefusesWhatIsNotDeclaredOrNotBounded` | An unknown verb or argument, a workload, container, unit or tree named by a parameter, an undeclared credential, verbs without root, a destination on loopback, link-local or a metadata service (also inside NAT64 or IPv4-mapped IPv6), the bastion's own unit, a tree on a credential store, a tree or container moved twice by one op, and more, are each refused when the op arrives. |
|
||||
| | `ops_action_test.go`: `TestSealedCredentialsOpenOnlyForThisRunAndThisOp` | A credential sealed for another run, to another machine, tampered, expired, undeclared, missing or sent twice is refused. |
|
||||
| A standing grant holds to its bounds on the machine | `ops_skill_bounds_test.go`: `TestGrantOutsideItsWindowIsNotNowAndSpendsNothing` | An apply outside the grant's window (days, hours, zone, read on the machine's clock) does not run, spends no use and is no strike, and says when the window opens; a dry run is not held; arguments outside the op are still refused. Inside the window it runs and spends one use. |
|
||||
| | `ops_skill_bounds_test.go`: `TestGrantInsideItsMinIntervalDoesNotRun` | An apply inside the approved interval since the last one does not run, spends nothing and is no strike; past it, it runs. |
|
||||
| | `ops_skill_bounds_test.go`: `TestGrantPerDayHoldsAcrossARestart` | The runs are logged in the grant's record on disk, and another process reading that record holds the third run in 24 hours as the first did. |
|
||||
| | `ops_skill_bounds_test.go`: `TestGrantWithATamperedRunLogIsNoGrant` | A record whose run log, uses, approver or seal was edited outside the bastion, or that carries no seal, holds no grant. |
|
||||
| | `ops_skill_bounds_test.go`: `TestGrantDiesWithTheKeyThatApprovedIt` | A grant keeps the key that approved it; with that device key unpinned, or the console factor enrolled again, it is refused, and unpinning drops it so pinning the key again does not bring it back. |
|
||||
| | `ops_skill_bounds_test.go`: `TestGrantDropGoesThroughAQuarantine` | `op_grant_drop`, of one grant or all, goes through while the host's writes are quarantined, with no approval. |
|
||||
| | `fuzz_grants_test.go`: `FuzzAGrantsRunBoundsHoldWhateverTheHostSends` | A window or rate the machine accepts is one it can hold, and no run inside an interval or past a day's count is let through. |
|
||||
| An op with no program needs no sandbox, and a program still does | `ops_skill_bounds_test.go`: `TestProgramOpStillRefusedWithoutTheSandbox` | On a machine with no overlay, a files op and an action op with a program are refused before anything runs. |
|
||||
| | `ops_skill_bounds_test.go`: `TestVerbsOnlyOpShippingRunIsRefused` | An op with `interpreter: none` that ships a `run` file is refused at sync; without one it is held. |
|
||||
| | `ops_verbs_only_linux_test.go`: `TestVerbsOnlyOpRunsWithNoOverlay` | On a machine with no overlay, an op with no program dry-runs as its verbs' own read, applies its verbs once under an approval bound to that list, and is refused, as the machine's own state and no strike, where a verb's tool is missing. |
|
||||
| On your own machine, an action op reaches the internet, never your network | `ops_action_local_test.go`: `TestALocalBoxRefusesAnActionOpThatDeclaresAPrivateNetwork` | Through `op_sync` on a local, unanchored machine: an op declaring `10.0.0.2:80`, a `192.168.0.0/16`, `172.16.0.0/12`, CGNAT, ULA or benchmarking address, or `10.0.0.2` inside NAT64 or 6to4, is refused with the reason and what to do instead, and nothing is held; a public address and a name are held. |
|
||||
| | `ops_action_local_linux_test.go`: `TestALocalBoxRefusesANameThatResolvesToAPrivateNetworkAtApply` | On a local machine a name that resolves to a private address, alone or beside a public one, is refused when an apply resolves its destinations; a public name resolves to its address; on an anchored machine the private name resolves. |
|
||||
| | `ops_action_local_linux_test.go`: `TestAPrivateDestinationHeldWhileAnchoredIsRefusedAtApplyOnceTheAnchorIsGone` | An op whose destination is `10.0.0.2:80` or its IPv4-mapped form resolves at apply on an anchored machine; once the machine is local, the same destinations are refused at apply. |
|
||||
| | `ops_action_local_linux_test.go`: `TestALocalBoxNeverFetchesAVerbsRepositoryFromAPrivateNetwork` | On a local machine a verb's fetch (the ancestry check `k8s.set_image` makes) of a repository whose host resolves to a private address, to a public and an IPv4-mapped private one, or to a link-local one is refused with the reason before git runs; a public host is fetched with git pinned to the checked address (IPv6 bracketed, the URL's port kept), a git that would drop the pin is refused, and an anchored machine fetches from its private forge unpinned. |
|
||||
| | `ops_action_local_linux_test.go`: `TestAnAnchoredBoxNeverFetchesAVerbsRepositoryFromItselfOrTheMetadataService` | On an anchored machine a verb's fetch (the ancestry check `k8s.set_image` makes) of a repository whose host resolves to loopback, to a public address beside an IPv4-mapped loopback one, to a link-local address, to a metadata service or to this machine's own address is refused with the reason before git runs; a private forge is fetched pinned to its address. |
|
||||
| | `ops_action_local_linux_test.go`: `TestALocalBoxNeverPutsLiveAnImageFromARegistryOnItsNetwork` | On a local machine `k8s.set_image`'s plan and apply refuse an image whose registry is a name resolving to a private address, a private address, `localhost` or a link-local address, with the reason, and set nothing; a public registry and a Docker Hub repository pass the registry check; an anchored machine keeps its private registry. |
|
||||
| | `ops_verbs_only_linux_test.go`: `TestSetImageNeedsGitForItsAncestryCheck` | `k8s.set_image` names kubectl and git as the tools it runs, and `tree.checkout` git; as root, a machine with kubectl and no git is told by the verb check and the beat that it cannot run `k8s.set_image`. |
|
||||
| | `ops_action_local_test.go`: `TestAnAnchoredFleetKeepsItsPrivateDestinations` | Through `op_sync` on an anchored machine: ops declaring a private, CGNAT or ULA address are held; loopback is still refused. |
|
||||
| Root verbs move only forward | `ops_verbs_linux_test.go`: `TestSetImageTouchesOnlyTheNamedContainerAndOnlyForward` | Against a stand-in kubectl: a digest with no receipt, a receipt from another repository, a container the workload lacks, a live image this machine never rolled (a receipt for it included), another image and a backward commit are each refused with nothing set; a forward roll sets exactly the named container and records it; an image already set still waits for its rollout, and fails when it does not finish. The ancestry answer is a stand-in here. |
|
||||
| | `ops_verbs_linux_test.go`: `TestAncestryByFetchRefusesABackwardOrSidewaysCommit` | With real git, the ancestry check `k8s.set_image` makes accepts a forward commit or the same one, and refuses a backward, sideways or unknown commit. |
|
||||
| | `ops_verbs_linux_test.go`: `TestTreeCheckoutMovesOnlyForward` | With real git: a tree moves forward to the exact commit, runs none of its own hooks, leaves untracked files and nested repositories alone, and refuses a backward or sideways commit, another origin, local changes and a config that could run a program. |
|
||||
@@ -237,6 +263,7 @@ says so. Those parts are enforced only by the code as written.
|
||||
| Observation changes nothing on the machine | `observe_endpoints_test.go`: `TestObserveEndpointAllowedIsTheInverseOfObserveNet` | The monitoring ops may reach only private addresses, exactly the ones `observe:net` refuses. |
|
||||
| | `observe_endpoints_test.go`: `TestObserveDialControlRefusesPublicAtConnectTime` | A public address is refused at the socket, so a DNS answer cannot move a query outside. |
|
||||
| | `observe_endpoints_test.go`: `TestObserveNeverFollowsARedirect` | A redirect from a monitoring endpoint is reported as a failure, and its target is never fetched. |
|
||||
| | `observe_reads_linux_test.go`: `TestObserveCertReadsOnlyThisMachinesListener` | `observe:cert` is refused for a port no listener on this machine holds, and never dials an address a listener is bound to that this machine does not hold. Linux only. |
|
||||
| | `claims_static_test.go`: `TestObserveOpsWriteNothing` | Nothing an observe or computer op can reach writes, removes, renames, changes permissions on, mounts or signals anything, apart from the bastion's own audit line and its journal of observe results; the programs those ops run are a fixed list of reads. |
|
||||
| | `claims_static_test.go`: `TestNoInputInjection` | No input-injection program is run, no Windows input API is loaded, and no script the computer ops run asks for a keystroke, a click or an accessibility action. |
|
||||
| A backup's key goes to a host only when you approve it here | `backupreseal_test.go`: `TestBackupResealWaitsForThePersonThenDeliversToTheHostOnly` | Through the management lane: a request is filed pending and hands out nothing; another pairing cannot read it; after approval here the host receives the record sealed to its key only; a refusal hands over nothing; a decided request cannot be decided again. |
|
||||
|
||||
+43
-2
@@ -61,10 +61,51 @@ Read-only, and reported so the host can show **why** rather than a verdict.
|
||||
the host. Alert counts also ride the heartbeat, refreshed on their own
|
||||
interval, and are absent — not zero — whenever no Alertmanager is
|
||||
configured.
|
||||
- **Machine questions** (Linux: `observe:folders`, `observe:journal`,
|
||||
`observe:logins`, `observe:cert`). What a host would otherwise ask the
|
||||
person to run a command for, answered here. Each value is checked against
|
||||
its written rule before anything is read or run, and a refusal says which
|
||||
rule it broke.
|
||||
- `observe:folders <absolute path> [depth 1-3, default 2]`: the 25 largest
|
||||
folders under the path, down to the depth, and the total, on the path's
|
||||
own filesystem only, counted as `du -x` counts (disk blocks, a file with
|
||||
several links once). Only folders are named, never files. What could not
|
||||
be read, and filesystems mounted under the path, are listed against the
|
||||
folder that holds them, with a reason and a count. A relative path, one
|
||||
containing `..`, and `/proc`, `/sys` and `/dev` (or a link leading into
|
||||
them) are refused. The walk shares the 20-second op budget; one that runs
|
||||
out answers with what it measured, marked incomplete. It follows no link,
|
||||
and a folder swapped for one while it reads is listed as changed. While
|
||||
four earlier walks are still inside a read that has not returned, no new
|
||||
walk starts.
|
||||
- `observe:journal <unit> [minutes 1-60, default 15]`: one systemd unit's
|
||||
journal over the window, up to 5000 lines and 200 KB, newest kept. A name
|
||||
with no type is a service. The unit must be in systemd's unit-name
|
||||
alphabet and one this machine has, loaded or installed; otherwise it is
|
||||
refused, with the units whose names come close.
|
||||
- `observe:logins [hours 1-168, default 24]`: sshd's failed logins counted
|
||||
and ranked by source address and by user name (20 each), and the last 20
|
||||
accepted logins with user, source, method and time. A user name the other
|
||||
side sent is cut to 32 characters, and anything outside the characters
|
||||
account names use reads `?`. No raw line, key fingerprint or password is
|
||||
returned. Only lines sshd wrote as root are read (`_UID=0`), so another
|
||||
account cannot add login lines or bury the real ones.
|
||||
- `observe:cert <port> [server name]`: the certificate this machine's own
|
||||
TCP listener on the port serves: subject, names, issuer, dates, days left,
|
||||
and whether the chain verifies against this machine's roots for that name.
|
||||
The port must be one `ss` shows listening here.
|
||||
|
||||
`observe:journal` and `observe:logins` first check that the bastion can read
|
||||
the system journal (root, as the networked install runs it, or a member of
|
||||
`systemd-journal`). An account that cannot is shown only its own lines by
|
||||
`journalctl`, which would read as a quiet machine, so they answer with the
|
||||
reason instead, and `observe:logins` reports its counts as -1.
|
||||
|
||||
The observe ops that send traffic are `observe:net`, `observe:k8s` (a
|
||||
read-only query of a cluster API) and the three monitoring ops above (private
|
||||
read-only query of a cluster API), the three monitoring ops above (private
|
||||
addresses only, refused at the socket if a redirect or a DNS answer would move
|
||||
the destination after validation). A build with no enumerator for its
|
||||
the destination after validation) and `observe:cert`, which connects only to
|
||||
this machine: loopback first, then an address of this machine the listener is
|
||||
bound to. A build with no enumerator for its
|
||||
platform **withholds** the capability, so the host can tell a machine that is
|
||||
quiet apart from one that is blind.
|
||||
|
||||
+16
-1
@@ -49,7 +49,22 @@ a dry run first, then an apply bound to the change list the dry run showed.
|
||||
- UDP is held to the declared addresses but not to their ports, and a port
|
||||
declared for one address is open on every declared address;
|
||||
- a destination on this machine, a link-local address, or a cloud's
|
||||
metadata service is refused, as written and as resolved.
|
||||
metadata service is refused, as written and as resolved;
|
||||
- on a machine no fleet has anchored (your own machine), so is a private,
|
||||
ULA, CGNAT or reserved address: one written as an address is refused when
|
||||
the op arrives, and a name is refused at apply if any address it resolves
|
||||
to is one. A verb's fetch (`tree.checkout`, and `k8s.set_image`'s
|
||||
ancestry check) is held the same way, its host checked once and git held
|
||||
to that address, which needs git 2.37 or newer; on an anchored machine
|
||||
that fetch is held to its checked address too, and is never this
|
||||
machine, its loopback, a link-local address or a metadata service. An
|
||||
image whose registry is on your network or this machine is never put
|
||||
live: it is refused when a program reports it and when `k8s.set_image`
|
||||
would set it. To reach another computer
|
||||
on your network, install a bastion on that computer and run the op there.
|
||||
An anchored machine keeps private
|
||||
destinations, because a fleet's ship steps reach its private control
|
||||
planes.
|
||||
|
||||
**A dry run has no network at all,** because nobody approved it.
|
||||
- **Memory: no ceiling, and the op goes first.** A run may use whatever
|
||||
|
||||
+38
-1
@@ -94,6 +94,41 @@
|
||||
changes anything outside its declared file effects. A one-time apply lands
|
||||
only the change list you saw; a run under a standing grant is held to the
|
||||
grant's bounds.
|
||||
- **A standing grant is held here to its window and rate, and dies with the
|
||||
key that approved it.** The machine reads the window on its own clock and
|
||||
counts the rate from the runs it logged in the grant's record in `ops.json`,
|
||||
which it seals with a key derived from its own encryption key. A record
|
||||
edited by hand or by a program without that key, or copied from another
|
||||
machine, holds no grant. The seal does not stop the bastion's own account:
|
||||
it can read the key, so it can write a new record or put back an older
|
||||
sealed copy with fewer runs logged and more uses left. That account is root
|
||||
on a networked install, and root on the machine is outside what this model
|
||||
defends. A grant taken back while the machine is offline stops at the Host
|
||||
at once, which routes no run through it; the machine keeps its own record
|
||||
until the drop is sent again or the grant expires (30 days at most). The
|
||||
heartbeat does not yet carry the grants the Host still holds, which would let
|
||||
the machine drop the rest itself as it does for coder grants. `ops drop
|
||||
--all` at the machine takes every one back there.
|
||||
- **On your own machine, an action op reaches the internet, never your
|
||||
network.** On a machine no fleet has anchored, an action op's destination
|
||||
is never a private, ULA, CGNAT or reserved address, also inside a NAT64 or
|
||||
6to4 address: one written as an address is refused when the op arrives
|
||||
(`op_sync`), and a name is refused at apply if any address it resolves to
|
||||
is one. A verb's fetch (`tree.checkout`'s repository, `k8s.set_image`'s
|
||||
ancestry check), which runs with the bastion's root outside the sandbox,
|
||||
is held to the same rule: its host is resolved once, every address checked,
|
||||
and git held to that address (git 2.37 or newer); on an anchored machine
|
||||
that fetch is still never this machine, its loopback, a link-local address
|
||||
or a cloud's metadata service. An image `k8s.set_image` puts live is
|
||||
pulled by the machine's container runtime, by name, outside the bastion's
|
||||
walls, so on your own machine its registry is judged where the bastion
|
||||
takes the image (a program's result, and the verb) and refused on your
|
||||
network or this machine; the runtime's own later lookup is not the
|
||||
bastion's to hold. Another computer of yours
|
||||
is reached through a bastion on that computer. An anchored machine keeps
|
||||
private destinations, since a fleet's ship steps reach its own private
|
||||
control planes. (Before this, a local machine held an op whose destination
|
||||
was on your LAN, and its apply reached it.)
|
||||
- **A server in the service slot has no network.** The host's tunnel is its
|
||||
only way in, and it cannot reach this machine, your network or the
|
||||
internet.
|
||||
@@ -189,7 +224,9 @@
|
||||
(`observe:alerts`, `observe:metrics`, `observe:logs`), read-only HTTP
|
||||
against an Alertmanager/Prometheus/Loki on the box's own network —
|
||||
private addresses only, re-checked at the socket so neither a redirect nor
|
||||
a DNS answer can walk a query out of that neighbourhood.
|
||||
a DNS answer can walk a query out of that neighbourhood; and
|
||||
`observe:cert`, a TLS handshake with a listener on this machine, at
|
||||
loopback or an address this machine holds, never one another machine could.
|
||||
- **A backup's key goes to a host only when you approve it here.** A host's
|
||||
request to restore files a request and gets nothing until you approve it
|
||||
on this machine; the key sealed to the host then opens that one backup and
|
||||
|
||||
Reference in New Issue
Block a user