Skip to content

feat: audit2allow-style permissive mode for sandbox policy learning #1839

@russellb

Description

@russellb

Problem Statement

Onboarding a new workload to OpenShell requires knowing what network endpoints, filesystem paths, and other resources the workload needs — before it runs. This is a chicken-and-egg problem: you can't write a policy without knowing the workload's behavior, and you can't observe the behavior without running it (and having it blocked by the restrictive default policy).

Today, policy discovery is iterative and disruptive: the agent hits a blocked endpoint, fails, you approve the draft proposal, rerun, hit the next blocked endpoint, fail again. You learn the workload's dependency graph one edge at a time across multiple runs.

An SELinux audit2allow-style workflow solves this: run the sandbox in permissive mode where nothing is blocked by policy, but all actions that would have been blocked are logged as draft policy proposals via the existing mechanistic mapper. The agent runs to completion with full credential injection, exercising its real behavior. You review and approve the full set of proposals at once, export the policy, done.

Workflow: Before and After

Example: Onboarding a coding agent that uses OpenAI and GitHub

Today (without permissive mode):

$ openshell sandbox create -- coding-agent
  1. Agent tries to call api.openai.com:443 — blocked. Agent fails.
  2. You check proposals: openshell policy draft get my-sandbox — see the OpenAI endpoint.
  3. openshell policy draft approve my-sandbox <chunk_id> — policy hot-reloads.
  4. Agent retries, calls OpenAI successfully, then tries api.github.com:443 — blocked. Agent fails again.
  5. Approve the GitHub proposal, policy reloads.
  6. Agent retries, calls GitHub successfully, then tries github.com:22 for git-over-SSH — blocked.
  7. Approve again. Repeat until the agent completes its task.

Each denial is discovered sequentially because the agent fails on the first blocked endpoint and may not reach the rest.

With permissive mode:

$ openshell sandbox create --permissive -- coding-agent
  1. Agent runs to completion — calls api.openai.com:443, api.github.com:443, github.com:22, and anything else it needs. All connections succeed with credentials. Nothing is blocked.
  2. Review everything at once:
$ openshell policy draft get my-sandbox

  #1 [pending] api.openai.com:443    confidence: 92%  binary: /usr/bin/curl
  #2 [pending] api.github.com:443    confidence: 90%  binary: /usr/bin/curl
     L7: GET /repos/myorg/myrepo, POST /repos/myorg/myrepo/pulls
  #3 [pending] github.com:22         confidence: 88%  binary: /usr/bin/ssh
  1. Bulk approve and export:
$ openshell policy draft approve-all my-sandbox
$ openshell sandbox get my-sandbox --policy-only > policy.yaml
  1. Run with real enforcement:
$ openshell sandbox create --policy policy.yaml -- coding-agent

Full dependency graph discovered in one run instead of three failure cycles.

Technical Context

OpenShell sandboxes enforce policy at multiple layers, each with different characteristics for permissive mode:

  • Network L4 (TCP): The sandbox proxy intercepts all outbound TCP connections via nftables redirect. OPA evaluates SandboxPolicy network rules against the raw (host, port, binary) tuple. This covers any protocol — HTTP, SSH, database, gRPC, raw TCP. This is the primary learning surface and where most onboarding friction lives.
  • Network L7 (HTTP): For endpoints that declare a protocol in the policy, the L7 relay inspects HTTP method/path against fine-grained rules. The l7/relay.rs module already has an EnforcementMode::Audit variant that logs denials but forwards requests. L7 audit only fires when protocol declarations exist — without them, connections are tunneled as raw TCP and L7 detail is invisible.
  • Filesystem (Landlock): Kernel-level LSM applied at sandbox startup. Irrevocable once applied — no audit mode available. Must be skipped entirely in permissive mode.
  • Process (seccomp, privilege drop): Kernel-level integrity controls that protect the sandbox itself. Must NEVER be weakened — these are not part of the learnable policy surface.
  • Inference routing: Already bypasses OPA policy entirely; not part of the learning surface.

The existing denial-to-proposal pipeline already handles ~90% of this feature:

  1. DenialAggregator (denial_aggregator.rs) captures deny events keyed by (host, port, binary) with L7 method/path detail, flushes to gateway every 10s.
  2. Mechanistic mapper (mechanistic_mapper.rs) converts denial summaries into draft PolicyChunk proposals with confidence scores, security notes, and L7 rule synthesis.
  3. Server (policy.rs) persists proposals, runs the prover for security findings, optionally auto-approves.
  4. CLI (run.rs) surfaces proposals via openshell policy draft get/approve/reject/approve-all.
  5. Export via openshell sandbox get <name> --policy-only outputs the effective policy as YAML.

The core change is making the proxy log-but-allow instead of log-and-block — the rest of the pipeline already exists.

Affected Components

