Documentation
¶
Overview ¶
Package security is an executable specification of cloop's threat model.
The security guarantees this suite protects are stated in prose in the README and enforced in code across four packages that do not import each other: pkg/executor (the host-execution policy), pkg/secretbroker (scoped credential grants), pkg/executorstore (enrollment token persistence), and pkg/ui (the control plane that must never execute anything itself). Nothing in that arrangement fails loudly when a refactor quietly reconnects the UI to os/exec, widens a container's privileges, or lets an expired lease redeem. A guarantee that only exists in prose is a guarantee that regresses silently.
So these tests assert the properties directly, at the boundary a reviewer would check by hand: not "does this function work" but "is it still impossible to reach exec.Command from an HTTP handler". They live outside the packages they audit on purpose — an external test can only use the exported surface, which is the same surface an attacker and an embedder see, and it cannot be weakened by a change to package internals without that change being visible here.
This file implements the static reachability analysis behind guarantee (1). The rest of the suite is behavioral.
Index ¶
- Constants
- Variables
- type Graph
- func (g *Graph) FindBackgroundSpawns(doors map[string]string) (spawns []Violation, reached map[string]bool)
- func (g *Graph) FindFunc(pkgPath, name string) (string, bool)
- func (g *Graph) FindSpawnReachable() []Violation
- func (g *Graph) HandlerCount() int
- func (g *Graph) MissingBackgroundRoots() []string
- func (g *Graph) Reaches(from, to string) ([]string, bool)
- type Violation
Constants ¶
const ExecutorBoundary = ModulePath + "/pkg/executor"
ExecutorBoundary is the one package tree permitted to spawn processes.
The whole architecture rests on this being the only door: a driver under pkg/executor knows *where* a workload runs (this host, a container, a remote agent) and is the only code entitled to decide. Everything upstream of it speaks in Specs and Handles. Traversal stops here rather than treating it as a violation, which is what makes this a boundary check and not a ban.
const ModulePath = "github.com/blechschmidt/cloop"
ModulePath is the import path prefix of this module. Everything outside it is a dependency we do not audit.
Variables ¶
var AuditedRoots = []string{ ModulePath + "/pkg/ui", ModulePath + "/pkg/apiserver", }
AuditedRoots are the packages whose HTTP handlers must not reach process execution. Both terminate network requests from potentially untrusted callers, so both inherit the same obligation.
var BackgroundRoots = []string{
"github.com/blechschmidt/cloop/pkg/ui.NewInCluster",
"(*github.com/blechschmidt/cloop/pkg/ui.Server).Run",
"(*github.com/blechschmidt/cloop/pkg/ui.Server).Start",
"(*github.com/blechschmidt/cloop/pkg/apiserver.Server).Run",
"(*github.com/blechschmidt/cloop/pkg/apiserver.Server).Start",
}
BackgroundRoots are where the hub runs code no request asked for (Task 20418): its construction — which bootstraps the executors and runs the startup sweep — and Run and Start, which start the watchers, the cluster bus and the periodic sweep. Every goroutine the hub starts outside a request is referenced from one of them, and the reference graph follows a function value as an edge, so these reach all of it. A goroutine a handler starts is the handler's, and the handler check covers it.
Functions ¶
This section is empty.
Types ¶
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph is a whole-module function reference graph.
func LoadGraph ¶
LoadGraph type-checks every package in the module and builds the reference graph. Each call returns a graph of its own, so a test may seed edges into it without another test seeing them.
It deliberately loads the whole module rather than just the audited packages: the interesting violations are transitive, hiding two or three hops away in a helper package that shells out to git, and a check that only reads pkg/ui would never see them.
func (*Graph) FindBackgroundSpawns ¶ added in v0.0.5
func (g *Graph) FindBackgroundSpawns(doors map[string]string) (spawns []Violation, reached map[string]bool)
FindBackgroundSpawns returns every process the background roots reach outside the executor boundary, one Violation per spawning function, and the doors it stopped at.
It differs from FindSpawnReachable in three ways. Every spawn is reported, not the first per root: a root here is the whole hub, and one violation must not hide the next. The search stops at an HTTP handler, which the handler check owns. And it stops at doors — functions that run one program on the hub deliberately, each with the reason it is allowed and a test proving the reason (sanctionedHostDoors in callgraph_test.go); a door the search never reaches is stale.
func (*Graph) FindFunc ¶
FindFunc returns the graph key for a function declared in pkgPath.
name may be a plain function ("forget") or a method ("(*Agent).forget" is matched by "forget" too, since a package rarely declares both). The lookup is by suffix rather than by an assembled FullName because the receiver's rendering — pointer, alias, type parameters — is a detail a test should not have to reproduce exactly.
func (*Graph) FindSpawnReachable ¶
FindSpawnReachable returns every audited handler that transitively reaches process execution outside the executor boundary.
Search is breadth-first so the reported path is the shortest one, which is almost always the one that explains the violation best. Packages under pkg/executor terminate the search: reaching a driver is the intended architecture, and the drivers spawn processes because that is their job.
func (*Graph) HandlerCount ¶
HandlerCount reports how many HTTP handlers were discovered, so a test can refuse to pass vacuously when a refactor renames or moves them all.
func (*Graph) MissingBackgroundRoots ¶ added in v0.0.5
MissingBackgroundRoots lists the roots the graph does not have, so a test can refuse to pass when a rename leaves the background check with nothing to start from.
func (*Graph) Reaches ¶
Reaches reports whether `to` is reachable from `from`, returning the path.
Same reference-graph semantics as FindSpawnReachable: taking a function's address counts as an edge. For a "the wipe is reachable" assertion that over-approximates in the *unsafe* direction, so the path is returned for the test to print and a human to sanity-check rather than trusted blindly.
type Violation ¶
type Violation struct {
Handler string
// Path is the chain of functions from handler to spawn, inclusive.
Path []string
// Spawn is the display name of the offending stdlib call.
Spawn string
}
Violation is one HTTP handler that can reach a process-spawning call without passing through the executor boundary.