Skip to content
Open
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
6 changes: 6 additions & 0 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ 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.

Endpoint `tls`, `enforcement`, and `access` use protobuf enums and retain their
named YAML spellings. `protocol` remains a string so the supported protocol set
can evolve, but every ingress validates it before persistence or activation.
The supervisor also refuses unknown enum numbers and protocol values
defensively; an unrecognized enforcement value never falls back to audit.

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
Expand Down
18 changes: 13 additions & 5 deletions crates/openshell-cli/src/policy_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,10 @@ fn parse_add_endpoint_spec(spec: &str) -> Result<NetworkEndpoint> {
port,
ports: vec![port],
protocol: protocol.to_string(),
enforcement: enforcement.to_string(),
access: access.to_string(),
enforcement: openshell_policy::network_enforcement_mode_from_str(enforcement)
.expect("validated enforcement") as i32,
access: openshell_policy::network_access_preset_from_str(access).expect("validated access")
as i32,
..Default::default()
};
apply_add_endpoint_options(spec, &mut endpoint, options)?;
Expand Down Expand Up @@ -548,8 +550,14 @@ mod tests {
let endpoint = &rule.endpoints[0];
assert_eq!(endpoint.host, "realtime.example.com");
assert_eq!(endpoint.protocol, "websocket");
assert_eq!(endpoint.access, "read-write");
assert_eq!(endpoint.enforcement, "enforce");
assert_eq!(
endpoint.access,
openshell_core::proto::NetworkAccessPreset::ReadWrite as i32
);
assert_eq!(
endpoint.enforcement,
openshell_core::proto::NetworkEnforcementMode::Enforce as i32
);
}

#[test]
Expand All @@ -569,7 +577,7 @@ mod tests {
panic!("expected add-rule preview");
};
assert_eq!(rule.endpoints[0].protocol, "tcp");
assert!(rule.endpoints[0].access.is_empty());
assert_eq!(rule.endpoints[0].access, 0);
}

#[test]
Expand Down
9 changes: 6 additions & 3 deletions crates/openshell-cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5942,8 +5942,11 @@ fn format_endpoint(endpoint: &openshell_core::proto::NetworkEndpoint) -> String
};
tags.push(layer_tag.to_string());

if !endpoint.access.is_empty() {
tags.push(format!("access={}", endpoint.access));
if endpoint.access != 0 {
tags.push(format!(
"access={}",
openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown")
));
}

for r in &endpoint.rules {
Expand Down Expand Up @@ -6878,7 +6881,7 @@ mod tests {
host: "host.example.test".to_string(),
port: 443,
protocol: "rest".to_string(),
access: "read-only".to_string(),
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
..Default::default()
};
assert_eq!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ impl OpenShell for TestOpenShell {
host: "api.user.example.com".to_string(),
port: 443,
protocol: "rest".to_string(),
enforcement: "enforce".to_string(),
access: "read-only".to_string(),
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce
as i32,
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
..Default::default()
}],
..Default::default()
Expand All @@ -204,8 +205,9 @@ impl OpenShell for TestOpenShell {
host: "api.provider.example.com".to_string(),
port: 443,
protocol: "rest".to_string(),
enforcement: "enforce".to_string(),
access: "read-only".to_string(),
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce
as i32,
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
..Default::default()
}],
..Default::default()
Expand Down Expand Up @@ -459,8 +461,8 @@ impl OpenShell for TestOpenShell {
host: "api.example.com".to_string(),
port: 443,
protocol: "rest".to_string(),
enforcement: "enforce".to_string(),
access: "read-only".to_string(),
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32,
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
..Default::default()
}],
..Default::default()
Expand Down
25 changes: 11 additions & 14 deletions crates/openshell-driver-mxc/src/policy_map/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,27 +506,25 @@ fn report_endpoint_l7_losses(endpoint: &NetworkEndpoint, path: &str, items: &mut
);
}

