Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/openshell-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,14 @@ enum SandboxCommands {
#[arg(long = "env", value_name = "KEY=VALUE")]
envs: Vec<String>,

/// Run in permissive mode: log but do not enforce network policy denials.
/// Connections that would be blocked are allowed through and logged as
/// draft policy proposals. Filesystem (Landlock) restrictions are skipped.
/// Seccomp and process-identity controls remain enforced.
/// Use with --policy to provide protocol hints for richer L7 learning.
#[arg(long)]
permissive: bool,

/// Approval mode for agent-authored policy proposals.
///
/// `manual` (default): every proposal lands in the draft inbox for
Expand Down Expand Up @@ -2622,6 +2630,7 @@ async fn main() -> Result<()> {
no_auto_providers,
labels,
envs,
permissive,
approval_mode,
command,
} => {
Expand Down Expand Up @@ -2709,6 +2718,7 @@ async fn main() -> Result<()> {
labels: labels_map,
environment: env_map,
approval_mode: &approval_mode,
permissive,
},
&tls,
))
Expand Down
25 changes: 25 additions & 0 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,7 @@ pub struct SandboxCreateConfig<'a> {
pub labels: HashMap<String, String>,
pub environment: HashMap<String, String>,
pub approval_mode: &'a str,
pub permissive: bool,
}

impl Default for SandboxCreateConfig<'_> {
Expand All @@ -1812,6 +1813,7 @@ impl Default for SandboxCreateConfig<'_> {
labels: HashMap::new(),
environment: HashMap::new(),
approval_mode: "manual",
permissive: false,
}
}
}
Expand Down Expand Up @@ -1842,6 +1844,7 @@ pub async fn sandbox_create(
labels,
environment,
approval_mode,
permissive,
} = config;

if editor.is_some() && !command.is_empty() {
Expand Down Expand Up @@ -1899,6 +1902,27 @@ pub async fn sandbox_create(
.await?;

let policy = load_sandbox_policy(policy)?;

if permissive {
let has_protocol_endpoints = policy.as_ref().is_some_and(|p| {
p.network_policies
.values()
.any(|rule| rule.endpoints.iter().any(|ep| !ep.protocol.is_empty()))
});
if !has_protocol_endpoints {
eprintln!(
"\n{} Permissive mode: no HTTP endpoints declared in policy.\n \
L7 audit will be limited to host:port only.\n \
To capture HTTP method/path detail, add endpoint protocol hints:\n\n \
endpoints:\n \
- host: api.example.com\n \
port: 443\n \
protocol: rest\n",
"⚠".yellow(),
);
}
}

let resource_limits = build_sandbox_resource_limits(cpu, memory)?;
let driver_config = driver_config_json
.map(parse_driver_config_json)
Expand All @@ -1924,6 +1948,7 @@ pub async fn sandbox_create(
policy,
providers: configured_providers,
template,
permissive,
..SandboxSpec::default()
}),
name: name.unwrap_or_default().to_string(),
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-core/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,8 @@ pub struct SettingsPollResult {
/// When `policy_source` is `Global`, the version of the global policy revision.
pub global_policy_version: u32,
pub provider_env_revision: u64,
/// When true, the sandbox logs but does not enforce network policy denials.
pub permissive: bool,
}

fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> SettingsPollResult {
Expand All @@ -762,6 +764,7 @@ fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> Settin
settings: inner.settings,
global_policy_version: inner.global_policy_version,
provider_env_revision: inner.provider_env_revision,
permissive: inner.permissive,
}
}

