Files
OpenShell/crates/openshell-sandbox
Adel Zaalouk def7da876c fix(ocsf): emit schema-conformant OCSF 1.8 events (#4288)
Native events failed the official OCSF 1.8.0 validator: HTTP Activity
emitted is_src_dst_assignment_known, sandbox Process Activity events had
no actor, connection_info lacked direction_id, and SSH Activity events
named no endpoint. Schema tests missed these because they checked only
top-level, non-profile attributes.

Add a connection direction enum, require an actor on Process Activity
and an endpoint on SSH Activity at compile time, and name the
supervisor's SSH server with svc_name and its socket path.

Validate against OCSF 1.8.0 only. validate_required_fields and
validate_known_fields share one recursive walker that rejects undefined
attributes at every level and, for events, attributes from undeclared
profiles; objects are validated as objects. Records labelled with
another version fail. Vendor tactic and technique so ATT&CK-mapped
findings validate, drop the unused v1.7.0 schemas, and make the
embedded-schema drift test tolerate CRLF checkouts.

Keep it fixed with a per-builder conformance test and a Podman e2e test
that runs an L7 policy against a local server and validates the
supervisor's real Network, HTTP and SSH Activity JSONL. Top-level
container stays unchanged; #4915 tracks it. The schema
downgrade is unchanged.

Closes #4283

Signed-off-by: Adel Zaalouk <azaalouk@redhat.com>
2026-10-28 20:50:57 +00:00
..

Sandbox runtime

openshell-sandbox owns the Linux workload isolation mechanisms and their kernel qualification probes. The linux module contains seccomp notifications and child filters, task-memory access, process-signal mediation, procfs descriptor lookup, socket identity, Landlock qualification, and the workload launch thread.

sandbox::linux composes policy enforcement. The top-level linux module owns the lower-level mechanisms used by that orchestration and the boundary server. Unsafe kernel ABI, process-launch, and descriptor-ownership operations stay in this runtime crate behind safe APIs where their contracts can be enforced.

The network broker retains source socket descriptors only while preparing a workload endpoint. A successful connection or listen releases that reference; the kernel then tracks the lifetime through workload-owned descriptors. Native accept and repeated listen preserve the socket's loopback confinement. Closing the last workload reference to a listener removes it without waiting for registry collection. Broker source descriptors always use close-on-exec independently of the workload's requested descriptor flags.

A boundary configured with RuntimeInitiatedTlsTcp binds no listener. It dials only the driver-provided supervisor addresses, which must be concrete, non-loopback, and share one port, and serves TLS and gRPC over that outbound connection. It keeps one spare connection that the supervisor has not yet used and dials a replacement when the supervisor starts HTTP/2 on it or the spare closes. Live connections keep the same disconnect, freeze, and recovery-deadline handling as listener mode, and the workload still cannot connect to the control port.

A runtime of a supervisor shared by several sandboxes (one with a route handle) names its route in a version 2 preface on every dial and follows the supervisors' redirects, on the same port and never on loopback, at most two hops. A supervisor that closes the negotiation without replying, as one from before version 2 does, is dialed again with a version 1 preface and treated as a lone supervisor; this changes only routing, since TLS pinning, request authentication, and takeover grants are unaffected. It keeps a spare at the member whose connection it last confirmed, its authenticated owner; an unauthenticated accept never makes a member the owner. Once a supervisor names itself a pool member or redirects it, the runtime also dials the configured addresses past every known member every 15 seconds, up to three times, so a member that hosts the route without a spare gets one: a previous owner that stays reachable after the gateway granted the route elsewhere, holding an unused spare, cannot keep the new owner from the runtime. A lone shared supervisor never names itself, so it holds the one spare and is never probed. The runtime keeps at most three unused spares, one per member, and never closes an established connection to refresh routing. A member's address grants nothing: a later term at a fenced owner's address authenticates with its own grant. A retired connection that does not shut down gracefully within two seconds, such as an unused spare, has its transport dropped.

When AgentSpec::otlp_relay_active is set, child_env::apply_otlp_relay_env gives every workload process (main, exec, and SSH) OTEL_EXPORTER_OTLP_ENDPOINT pointing at the relay address, OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf, and none for OTEL_EXPORTER_OTLP_COMPRESSION and OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, and removes any inherited OTEL_EXPORTER_OTLP_TRACES_ENDPOINT. These defaults apply after the provider variables. If the creation request or a per-exec environment names a generic or traces-specific OTLP endpoint, the runtime injects nothing, and the workload exports to that endpoint under the sandbox's network policy. An endpoint in the image ENV does the same for the main process; exec children start from a cleared environment (ChildEnvironment::Cleared), never see the image's endpoint, and still get the relay default.

Supervisor takeover

The boundary advertises openshell.sandbox.supervisor-takeover. A replacement supervisor process may attach to a running generation only with a gateway-signed takeover grant for a later credential epoch, its bearer of that epoch, and the grant's supervisor process identity. The attach is staged and promoted at confirmation, which advances the accepted epoch, pins the new process, and closes every connection of the previous owner; credentials of earlier epochs are refused from then on. Refused attempts emit an OCSF configuration-state event and a sandbox-supervisor-takeover-denied finding.

Attach and confirmation are serialized, and the registry checks an attach before it binds anything. Owner operations (exec, signals, termination, provider updates, network decisions, DNS answers) hold an ownership guard from authorization through their effect, and a promotion takes it exclusively, so none of the replaced owner's operations takes effect afterwards. The guard is asynchronous: tasks on the runtime's workers wait for it without blocking them. A confirmation replay on the connection in force promotes nothing and skips the guard. Promotion and the supervisory state change together, so a replacement lost mid-promotion leaves the workload frozen under a fresh recovery deadline. Each supervisor offers a recovery deadline when it attaches (10 to 300 seconds; the runtime refuses any other before binding anything), and the deadline of the connection in force when supervision is lost bounds the freeze: 30 seconds without an offer. A supervisor pool offers 60 seconds unless the gateway configures another.

The runtime keeps the trust-on-first-use executable pins of the generation (binary_pins): the first digest it offers for each path, from its own measurements. A contradicting identity is offered as a resolution failure, which every supervisor denies, so a replacement with an empty cache cannot pin a binary the workload replaced.