if !endpoint.tls.is_empty() {
let severity = if endpoint.tls == "skip" {
"warning"
} else {
"error"
};
if endpoint.tls != 0 {
let tls = openshell_policy::network_tls_mode_to_str(endpoint.tls).unwrap_or("unknown");
let severity = if tls == "skip" { "warning" } else { "error" };
add_loss(
items,
&format!("{path}.tls"),
severity,
&format!(
"MXC has no OpenShell TLS inspection mode equivalent for '{}'.",
endpoint.tls
tls
),
"TLS inspection mode",
"MXC network policy is host-level only.",
);
}

if !endpoint.enforcement.is_empty() {
if endpoint.enforcement == "audit" {
if endpoint.enforcement != 0 {
if openshell_policy::network_enforcement_mode_to_str(endpoint.enforcement) == Some("audit")
{
add_loss(
items,
&format!("{path}.enforcement"),
Expand All @@ -547,15 +545,14 @@ fn report_endpoint_l7_losses(endpoint: &NetworkEndpoint, path: &str, items: &mut
}
}

if !endpoint.access.is_empty() {
if endpoint.access != 0 {
let access =
openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown");
add_loss(
items,
&format!("{path}.access"),
"error",
&format!(
"MXC has no access preset equivalent for '{}'.",
endpoint.access
),
&format!("MXC has no access preset equivalent for '{}'.", access),
"REST/WebSocket/GraphQL access preset",
"MXC cannot enforce method or operation-level access.",
);
Expand Down
16 changes: 8 additions & 8 deletions crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ fn b_tls_skip_warning() {
"r",
NetworkEndpoint {
host: "api.example.com".into(),
tls: "skip".into(),
tls: openshell_core::proto::NetworkTlsMode::Skip as i32,
..Default::default()
},
);
Expand All @@ -459,7 +459,7 @@ fn b_tls_non_skip_error() {
"r",
NetworkEndpoint {
host: "api.example.com".into(),
tls: "terminate".into(),
tls: openshell_core::proto::NetworkTlsMode::Terminate as i32,
..Default::default()
},
);
Expand All @@ -474,7 +474,7 @@ fn b_enforcement_audit_error() {
"r",
NetworkEndpoint {
host: "api.example.com".into(),
enforcement: "audit".into(),
enforcement: openshell_core::proto::NetworkEnforcementMode::Audit as i32,
..Default::default()
},
);
Expand All @@ -494,7 +494,7 @@ fn b_enforcement_non_audit_warning() {
"r",
NetworkEndpoint {
host: "api.example.com".into(),
enforcement: "enforce".into(),
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32,
..Default::default()
},
);
Expand All @@ -514,7 +514,7 @@ fn b_access_error() {
"r",
NetworkEndpoint {
host: "api.example.com".into(),
access: "read-only".into(),
access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32,
..Default::default()
},
);
Expand Down Expand Up @@ -1200,9 +1200,9 @@ fn handled_fields_inventory() {
// Two ports → serializes as `ports: [80, 443]` (array form).
ports: vec![80, 443],
protocol: "graphql".into(),
tls: "skip".into(),
enforcement: "enforce".into(),
access: "full".into(),
tls: openshell_core::proto::NetworkTlsMode::Skip as i32,
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32,
access: openshell_core::proto::NetworkAccessPreset::Full as i32,
allowed_ips: vec!["10.0.0.1".into()],
allow_encoded_slash: true,
websocket_credential_rewrite: true,
Expand Down
60 changes: 34 additions & 26 deletions crates/openshell-policy/src/ambiguity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

//! Validation for endpoint selectors whose policy-derived behavior conflicts.

use openshell_core::proto::{NetworkEndpoint, SandboxPolicy};
use openshell_core::proto::{
NetworkEndpoint, NetworkEnforcementMode, NetworkTlsMode, SandboxPolicy,
};
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::fmt;

Expand Down Expand Up @@ -172,8 +174,8 @@ fn connection_conflicts(left: &NetworkEndpoint, right: &NetworkEndpoint) -> Vec<
push_conflict(
&mut conflicts,
"tls",
&normalized_tls(&left.tls),
&normalized_tls(&right.tls),
&normalized_tls(left.tls),
&normalized_tls(right.tls),
);
push_conflict(
&mut conflicts,
Expand All @@ -196,7 +198,7 @@ fn is_explicit_tcp(protocol: &str) -> bool {
fn endpoint_contributes_request_pipeline_metadata(endpoint: &NetworkEndpoint) -> bool {
(!endpoint.protocol.is_empty() && !endpoint.protocol.eq_ignore_ascii_case("tcp"))
|| !endpoint.allowed_ips.is_empty()
|| !endpoint.tls.is_empty()
|| endpoint.tls != NetworkTlsMode::Unspecified as i32
|| endpoint.credential_binding.is_some()
}

Expand All @@ -211,8 +213,8 @@ fn request_pipeline_conflicts(left: &NetworkEndpoint, right: &NetworkEndpoint) -
push_conflict(
&mut conflicts,
"enforcement",
&normalized_enforcement(&left.enforcement),
&normalized_enforcement(&right.enforcement),
&normalized_enforcement(left.enforcement),
&normalized_enforcement(right.enforcement),
);
push_conflict(
&mut conflicts,
Expand Down Expand Up @@ -343,19 +345,23 @@ fn push_conflict<T: fmt::Debug + PartialEq>(
}
}

fn normalized_tls(value: &str) -> &'static str {
if value.eq_ignore_ascii_case("skip") {
"skip"
fn normalized_tls(value: i32) -> i32 {
if value == NetworkTlsMode::Skip as i32 {
NetworkTlsMode::Skip as i32
} else if NetworkTlsMode::try_from(value).is_ok() {
NetworkTlsMode::Unspecified as i32
} else {
"auto"
value
}
}

fn normalized_enforcement(value: &str) -> &'static str {
if value.eq_ignore_ascii_case("enforce") {
"enforce"
fn normalized_enforcement(value: i32) -> i32 {
if value == NetworkEnforcementMode::Enforce as i32 {
NetworkEnforcementMode::Enforce as i32
} else if NetworkEnforcementMode::try_from(value).is_ok() {
NetworkEnforcementMode::Audit as i32
} else {
"audit"
value
}
}

Expand Down Expand Up @@ -707,7 +713,9 @@ fn complement_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
#[cfg(test)]
mod tests {
use super::*;
use openshell_core::proto::{L7Allow, L7Rule, NetworkBinary, NetworkPolicyRule};
use openshell_core::proto::{
L7Allow, L7Rule, NetworkAccessPreset, NetworkBinary, NetworkPolicyRule,
};

fn endpoint(host: &str, port: u32) -> NetworkEndpoint {
NetworkEndpoint {
Expand Down Expand Up @@ -764,7 +772,7 @@ mod tests {
#[test]
fn disjoint_ports_do_not_overlap() {
let mut left = endpoint("api.example.com", 443);
left.tls = "skip".to_string();
left.tls = NetworkTlsMode::Skip as i32;
let right = endpoint("api.example.com", 8443);
assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty());
}
Expand All @@ -773,10 +781,10 @@ mod tests {
fn compatible_request_rules_may_overlap() {
let mut left = endpoint("api.example.com", 443);
left.protocol = "rest".to_string();
left.tls = "skip".to_string();
left.tls = NetworkTlsMode::Skip as i32;
let mut right = left.clone();
left.access = "read-only".to_string();
right.access = "read-write".to_string();
left.access = NetworkAccessPreset::ReadOnly as i32;
right.access = NetworkAccessPreset::ReadWrite as i32;

assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty());
}
Expand All @@ -795,7 +803,7 @@ mod tests {
let left = endpoint("api.example.com", 443);
let mut right = endpoint("api.example.com", 443);
right.protocol = "rest".to_string();
right.enforcement = "enforce".to_string();
right.enforcement = NetworkEnforcementMode::Enforce as i32;

assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty());
}
Expand Down Expand Up @@ -868,19 +876,19 @@ mod tests {
fn more_specific_path_may_override_request_pipeline_metadata() {
let mut left = endpoint("api.example.com", 443);
left.protocol = "rest".to_string();
left.enforcement = "enforce".to_string();
left.enforcement = NetworkEnforcementMode::Enforce as i32;
let mut right = endpoint("api.example.com", 443);
right.path = "/graphql".to_string();
right.protocol = "graphql".to_string();
right.enforcement = "enforce".to_string();
right.enforcement = NetworkEnforcementMode::Enforce as i32;

assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty());
}

#[test]
fn exact_wildcard_tls_conflict_is_rejected() {
let mut left = endpoint("*.example.com", 443);
left.tls = "skip".to_string();
left.tls = NetworkTlsMode::Skip as i32;
let right = endpoint("api.example.com", 443);
let ambiguities = find_endpoint_ambiguities(&policy_with(left, right));

Expand Down Expand Up @@ -1040,7 +1048,7 @@ mod tests {
#[test]
fn different_binary_lists_do_not_hide_endpoint_ambiguity() {
let mut left = endpoint("api.example.com", 443);
left.tls = "skip".to_string();
left.tls = NetworkTlsMode::Skip as i32;
let right = endpoint("api.example.com", 443);

assert_eq!(
Expand All @@ -1053,9 +1061,9 @@ mod tests {
fn explicit_tcp_and_omitted_protocol_are_ambiguous_for_native_tcp_eligibility() {
let mut explicit_tcp = endpoint("api.example.com", 443);
explicit_tcp.protocol = "tcp".to_string();
explicit_tcp.tls = "skip".to_string();
explicit_tcp.tls = NetworkTlsMode::Skip as i32;
let mut omitted = endpoint("api.example.com", 443);
omitted.tls = "skip".to_string();
omitted.tls = NetworkTlsMode::Skip as i32;

let ambiguities = find_endpoint_ambiguities(&policy_with(explicit_tcp, omitted));
assert_eq!(ambiguities.len(), 1);
Expand Down
6 changes: 3 additions & 3 deletions crates/openshell-policy/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ mod tests {
host: host.to_string(),
port: 443,
protocol: "rest".to_string(),
tls: String::new(),
enforcement: "enforce".to_string(),
access: "read-write".to_string(),
tls: 0,
enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32,
access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32,
rules: Vec::new(),
allowed_ips: Vec::new(),
ports: Vec::new(),
Expand Down
Loading
Loading