Component Key Files Role
Sandbox proxy crates/openshell-sandbox/src/proxy.rs L4 TCP enforcement point; emits DenialEvents
L7 relay crates/openshell-sandbox/src/l7/relay.rs, l7/mod.rs L7 HTTP enforcement; existing EnforcementMode::Audit
Denial aggregator crates/openshell-sandbox/src/denial_aggregator.rs Aggregates deny events by (host, port, binary); flushes to gateway
Mechanistic mapper crates/openshell-sandbox/src/mechanistic_mapper.rs Converts denial summaries into draft PolicyChunk proposals (unchanged)
OPA engine crates/openshell-sandbox/src/opa.rs Policy evaluation (unchanged — still evaluates, just doesn't enforce)
Landlock crates/openshell-sandbox/src/sandbox/linux/landlock.rs Filesystem restrictions (skip in permissive mode)
Proto definitions proto/sandbox.proto SandboxSpec, SandboxPolicy schemas
CLI crates/openshell-cli/src/main.rs, run.rs Sandbox create/exec commands, --permissive flag
OCSF logging crates/openshell-ocsf/src/builders/network.rs, http.rs Structured audit event emission

No changes needed to: mechanistic mapper, server policy handler, CLI draft commands, policy serialization, or policy export (sandbox get --policy-only).

Technical Investigation

Architecture Overview

Policy enforcement flows through a layered pipeline:

  1. Network namespace (netns.rs): All sandbox egress is routed through the proxy via nftables redirect rules. Unchanged in permissive mode — the proxy still intercepts all traffic.
  2. Proxy CONNECT handler (proxy.rs:522-523): Receives CONNECT requests for any TCP connection, resolves binary identity via /proc/<pid>/exe, calls OpaEngine::evaluate_network_action(), gets NetworkAction::Allow or NetworkAction::Deny. On deny, emits OCSF NetworkActivityBuilder event and sends DenialEvent to aggregator.
  3. L7 relay (l7/relay.rs:324-347): For policy-allowed endpoints with protocol configuration, inspects HTTP method/path against L7 rules. Already has EnforcementMode::Audit that logs but doesn't block.
  4. Landlock (landlock.rs:99-307): Applied at sandbox startup via restrict_self(). Kernel-enforced, irrevocable.
  5. Seccomp (seccomp.rs:72-81): BPF filters applied at startup. Infrastructure integrity control.

Code References

Location Description
proxy.rs:522-523 L4 enforcement decision point: NetworkAction::Allow vs Deny
proxy.rs:560-576 OCSF NetworkActivityBuilder emission on deny
proxy.rs:593 DenialEvent sent to aggregator on deny
l7/mod.rs:54-61 EnforcementMode::Audit | Enforce enum definition
l7/relay.rs:324-328 Audit/enforce decision: logs but forwards in audit mode
denial_aggregator.rs:18-36 DenialEvent struct with host, port, binary, L7 fields
mechanistic_mapper.rs:59-242 generate_proposals() — denial summaries to PolicyChunk proposals
opa.rs:241-297 evaluate_network() — policy evaluation (unchanged in permissive mode)
landlock.rs:99-307 Landlock ruleset preparation and restrict_self()
seccomp.rs:72-81 Seccomp BPF filter application
openshell-cli/src/main.rs:1165-1297 SandboxCommands::Create with --policy flag
openshell-cli/src/run.rs:2521-2562 sandbox get --policy-only — export effective policy as YAML
openshell-cli/src/run.rs:7229-7318 sandbox_draft_get() — display draft proposals
proto/sandbox.proto:317 SandboxSpec.policy field

What Would Need to Change

Sandbox runtime (proxy permissive bypass):

  • proxy.rs: On NetworkAction::Deny in permissive mode, still emit OCSF event and DenialEvent, but allow the TCP connection instead of returning 403. Inject credentials normally. The OCSF event should use a distinct disposition (e.g., DispositionId::Logged or Other with unmapped: {"audit_mode": true}) to distinguish from actual denials.
  • L7 enforcement: Force all endpoints to EnforcementMode::Audit in permissive mode.
  • SSRF checks (internal IP blocking, cloud metadata blocking) must remain active even in permissive mode — these are infrastructure safety guards, not learnable policy.

Sandbox runtime (Landlock skip):

  • landlock.rs: Skip prepare() and enforce() in permissive mode.

Proto schema:

  • Add bool permissive to SandboxSpec (or SandboxPolicy). This flag flows from CLI → server → sandbox.

Denial aggregator:

  • Add audit_mode: bool to DenialEvent to distinguish genuine denials from permissive-mode would-be-denials in mixed fleets.

CLI — sandbox creation:

  • Add --permissive flag to sandbox create and sandbox exec.
  • On startup in permissive mode, if the active policy has no endpoints with protocol declarations, log a hint:
⚠ Permissive mode: no HTTP endpoints declared in policy.
  L7 audit will be limited to host:port only.
  To capture HTTP method/path detail, add endpoint protocol hints:

    endpoints:
      - host: api.example.com
        port: 443
        protocol: rest

  See: https://docs.openshell.dev/...

Patterns to Follow

  1. EnforcementMode::Audit in L7 relay (l7/relay.rs:324-347): Already implements log-but-don't-block. L4 permissive mode should follow the same pattern.
  2. DenialEvent + DenialAggregator (denial_aggregator.rs): Existing pipeline from deny → aggregate → gateway flush. Extend, don't replace.
  3. Mechanistic mapper (mechanistic_mapper.rs): Already generates proposals with confidence scores, security notes, and L7 rules. No changes needed — permissive mode just feeds it more data in a single run.

Data Flow

Agent makes any outbound TCP connection
        │
        ▼
nftables redirect ──► Proxy CONNECT handler
        │
        ▼
OPA evaluates policy on (host, port, binary) ──► Would-be-Deny
        │                                              │
        │                              [permissive: allow anyway,
        │                               inject credentials normally]
        │                                              │
        ▼                                              ▼
OCSF NetworkActivity event              DenialEvent{audit_mode: true}
(disposition: Logged)                   sent to DenialAggregator
                                                       │
                                        Aggregated by (host, port, binary)
                                                       │
                                        Mechanistic mapper ──► PolicyChunk proposals
                                                       │
                                        SubmitPolicyAnalysis ──► Gateway
                                        (every 10s, existing infra)
                                                       │
                                        [user: openshell policy draft get <name>]
                                        [user: openshell policy draft approve-all <name>]
                                        [user: openshell sandbox get <name> --policy-only > policy.yaml]

Proposed Approach

Add a --permissive flag to sandbox create/exec that configures the sandbox proxy to log-but-allow policy-denied TCP connections with full credential injection, while preserving SSRF and integrity controls. Skip Landlock in permissive mode since kernel LSMs have no audit-only mode. The existing denial → mechanistic mapper → draft proposal → CLI review/approve pipeline handles everything downstream with no changes. On startup, hint users to add protocol declarations for richer L7 learning. The generated policy will include a placeholder noting filesystem policy must be created manually.

Scope Assessment

  • Complexity: Medium
  • Confidence: High — existing infrastructure handles ~90% of the work. The core change is making the proxy log-but-allow on deny.
  • Estimated files to change: ~8-10
  • Issue type: feat

Design Decisions (Resolved)

Decision Resolution
CLI UX model --permissive flag on sandbox create/exec. No new subcommand.
Base policy required? No. --permissive works with zero policy (pure L4 discovery). A startup hint recommends adding protocol declarations for richer L7 learning.
Credential injection in permissive mode? Yes — inject credentials for all connections, including would-be-denied. Permissive means permissive.
Permissive mode timeout? No timeout required.
Filesystem in generated policy? Include a placeholder noting filesystem policy must be created manually, since Landlock has no audit mode.
Gateway-level disable switch? Not needed for initial implementation. Document as a potential future option if there is demand.
Policy output format Strict and relaxed variants via existing mechanistic mapper heuristics.

Risks & Open Questions

  1. Relaxed policy heuristics. How aggressively should the relaxed variant generalize? The mechanistic mapper's generalise_path() already has heuristics — may need tuning for the permissive-mode use case where more data is available in a single run.

  2. Security posture of permissive mode. Permissive mode with credential injection allows the agent to reach any endpoint with real credentials. This is a development/onboarding tool, not for production. SSRF and seccomp guardrails still protect the host. Should be documented clearly.

Test Considerations

  • Unit tests: OPA evaluation returns correct allow/deny in permissive mode (evaluation unchanged, enforcement changed). DenialEvent emitted for would-be-denials with audit_mode: true.
  • Integration tests: Permissive sandbox allows TCP connections that would normally be denied. Credentials injected normally. OCSF events emitted with audit disposition. DenialEvents collected, flushed, and proposals generated.
  • E2E tests: Create sandbox with --permissive, make connection that would be denied, verify it succeeds with credentials, verify denial appears as draft proposal via policy draft get, approve, export via sandbox get --policy-only, verify exported policy covers the connection.
  • Security tests: SSRF checks NOT bypassed in permissive mode. Seccomp and process identity NOT weakened.
  • Startup hint test: Verify protocol-hint log message appears when permissive mode is used with no L7 endpoint declarations, and does not appear when endpoints have protocol config.
  • Existing test patterns to follow: OPA engine tests (opa.rs:1187+), L7 relay audit/enforce tests, mechanistic mapper tests, seccomp behavioral tests with fork().

LSM Impact

Permissive mode does not introduce new Linux Security Module concerns:

  • Skipping Landlock removes OpenShell's filesystem restriction, but host SELinux/AppArmor container labels still apply.
  • Seccomp is unchanged in permissive mode.
  • Network namespace and nftables are unchanged — the proxy still intercepts all traffic.
  • /proc/<pid>/exe visibility for binary identity resolution is unaffected.

Gateway Config Impact

The --permissive flag is a per-sandbox runtime flag flowing through SandboxSpec in the proto, not through gateway.toml. No changes to docs/reference/gateway-config.mdx needed for the initial implementation.


Created by spike investigation. Use build-from-issue to plan and implement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions