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
15 changes: 15 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ paths, such as proxy support files or GPU device paths when a GPU is present.
All ordinary agent egress is routed through the sandbox proxy. The proxy
identifies the calling binary, checks trust-on-first-use binary identity, rejects
unsafe internal destinations, and evaluates the active policy.

CONNECT and absolute-form forward HTTP are explicit-proxy adapters over the same
egress pipeline. Each adapter normalizes its request into an egress intent, and
the shared authorization result carries the process evidence used by destination
validation and relay selection. During the compatibility migration, endpoint
state is hydrated at the adapters' existing policy query points; it is not yet
one atomic, generation-consistent authorization result. Destination validation
returns an unopened connector so adapters retain their existing response and
upstream-dial timing. CONNECT prepares a generation-pinned relay context before
entering shared TLS-terminated or plaintext HTTP relays; non-HTTP traffic uses
the shared raw byte relay after the existing adapter gates. Forward HTTP retains
its guarded single-request relay while sharing authorization, request context,
policy-pinning, and destination boundaries.
Adapter-specific response and OCSF event shapes remain at the protocol boundary.

For inspected HTTP traffic, the proxy can enforce REST method/path rules,
WebSocket upgrade and text-message rules, GraphQL operation rules, and
MCP method, tool, and supported params rules or generic JSON-RPC method rules
Expand Down
28 changes: 21 additions & 7 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ 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
reloads affect the next connection or the next parsed HTTP request; they do not
rewrite bytes already being relayed. HTTP upgrades switch to raw relay by
default. A `protocol: rest` endpoint can opt in to
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.
Expand All @@ -91,10 +91,24 @@ 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.

If a new policy fails validation or loading, the supervisor reports the failure
and keeps the last-known-good policy. Static controls, such as filesystem
allowlists and process identity, require a new sandbox because they are applied
before the child process starts.
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.

The `[openshell.gateway] policy_validation_failure_mode` configuration controls
rejected generations. It 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.
Expand Down
63 changes: 60 additions & 3 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,43 @@ pub const DEFAULT_DOCKER_NETWORK_NAME: &str = "openshell-docker";
/// Default domain used for browser-facing sandbox service URLs.
pub const DEFAULT_SERVICE_ROUTING_DOMAIN: &str = "openshell.localhost";

/// Gateway posture when a sandbox rejects a candidate policy generation.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyValidationFailureMode {
/// Deactivate the previous policy and deny new egress until a valid
/// generation is loaded.
#[default]
FailClosed,
/// Keep the last valid generation active when a newer candidate fails
/// validation. Startup still fails closed when no valid generation exists.
RetainLastValid,
}

impl PolicyValidationFailureMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::FailClosed => "fail_closed",
Self::RetainLastValid => "retain_last_valid",
}
}
}

impl FromStr for PolicyValidationFailureMode {
type Err = String;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"fail_closed" => Ok(Self::FailClosed),
"retain_last_valid" => Ok(Self::RetainLastValid),
_ => Err(format!(
"invalid policy validation failure mode '{value}'; expected fail_closed or retain_last_valid"
)),
}
}
}

/// Default OCI repository for the supervisor image (no tag).
pub const DEFAULT_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/supervisor";

