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), under a task root hidden from every other task;
- 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 ( RelayProxyAddr = "127.0.0.1:1080" RelayDNSAddr = "127.0.0.1:53" )
RelayProxyAddr is where a confined task finds its proxy (HTTP CONNECT, absolute-form HTTP or SOCKS5, all relayed to its forwarder), and RelayDNSAddr its resolver.
const EnvEgressProxy = "OPENCTEM_EGRESS_PROXY"
EnvEgressProxy is set, for a confined task with a forwarder, to the relay's proxy URL (HTTP CONNECT and SOCKS5 on one port): a wrapper passes it to a tool that takes a proxy flag rather than the proxy variables.
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 NewTaskDir ¶ added in v0.19.0
NewTaskDir makes a new private directory (0700) under TaskRoot for a caller that keeps a task's files outside the backend's own directory (the tool host's protocol directory).
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.
func TaskRoot ¶ added in v0.19.0
func TaskRoot() string
TaskRoot is the directory private task directories are made under by default (<os.TempDir()>/openctem-tasks). The process backend hides it, and its own WorkRoot, from every task: a task reads and writes its own directory and cannot read or list a sibling's, whether the sibling is a task of the same tenant or, on a shared sensor, of another.
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
// ConfineNetwork runs every task in its own user, network and mount
// namespaces (Linux; api RFC-060): only loopback exists, and the task's
// way out is its forwarder (TaskSpec.EgressProxy, EgressDNS). It needs
// unprivileged user namespaces (a container seccomp profile that
// allows them; on a host with AppArmor's restriction, a profile for the
// sensor). Where they are not available, tasks run unconfined and
// Status.NetworkMissing says why, unless RequireNetwork is set.
ConfineNetwork bool
// RequireNetwork makes NewProcessBackend fail when ConfineNetwork
// cannot be enforced (a shared sensor, where it is a requirement).
RequireNetwork bool
// Private are more directories hidden from every task, like the task
// root: a task reads beneath them only what TaskSpec.ReadPaths grants
// it (the sensor's content cache: one tenant's packs are never
// readable by another tenant's task).
Private []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" // NetworkResolver: DNS questions through the task's forwarder only, no // connection to any host. NetworkResolver NetworkClass = "resolver" 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. With RequireNetwork it fails unless tasks' network is confined.
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's
// network: the process backend with Config.ConfineNetwork, where user
// namespaces are available, runs each task in its own network
// namespace whose only way out is its forwarder (TaskSpec.EgressProxy,
// EgressDNS). Without it 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"`
// NetworkMissing says why network confinement was asked for and is not
// enforced (empty when it is, or was not asked for).
NetworkMissing string `json:"network_missing,omitempty"`
// 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
// ReadPaths are paths the task may read even beneath a private root
// (Config.Private, the task root): the content packs of this task
// (templates, rules, wordlists), which no other task may read. Never
// beneath a protected path.
ReadPaths []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
// EgressProxy and EgressDNS are unix socket paths of the task's
// forwarder (pkg/sensorkit/egress): its proxy listener and its DNS
// stream listener. On a backend that confines the network (Status.
// NetworkEnforced) they are the task's only way out: the task sees a
// proxy on 127.0.0.1:1080 (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) and a
// resolver on 127.0.0.1:53 that lead to them, and nothing else. Empty:
// a confined task has no network at all. Ignored when the network is
// not confined.
EgressProxy string
EgressDNS string
}
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.