* refactor(network): introduce shared egress pipeline Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): cover shared proxy egress paths Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * refactor(network): make destination authorization explicit Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * refactor(network): pin proxy relay policy context Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): lock relay generation contracts Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): establish phase zero compatibility baseline Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * feat(policy): detect ambiguous network endpoints Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * feat(sandbox): fail closed on invalid policy updates Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * refactor(network): invalidate relays on policy changes Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * docs(policy): document validation failure posture Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): cover validation and middleware egress Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(config): move policy failure mode to gateway toml Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): name proxy contracts by behavior Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(network): align overlap validation with endpoint selection Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): expect hard loopback denial Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): match declared endpoint denial Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(policy): preserve path-specific endpoint overrides Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(network): respect hard-blocked host gateways Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(network): reconcile proxy refactor with main Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(network): preserve CONNECT policy generation Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(policy): cover runtime endpoint glob semantics Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(server): reject ambiguous policies before persistence Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * docs(policy): explain ambiguity preflight behavior Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(policy): compare body limits within protocol Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(proxy): avoid global tracing capture race Signed-off-by: John Myers <johntmyers@users.noreply.github.com> * chore(server): format rebased provider tests Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * test(server): authenticate rebased policy requests Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): retain runtime on middleware outage Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(server): preflight provider composition activation Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(sandbox): distinguish runtime failure transitions Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> Signed-off-by: John Myers <johntmyers@users.noreply.github.com> Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
16 KiB
Security Policy
OpenShell policy defines what a sandboxed agent can access. The policy is enforced inside each sandbox by kernel controls, process setup, and the local policy proxy. The gateway stores and delivers policy, but it does not make per-request egress decisions.
For the field-by-field YAML reference, use Policy Schema Reference.
Policy Areas
| Area | Enforcement |
|---|---|
| Filesystem | Landlock restricts read-only and read-write paths. |
| Process | The supervisor launches the agent as an unprivileged user with reduced capabilities. |
| Network | The proxy evaluates destination, port, calling binary, and optional L7 rules. |
| Inference | inference.local is configured through gateway inference settings, not OPA network policy. |
| Runtime settings | Typed settings are delivered with policy and can be global or sandbox scoped. |
Filesystem and process policy are startup-time controls. Network policy is dynamic and can be hot-reloaded when the new policy validates successfully.
Before applying Landlock, the supervisor enriches baseline filesystem paths that
the runtime needs. Missing baseline paths are skipped so one absent runtime path
does not weaken the whole ruleset. When GPU devices are present, GPU baseline
enrichment adds existing GPU device nodes as read-write paths and promotes
/proc to read-write because CUDA workloads write thread metadata under
/proc/<pid>/task/<tid>/comm.
Landlock rules are tailored to the inode type reported by the already-opened
path descriptor. Directories retain the requested directory and file rights;
regular files, device nodes, sockets, and other non-directories retain only
file-compatible rights. This avoids rejecting valid mixed-path policies without
weakening hard_requirement: genuine unsupported ABI capabilities and
preparation failures still fail sandbox startup.
Network Decisions
Ordinary network traffic follows this order:
- Force traffic through the sandbox proxy with namespace and seccomp controls.
- Identify the calling binary and compare its trusted identity.
- Reject hard-blocked destinations, including unsafe internal IP ranges unless explicitly allowed.
- Match the destination and binary against network policy blocks.
- Apply optional HTTP/L7 rules for endpoints that enable protocol inspection.
- Allow, deny, audit, or log according to the matched policy.
Explicit deny and hardening checks win over allow rules. If no rule matches, the request is denied.
Host Wildcards
Network endpoint host patterns accept a * wildcard inside the first DNS
label and as an entire middle DNS label. The OPA runtime matches with a .
label boundary, so a wildcard never spans dots. The validator enforces the same
boundary so that policy load fails fast instead of silently mismatching at the
proxy.
| Pattern | Accepted | Example match | Notes |
|---|---|---|---|
*.example.com |
Yes | api.example.com |
Single first label of any value. |
**.example.com |
Yes | a.b.example.com |
Recursive wildcard as the entire first label. |
*-aiplatform.googleapis.com |
Yes | us-central1-aiplatform.googleapis.com |
Intra-label wildcard inside the first DNS label. |
*.s3.*.amazonaws.com |
Yes | bucket.s3.us-east-1.amazonaws.com |
Middle-label * matches exactly one DNS label. |
* or ** |
No | — | Matches every host. |
*.com, **.com |
No | — | TLD wildcards (labels <= 2). |
foo.us-*.example.com |
No | — | Partial middle-label wildcards are not allowed. |
foo.**.example.com |
No | — | Recursive wildcard outside the first label is not allowed. |
foo**.example.com |
No | — | Recursive ** mixed inside a label; allowed only as the entire first label. |
Validation rejects the disallowed patterns at policy load time with a message that names the offending host. Exact hosts and IP addresses do not use this path.
TLS and L7 Inspection
For HTTP endpoints that need request-level controls, the proxy can terminate TLS with the sandbox's ephemeral CA and inspect method/path or protocol-specific metadata before forwarding. The proxy also supports credential injection on terminated HTTP streams when policy allows the endpoint.
Raw streams and long-lived response bodies are connection scoped. Policy
generation changes close relays pinned to the previous generation instead of
allowing them to continue under stale authorization. HTTP upgrades switch to
raw relay by default. A protocol: rest endpoint can opt in to
websocket_credential_rewrite for client-to-server WebSocket text messages
after an allowed 101 upgrade; server-to-client traffic and all other upgraded
protocols remain raw passthrough.
Live Updates
The gateway stores sandbox-authored policy revisions separately from derived effective sandbox configuration. Effective configuration can include gateway-global policy overrides and provider-profile policy layers. The supervisor polls for config revisions and attempts to load new dynamic policy into the in-process OPA engine; CLI reads of the latest sandbox policy use the same effective configuration path.
The supervisor validates complete effective policy generations before activation. Overlapping endpoint selectors may contribute request allow and deny rules only when their connection and request-processing metadata agree; conflicting TLS, destination, credential, parser, or enforcement metadata rejects the complete generation. Plain L4 endpoints do not contribute request-processing metadata, so they may overlap an L7 endpoint when their connection metadata agrees. When request paths overlap, a path endpoint with a higher specificity rank deterministically overrides broader request-processing metadata. Equally specific overlapping endpoints must agree.
Gateway mutation paths validate the complete effective candidate before persistence when the affected sandbox scope is known. Direct replacements, incremental merges and approvals, provider attachment, and profile fanout reject ambiguity atomically, without creating an invalid revision or partially activating an update. Supervisor validation remains the defense-in-depth boundary for startup, concurrent changes, and sources outside those mutations.
The [openshell.gateway] policy_validation_failure_mode configuration controls
candidates rejected by supervisor runtime validation. Gateway preflight
rejections never become generations and leave the active policy unchanged. The
runtime mode defaults to fail_closed, which publishes a quarantine generation,
denies new egress, invalidates existing relays, and leaves the previous policy
inactive. Operators may explicitly select
retain_last_valid, which keeps the previous generation active. With no
previous valid generation, the effective mode remains fail_closed regardless
of the configured mode. The gateway distributes this startup configuration to
sandbox supervisors with each effective policy snapshot. OCSF configuration and finding events state the
candidate version, validation rationale, configured and effective modes, active
generation, and whether the previous policy is active. Static controls,
such as filesystem allowlists and process identity, require a new sandbox
because they are applied before the child process starts.
Gateway-global policy can override sandbox-scoped policy. Use it sparingly because it changes the effective access model for every sandbox on the gateway.
Policy Advisor
The policy advisor pipeline turns observed denials into draft policy
recommendations. There are two proposers (sandbox-side mechanistic mapper,
agent-authored via policy.local); the gateway is the single referee.
When enabled, L7 policy_denied responses include both structured
next_steps and a short agent_guidance string so generic agents can continue
through the proposal loop instead of treating the denial as terminal.
- Submit. Both proposers POST through the same
SubmitPolicyAnalysispath. Each chunk is persisted with itsanalysis_modefor audit provenance. - Validate. The gateway runs the prover (
openshell-prover) on every chunk regardless of mode. The prover builds a Z3 model from the merged policy plus the sandbox's attached-provider credential set, then computes the delta of findings between the current baseline and the merged policy. - Auto-approval gate (proposer-agnostic, opt-in). Auto-approval fires
only when all three conditions hold: (a)
proposal_approval_moderesolves to"auto"— gateway scope wins, sandbox scope is the per-sandbox override, default is"manual"; (b) the prover delta is empty (prover: no new findings); and (c) the security notes recomputed from the chunk's current proposed rule are empty (see Security-notes gate). Before merging, the gateway reloads the stored chunk and reruns both checks on its current rule. This is important after edits and mechanistic deduplication: the stored rule, not a duplicate incoming payload or stale persisted analysis, controls the decision. The recalculated prover verdict is decision-local rather than persisted, sovalidation_resultreads can still show the submit-time verdict after an edit or deduplication. Decode, prover, or merge failures leave the chunk pending. The audit event usesCONFIG:APPROVEDand carriesauto=true,source=<mode>,prover_delta=empty, andresolved_from=<gateway|sandbox>as unmapped fields, with message text"auto-approved: no new prover findings"— neversafe. The opt-in gate preserves OpenShell's default-deny posture: with no setting at either scope, every proposal lands inpendingfor human review, even when the prover sees no findings. - Implicit supersede. On any successful submission, the gateway scans
the sandbox's pending chunks for matches on
(host, port, binary)and auto-rejects the older ones with reason"superseded by chunk X". This gives the agent a refinement path (broad mechanistic L4 → narrow agent L7) without an explicitsupersedes_chunk_idfield. - Mechanistic dedup and self-reject. Mechanistic submissions dedup on
(host, port, binary): a repeat denial for an endpoint that already has a draft row folds into that row (bumpinghit_count) instead of creating a new chunk. When the endpoint is already covered by a different approved chunk, the redundant mechanistic submission self-rejects on arrival with reason"already covered by approved chunk X". This only ever acts on a genuinely fresh, still-pendingsubmission; it never rewrites the status of an already-decided chunk. A dedup hit that resolves to an approved row's own id leaves that chunkapprovedand merged — the self-reject path does not un-merge a rule, so flipping an approved chunk torejectedwould leave the governance ledger disagreeing with the still-enforced policy. - Escalation. Anything else lands in
pendingfor human review.
Security-notes gate
Separately from the prover, each chunk carries advisory security_notes.
Reads, bulk approval, and auto-approval regenerate them from the current
stored proposed rule instead of trusting a persisted value that may be stale
after an edit. Non-empty notes block auto-approval and make
ApproveAllDraftChunks skip the chunk unless include_security_flagged is
set. The chunk stays pending; an explicit human approval can still merge a
flagged chunk.
Private/internal destinations are advisory, not blocking. A literal endpoint
IP, allowed_ips entry, or CIDR intersection in RFC 1918, CGNAT
100.64.0.0/10, IPv6 ULA fc00::/7, or another special-use range covered by
openshell-core net::is_internal_net produces a note. A hostless rule
carrying allowed_ips earns an extra note because it can match any hostname
resolving into the range.
Always-blocked destinations are separate from this advisory classification.
Loopback, link-local, and unspecified IPs/CIDRs, plus localhost and known
metadata endpoint hostnames, are excluded from security notes. Submit and edit
may store such a draft, but existing merge validation rejects it when an
approval attempts to add it to policy; runtime SSRF protections remain the
final enforcement boundary.
What the prover decides
The prover answers four formal questions about each proposed policy
change. Each "yes" answer becomes its own categorical finding — there is
no severity grade. Any finding (of any category) blocks auto-approval.
The categories are intended to be (mostly) mutually exclusive per
underlying change: the gateway suppresses capability_expansion paths
whose (binary, host, port) is also in the credential_reach_expansion
delta, so a brand-new credentialed reach surfaces as one finding rather
than one reach + N method findings.
| Category | The prover detects… |
|---|---|
link_local_reach |
The proposal grants reach to a host in 169.254.0.0/16, fe80::/10, or a known metadata hostname such as metadata.google.internal. Unconditional — cloud-metadata endpoints serve credentials regardless of sandbox state. |
l7_bypass_credentialed |
The proposal lets a binary using a non-HTTP wire protocol (git-remote-https, ssh, nc) reach a host where a sandbox credential is in scope. The L7 proxy cannot inspect the wire protocol; the reviewer decides whether to trust the binary with the credential. |
credential_reach_expansion |
A binary gained credentialed reach to a (host, port) it could not reach before. New authenticated reach is a stated intent change; the reviewer confirms the binary should authenticate to the host at all. |
capability_expansion |
On a (binary, host, port) that already had credentialed reach, the policy adds a new HTTP method. The reviewer sees exactly which method was added (e.g., PUT) and decides if it's part of the agent's task. |
"Credential in scope" is sandbox-coarse, not binary-fine: a credential is
considered in scope if the sandbox has a provider attached whose
target_hosts include the proposed endpoint's host, including runtime-like
first-label wildcard coverage such as *.github.com covering
api.github.com. v1 does not model credential scopes (read-only vs write);
presence is enough.
Proposals intentionally omit allowed_ips. If a proposed rule targets a host
that resolves to a private IP, the proxy's runtime SSRF classification blocks
the connection. The operator must then add an explicit allowed_ips entry to
permit it — a two-step flow that keeps SSRF protection on by default.
The advisor proposes narrow additions and preserves explicit-deny behavior. Auto-approval is gated on prover determinism, not human judgment; an LLM-based contextual reviewer is a deliberate future addition layered on top of the deterministic prover gate.
Security Logging
Sandbox events that represent observable behavior use OCSF structured logs:
| Event | OCSF class |
|---|---|
| Network and proxy decisions | Network or HTTP activity |
| SSH authentication and relay activity | SSH activity |
| Process lifecycle | Process activity |
| Policy and settings changes | Configuration state change |
| Security findings | Detection finding |
Use plain tracing for internal plumbing such as retries, debug state, and intermediate steps where the final observable event is logged separately. Forward-proxy success is a final observable event: emit it only after middleware, token grants, credential rewriting, policy-generation checks, and the HTTP relay have succeeded so a later denial cannot coexist with an allowed record for the same request.
Never log secrets, credentials, bearer tokens, or query parameters in OCSF messages. OCSF JSONL output may be shipped to external systems.