Expand Down Expand Up @@ -386,6 +423,9 @@ pub struct Config {
/// Log level (trace, debug, info, warn, error).
pub log_level: String,

/// Security posture for rejected sandbox policy generations.
pub policy_validation_failure_mode: PolicyValidationFailureMode,

/// TLS configuration. When `None`, the server listens on plaintext HTTP.
pub tls: Option<TlsConfig>,

Expand Down Expand Up @@ -727,6 +767,7 @@ impl Config {
health_bind_address: None,
metrics_bind_address: None,
log_level: default_log_level(),
policy_validation_failure_mode: PolicyValidationFailureMode::default(),
tls,
oidc: None,
auth: GatewayAuthConfig::default(),
Expand Down Expand Up @@ -971,9 +1012,10 @@ mod tests {
use super::{
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayInterceptorBindingPolicy,
GatewayInterceptorConfig, GatewayInterceptorFailurePolicy, GatewayJwtConfig,
GatewayProviderProfileSourceConfig, detect_docker_socket_from_candidates, detect_driver,
docker_host_unix_socket_path, docker_socket_responds, is_unix_socket,
normalize_compute_driver_name, podman_socket_candidates_from_env, podman_socket_responds,
GatewayProviderProfileSourceConfig, PolicyValidationFailureMode,
detect_docker_socket_from_candidates, detect_driver, docker_host_unix_socket_path,
docker_socket_responds, is_unix_socket, normalize_compute_driver_name,
podman_socket_candidates_from_env, podman_socket_responds,
};
#[cfg(unix)]
use std::io::{Read as _, Write as _};
Expand Down Expand Up @@ -1009,6 +1051,21 @@ mod tests {
assert!(err.contains("unsupported compute driver 'firecracker'"));
}

#[test]
fn policy_validation_failure_mode_is_secure_by_default() {
assert_eq!(
Config::new(None).policy_validation_failure_mode,
PolicyValidationFailureMode::FailClosed
);
assert_eq!(
"retain_last_valid"
.parse::<PolicyValidationFailureMode>()
.unwrap(),
PolicyValidationFailureMode::RetainLastValid
);
assert!("keep_old".parse::<PolicyValidationFailureMode>().is_err());
}

#[test]
fn compute_driver_name_normalization_accepts_builtin_and_custom_names() {
assert_eq!(normalize_compute_driver_name(" VM ").unwrap(), "vm");
Expand Down
37 changes: 37 additions & 0 deletions crates/openshell-core/src/grpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,8 @@ pub struct SettingsPollResult {
pub global_policy_version: u32,
pub provider_env_revision: u64,
pub supervisor_middleware_services: Vec<crate::proto::SupervisorMiddlewareService>,
/// Gateway-configured posture for rejected policy generations.
pub policy_validation_failure_mode: crate::PolicyValidationFailureMode,
}

fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> SettingsPollResult {
Expand All @@ -752,6 +754,41 @@ fn settings_poll_result(inner: crate::proto::GetSandboxConfigResponse) -> Settin
global_policy_version: inner.global_policy_version,
provider_env_revision: inner.provider_env_revision,
supervisor_middleware_services: inner.supervisor_middleware_services,
policy_validation_failure_mode: inner
.policy_validation_failure_mode
.parse()
.unwrap_or_default(),
}
}

#[cfg(test)]
mod settings_poll_tests {
use super::settings_poll_result;
use crate::PolicyValidationFailureMode;
use crate::proto::GetSandboxConfigResponse;

#[test]
fn validation_failure_mode_round_trips_from_gateway_config() {
let result = settings_poll_result(GetSandboxConfigResponse {
policy_validation_failure_mode: "retain_last_valid".to_string(),
..Default::default()
});
assert_eq!(
result.policy_validation_failure_mode,
PolicyValidationFailureMode::RetainLastValid
);
}

#[test]
fn unknown_validation_failure_mode_fails_closed() {
let result = settings_poll_result(GetSandboxConfigResponse {
policy_validation_failure_mode: "future_mode".to_string(),
..Default::default()
});
assert_eq!(
result.policy_validation_failure_mode,
PolicyValidationFailureMode::FailClosed
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub use config::{
ComputeDriverKind, Config, GatewayAuthConfig, GatewayInterceptorBindingOverride,
GatewayInterceptorBindingPolicy, GatewayInterceptorConfig, GatewayInterceptorFailurePolicy,
GatewayInterceptorPhaseConfig, GatewayJwtConfig, GatewayProviderProfileSourceConfig,
MtlsAuthConfig, OidcConfig, TlsConfig,
MtlsAuthConfig, OidcConfig, PolicyValidationFailureMode, TlsConfig,
};
pub use error::{ComputeDriverError, Error, Result};
pub use metadata::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, SetResourceVersion};
Expand Down
Loading
Loading