Documentation
¶
Overview ¶
Package executor runs one task (one tool invocation) for a sensor, in a sandbox.
An Executor backend takes a generic task specification (argv, environment, working directory, resource limits, network class, writable paths) and runs it; it knows nothing about the tool. The process backend (the only one today) runs the task as a child process of the sensor, through a small launcher (the sensor's own binary, re-executed) that confines it before the tool starts:
- a private, throwaway working directory (also its HOME and TMPDIR);
- resource limits: memory (RLIMIT_DATA), processes (RLIMIT_NPROC), file size, open files, CPU time, no core dumps;
- no_new_privs: no setuid/file-capability escalation;
- Landlock (Linux 5.13+): it writes only under its working directory and the paths the caller names, and cannot read the paths the sensor protects (its key and credentials, outbox, configuration, local policy);
- a seccomp filter refusing the syscalls a scanner has no business making (ptrace, mount, namespaces, kernel modules, keyrings, bpf, …);
- its own process group, killed whole on timeout or cancel.
The sensor also makes itself non-dumpable, so a task running under the same user cannot read the sensor's memory, environment or open files through /proc.
A Docker socket is never used: a container backend, when one is added, must be rootless or a Kubernetes Pod per task, never the host's docker.
Mode "auto" (the default for a sensor) enforces what the kernel and the container allow and reports what it could not; "required" refuses to run tasks without every control; "off" runs tasks as plain child processes.
Stability: Beta (docs/STABILITY.md).
Index ¶
Constants ¶
const LauncherArg = "__openctem-task"
LauncherArg is the first argument that makes the sensor binary act as the launcher. It is not a user-facing command.
Variables ¶
This section is empty.
Functions ¶
func RunLauncherIfRequested ¶
func RunLauncherIfRequested()
RunLauncherIfRequested turns this process into the launcher when it was started as one (first argument LauncherArg) and never returns then. A program that uses a sandboxing backend calls it first thing in main, before flags or anything else.
func SetCurrent ¶
func SetCurrent(b Backend)
SetCurrent installs b as the backend (tests, or a backend built elsewhere). nil restores the default.
Types ¶
type Backend ¶
type Backend interface {
// Name is the backend ("process").
Name() string
// Status is the protection tasks get.
Status() Status
// Prepare checks spec and sets up the task without starting it.
Prepare(spec TaskSpec) (Task, error)
}
Backend runs tasks.
type Config ¶
type Config struct {
// Mode is how strictly tasks are sandboxed ("" is ModeAuto).
Mode Mode
// ReadDeny are paths no task may read or write: the sensor's key and
// credentials, its outbox, its configuration and local policy. A path
// that does not exist yet is skipped.
ReadDeny []string
// Limits are the default limits of every task (zero fields:
// DefaultLimits).
Limits Limits
// WorkRoot is where private task directories are made (default
// <os.TempDir()>/openctem-tasks).
WorkRoot string
// Launcher is the binary re-executed as the launcher (default: this
// program). It must call RunLauncherIfRequested first thing in main.
Launcher string
}
Config sets up a ProcessBackend.
type Limits ¶
type Limits struct {
// MemoryBytes bounds the data segment (heap and private mappings).
MemoryBytes uint64 `json:"memory_bytes,omitempty"`
// Processes is how many more processes and threads the task may create.
Processes uint64 `json:"processes,omitempty"`
// FileSizeBytes bounds any file the task writes.
FileSizeBytes uint64 `json:"file_size_bytes,omitempty"`
// OpenFiles bounds the task's open file descriptors.
OpenFiles uint64 `json:"open_files,omitempty"`
// CPUSeconds bounds CPU time per process (0: none; the timeout bounds
// wall time).
CPUSeconds uint64 `json:"cpu_seconds,omitempty"`
}
Limits bound one task. Zero means the backend's default (DefaultLimits).
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits are generous enough for every bundled scanner and stop a runaway or hostile one.
type Mode ¶
type Mode string
Mode says how strictly tasks are sandboxed.
const ( // ModeOff runs tasks as plain child processes (process group and the // caller's environment allowlist only). ModeOff Mode = "off" // ModeAuto enforces every control the host supports and reports the // rest (Status.Missing). ModeAuto Mode = "auto" // ModeRequired refuses to run a task unless every control is enforced. ModeRequired Mode = "required" )
Modes.
type NetworkClass ¶
type NetworkClass string
NetworkClass is the network a task may reach. The process backend records it in the result; it does not enforce it (the sensor's target guard and egress settings do). Backends that can (a Pod per task with a NetworkPolicy) enforce it.
const ( NetworkAny NetworkClass = "any" NetworkTargetsOnly NetworkClass = "targets-only" NetworkEgressProxy NetworkClass = "egress-proxy" NetworkNone NetworkClass = "none" )
Network classes.
type ProcessBackend ¶
type ProcessBackend struct {
// contains filtered or unexported fields
}
ProcessBackend runs each task as a child process, through the launcher when sandboxing is on.
func NewProcessBackend ¶
func NewProcessBackend(cfg Config) (*ProcessBackend, error)
NewProcessBackend checks what this host enforces by running the launcher once in probe mode, and returns the backend. With ModeRequired it fails unless every control is enforced; with ModeAuto it degrades and lists what is missing in Status.
func (*ProcessBackend) Prepare ¶
func (b *ProcessBackend) Prepare(spec TaskSpec) (Task, error)
Prepare makes the task's private directory and its command.
func (*ProcessBackend) Status ¶
func (b *ProcessBackend) Status() Status
Status is the protection tasks get.
type ProcessHooks ¶
type ProcessHooks struct {
Configure func(*exec.Cmd)
Started func(*exec.Cmd)
Finished func(*exec.Cmd)
}
ProcessHooks are the caller's process-level adjustments, called by the process backend: Configure before start (process group, cancel), Started after start (priority), Finished after wait (reap the group, accounting).
type Result ¶
type Result struct {
// ExitCode is the exit status (-1: killed by a signal or not run).
ExitCode int
// State is the process state (process backend).
State *os.ProcessState
// Killed is why the executor stopped the task ("" when it exited).
Killed string
// Duration is the wall time.
Duration time.Duration
// Sandbox is the protection the task ran under (runtime-stamped
// provenance).
Sandbox Status
// Network is the task's network class.
Network NetworkClass
}
Result is how a task ended.
type Status ¶
type Status struct {
Backend string `json:"backend"`
Mode Mode `json:"mode"`
// Sandboxed is true when tasks run through the launcher.
Sandboxed bool `json:"sandboxed"`
// Landlock is the Landlock ABI used (0: not available).
Landlock int `json:"landlock,omitempty"`
Seccomp bool `json:"seccomp"`
NoNewPrivs bool `json:"no_new_privs"`
Rlimits bool `json:"rlimits"`
// NetworkEnforced is true when the backend itself confines the task to
// its network class (a per-task network namespace or policy). The
// process backend records the class but does not enforce it, so it is
// false there; the sensor's target guard and egress settings apply
// instead. Code from outside the project must not run on a backend
// without it.
NetworkEnforced bool `json:"network_enforced"`
// Missing lists the controls this host could not enforce, and why.
Missing []string `json:"missing,omitempty"`
}
Status is the protection a backend gives its tasks.
type Task ¶
type Task interface {
// Workdir is the task's private directory.
Workdir() string
Start(ctx context.Context) error
// Wait returns when the task ends. An error means it could not be
// waited for; a non-zero exit is in the result.
Wait() (*Result, error)
// Kill stops the task and its process group; reason is recorded.
Kill(reason string)
Cleanup() error
}
Task is a prepared task: Start it, Wait for it, Kill it, then Cleanup (always; it removes the private directory).
type TaskSpec ¶
type TaskSpec struct {
// ID names the task in logs and the working directory (optional).
ID string
// Argv is the program and its arguments. Argv[0] is resolved on PATH
// when it has no slash.
Argv []string
// Env is the task's environment ("KEY=value"), already filtered by the
// caller (the SDK passes an allowlist; never the sensor's credentials).
Env []string
// SetEnv are variables the caller sets explicitly; they win over the
// sandbox's own HOME, TMPDIR and XDG_* defaults.
SetEnv map[string]string
// Dir is the working directory; "" is the task's private directory.
Dir string
// WritePaths are paths the task may write besides its private
// directory (an output file's directory, a tool cache it owns).
WritePaths []string
// Limits bound the task (zero fields: DefaultLimits).
Limits Limits
// Network is the network class (see NetworkClass).
Network NetworkClass
// Stdin is the task's standard input (nil: none). A tool that speaks
// the adapter protocol reads its messages from it. Pass an *os.File
// (the read end of an os.Pipe) so the process gets the descriptor
// itself: any other reader is copied by a goroutine that Wait waits
// for.
Stdin io.Reader
// Stdout and Stderr receive the task's output.
Stdout, Stderr io.Writer
// Hooks let the caller adjust the process (process backend only).
Hooks ProcessHooks
}
TaskSpec is one task. It names no tool: whatever a tool needs beyond the defaults (a path it must write, a variable it reads) the caller says here.