Expand Down
7 changes: 6 additions & 1 deletion crates/openshell-core/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pub enum NetworkMode {
pub struct ProxyPolicy {
/// TCP address for a local HTTP proxy (loopback-only).
pub http_addr: Option<SocketAddr>,
/// When true, log but do not enforce policy denials (audit2allow mode).
pub permissive: bool,
}

#[derive(Debug, Clone, Default)]
Expand Down Expand Up @@ -104,7 +106,10 @@ impl TryFrom<ProtoSandboxPolicy> for SandboxPolicy {
// can be evaluated by OPA and `inference.local` is always addressable.
let network = NetworkPolicy {
mode: NetworkMode::Proxy,
proxy: Some(ProxyPolicy { http_addr: None }),
proxy: Some(ProxyPolicy {
http_addr: None,
permissive: false,
}),
};

Ok(Self {
Expand Down
31 changes: 26 additions & 5 deletions crates/openshell-sandbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,10 @@ fn process_policy_for_topology(
let proxy = process_policy
.network
.proxy
.get_or_insert(ProxyPolicy { http_addr: None });
.get_or_insert(ProxyPolicy {
http_addr: None,
permissive: false,
});
if proxy.http_addr.is_none() {
proxy.http_addr = Some(SIDECAR_PROCESS_PROXY_ADDR.parse().into_diagnostic()?);
}
Expand Down Expand Up @@ -1656,7 +1659,10 @@ mod baseline_tests {
},
network: NetworkPolicy {
mode: NetworkMode::Proxy,
proxy: Some(ProxyPolicy { http_addr: None }),
proxy: Some(ProxyPolicy {
http_addr: None,
permissive: false,
}),
},
landlock: LandlockPolicy::default(),
process: ProcessPolicy::default(),
Expand Down Expand Up @@ -1811,7 +1817,10 @@ async fn load_policy(
filesystem: config.filesystem,
network: NetworkPolicy {
mode: NetworkMode::Proxy,
proxy: Some(ProxyPolicy { http_addr: None }),
proxy: Some(ProxyPolicy {
http_addr: None,
permissive: false,
}),
},
landlock: config.landlock,
process: config.process,
Expand Down Expand Up @@ -1943,14 +1952,22 @@ async fn load_policy(
}
};

let policy = match SandboxPolicy::try_from(proto_policy.clone()) {
let mut policy = match SandboxPolicy::try_from(proto_policy.clone()) {
Ok(policy) => policy,
Err(e) => {
report_initial_policy_failure(endpoint, id, loaded_policy_revision.as_ref(), &e)
.await;
return Err(e);
}
};
if snapshot.permissive {
if let Some(ref mut proxy) = policy.network.proxy {
proxy.permissive = true;
}
info!(
"Sandbox running in permissive mode: policy denials will be logged but not enforced"
);
}
return Ok((
policy,
opa_engine,
Expand Down Expand Up @@ -2761,7 +2778,10 @@ mod tests {
filesystem: FilesystemPolicy::default(),
network: NetworkPolicy {
mode: NetworkMode::Proxy,
proxy: Some(ProxyPolicy { http_addr }),
proxy: Some(ProxyPolicy {
http_addr,
permissive: false,
}),
},
landlock: LandlockPolicy::default(),
process: ProcessPolicy::default(),
Expand Down Expand Up @@ -3008,6 +3028,7 @@ filesystem_policy:
settings: std::collections::HashMap::new(),
global_policy_version: 0,
provider_env_revision: 0,
permissive: false,
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-server/src/grpc/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,8 @@ pub(super) async fn handle_get_sandbox_config(
let provider_env_revision =
compute_provider_env_revision(state.store.as_ref(), &sandbox_provider_names).await?;

let permissive = sandbox.spec.as_ref().is_some_and(|s| s.permissive);

Ok(Response::new(GetSandboxConfigResponse {
policy,
version,
Expand All @@ -1260,6 +1262,7 @@ pub(super) async fn handle_get_sandbox_config(
policy_source: policy_source.into(),
global_policy_version,
provider_env_revision,
permissive,
}))
}

Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-supervisor-network/src/l7/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,8 @@ network_policies:
activity_tx: None,
dynamic_credentials: None,
token_grant_resolver: None,
permissive: false,
denial_tx: None,
};
let request_info = crate::l7::L7RequestInfo {
action: req.action,
Expand Down
Loading
Loading