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
- Agent tries to call
api.openai.com:443 — blocked. Agent fails.
- You check proposals:
openshell policy draft get my-sandbox — see the OpenAI endpoint.
openshell policy draft approve my-sandbox <chunk_id> — policy hot-reloads.
- Agent retries, calls OpenAI successfully, then tries
api.github.com:443 — blocked. Agent fails again.
- Approve the GitHub proposal, policy reloads.
- Agent retries, calls GitHub successfully, then tries
github.com:22 for git-over-SSH — blocked.
- 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
- 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.
- 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
- Bulk approve and export:
$ openshell policy draft approve-all my-sandbox
$ openshell sandbox get my-sandbox --policy-only > policy.yaml
- 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:
DenialAggregator (denial_aggregator.rs) captures deny events keyed by (host, port, binary) with L7 method/path detail, flushes to gateway every 10s.
- Mechanistic mapper (
mechanistic_mapper.rs) converts denial summaries into draft PolicyChunk proposals with confidence scores, security notes, and L7 rule synthesis.
- Server (
policy.rs) persists proposals, runs the prover for security findings, optionally auto-approves.
- CLI (
run.rs) surfaces proposals via openshell policy draft get/approve/reject/approve-all.
- 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:
- 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.
- 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.
- 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.
- Landlock (
landlock.rs:99-307): Applied at sandbox startup via restrict_self(). Kernel-enforced, irrevocable.
- 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
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.
DenialEvent + DenialAggregator (denial_aggregator.rs): Existing pipeline from deny → aggregate → gateway flush. Extend, don't replace.
- 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
-
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.
-
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.
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):
api.openai.com:443— blocked. Agent fails.openshell policy draft get my-sandbox— see the OpenAI endpoint.openshell policy draft approve my-sandbox <chunk_id>— policy hot-reloads.api.github.com:443— blocked. Agent fails again.github.com:22for git-over-SSH — blocked.Each denial is discovered sequentially because the agent fails on the first blocked endpoint and may not reach the rest.
With permissive mode:
api.openai.com:443,api.github.com:443,github.com:22, and anything else it needs. All connections succeed with credentials. Nothing is blocked.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:
SandboxPolicynetwork 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.protocolin the policy, the L7 relay inspects HTTP method/path against fine-grained rules. Thel7/relay.rsmodule already has anEnforcementMode::Auditvariant 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.The existing denial-to-proposal pipeline already handles ~90% of this feature:
DenialAggregator(denial_aggregator.rs) captures deny events keyed by(host, port, binary)with L7 method/path detail, flushes to gateway every 10s.mechanistic_mapper.rs) converts denial summaries into draftPolicyChunkproposals with confidence scores, security notes, and L7 rule synthesis.policy.rs) persists proposals, runs the prover for security findings, optionally auto-approves.run.rs) surfaces proposals viaopenshell policy draft get/approve/reject/approve-all.openshell sandbox get <name> --policy-onlyoutputs 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
crates/openshell-sandbox/src/proxy.rscrates/openshell-sandbox/src/l7/relay.rs,l7/mod.rsEnforcementMode::Auditcrates/openshell-sandbox/src/denial_aggregator.rscrates/openshell-sandbox/src/mechanistic_mapper.rscrates/openshell-sandbox/src/opa.rscrates/openshell-sandbox/src/sandbox/linux/landlock.rsproto/sandbox.protoSandboxSpec,SandboxPolicyschemascrates/openshell-cli/src/main.rs,run.rs--permissiveflagcrates/openshell-ocsf/src/builders/network.rs,http.rsNo 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:
netns.rs): All sandbox egress is routed through the proxy via nftables redirect rules. Unchanged in permissive mode — the proxy still intercepts all traffic.proxy.rs:522-523): Receives CONNECT requests for any TCP connection, resolves binary identity via/proc/<pid>/exe, callsOpaEngine::evaluate_network_action(), getsNetworkAction::AlloworNetworkAction::Deny. On deny, emits OCSFNetworkActivityBuilderevent and sendsDenialEventto aggregator.l7/relay.rs:324-347): For policy-allowed endpoints withprotocolconfiguration, inspects HTTP method/path against L7 rules. Already hasEnforcementMode::Auditthat logs but doesn't block.landlock.rs:99-307): Applied at sandbox startup viarestrict_self(). Kernel-enforced, irrevocable.seccomp.rs:72-81): BPF filters applied at startup. Infrastructure integrity control.Code References
proxy.rs:522-523NetworkAction::AllowvsDenyproxy.rs:560-576NetworkActivityBuilderemission on denyproxy.rs:593DenialEventsent to aggregator on denyl7/mod.rs:54-61EnforcementMode::Audit | Enforceenum definitionl7/relay.rs:324-328denial_aggregator.rs:18-36DenialEventstruct with host, port, binary, L7 fieldsmechanistic_mapper.rs:59-242generate_proposals()— denial summaries to PolicyChunk proposalsopa.rs:241-297evaluate_network()— policy evaluation (unchanged in permissive mode)landlock.rs:99-307restrict_self()seccomp.rs:72-81openshell-cli/src/main.rs:1165-1297SandboxCommands::Createwith--policyflagopenshell-cli/src/run.rs:2521-2562sandbox get --policy-only— export effective policy as YAMLopenshell-cli/src/run.rs:7229-7318sandbox_draft_get()— display draft proposalsproto/sandbox.proto:317SandboxSpec.policyfieldWhat Would Need to Change
Sandbox runtime (proxy permissive bypass):
proxy.rs: OnNetworkAction::Denyin permissive mode, still emit OCSF event andDenialEvent, but allow the TCP connection instead of returning 403. Inject credentials normally. The OCSF event should use a distinct disposition (e.g.,DispositionId::LoggedorOtherwithunmapped: {"audit_mode": true}) to distinguish from actual denials.EnforcementMode::Auditin permissive mode.Sandbox runtime (Landlock skip):
landlock.rs: Skipprepare()andenforce()in permissive mode.Proto schema:
bool permissivetoSandboxSpec(orSandboxPolicy). This flag flows from CLI → server → sandbox.Denial aggregator:
audit_mode: booltoDenialEventto distinguish genuine denials from permissive-mode would-be-denials in mixed fleets.CLI — sandbox creation:
--permissiveflag tosandbox createandsandbox exec.protocoldeclarations, log a hint:Patterns to Follow
EnforcementMode::Auditin L7 relay (l7/relay.rs:324-347): Already implements log-but-don't-block. L4 permissive mode should follow the same pattern.DenialEvent+DenialAggregator(denial_aggregator.rs): Existing pipeline from deny → aggregate → gateway flush. Extend, don't replace.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
Proposed Approach
Add a
--permissiveflag tosandbox create/execthat 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
featDesign Decisions (Resolved)
--permissiveflag onsandbox create/exec. No new subcommand.--permissiveworks with zero policy (pure L4 discovery). A startup hint recommends adding protocol declarations for richer L7 learning.Risks & Open Questions
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.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
audit_mode: true.--permissive, make connection that would be denied, verify it succeeds with credentials, verify denial appears as draft proposal viapolicy draft get, approve, export viasandbox get --policy-only, verify exported policy covers the connection.opa.rs:1187+), L7 relay audit/enforce tests, mechanistic mapper tests, seccomp behavioral tests withfork().LSM Impact
Permissive mode does not introduce new Linux Security Module concerns:
/proc/<pid>/exevisibility for binary identity resolution is unaffected.Gateway Config Impact
The
--permissiveflag is a per-sandbox runtime flag flowing throughSandboxSpecin the proto, not throughgateway.toml. No changes todocs/reference/gateway-config.mdxneeded for the initial implementation.Created by spike investigation. Use
build-from-issueto plan and implement.