Skip to content
Merged
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
38 changes: 26 additions & 12 deletions crates/openshell-sandbox/data/sandbox-policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,25 @@ network_action := "allow" if {

default allow_request = false

# L7 request allowed if: L4 policy matches AND the specific endpoint's rules allow the request.
# Per-policy helper: true when this single policy has at least one endpoint
# matching the L4 request whose L7 rules also permit the specific request.
# Isolating the endpoint iteration inside a function avoids the regorus
# "duplicated definition of local variable" error that occurs when the
# outer `some name` iterates over multiple policies that share a host:port.
_policy_allows_l7(policy) if {
some ep
ep := policy.endpoints[_]
endpoint_matches_request(ep, input.network)
request_allowed_for_endpoint(input.request, ep)
}

# L7 request allowed if any matching L4 policy also allows the L7 request.
allow_request if {
some name
policy := data.network_policies[name]
endpoint_allowed(policy, input.network)
binary_allowed(policy, input.exec)
some ep
ep := policy.endpoints[_]
endpoint_matches_request(ep, input.network)
request_allowed_for_endpoint(input.request, ep)
_policy_allows_l7(policy)
}

# --- L7 deny reason ---
Expand Down Expand Up @@ -239,19 +248,24 @@ command_matches(actual, expected) if {
# Used by Rust to extract L7 config (protocol, tls, enforcement) and/or
# allowed_ips for SSRF allowlist validation.

# Collect all matching endpoint configs into an array to avoid complete-rule
# conflicts when multiple policies cover the same endpoint. Return the first.
_matching_endpoint_configs := [ep |
some name
policy := data.network_policies[name]
endpoint_allowed(policy, input.network)
binary_allowed(policy, input.exec)
# Per-policy helper: returns matching endpoint configs for a single policy.
_policy_endpoint_configs(policy) := [ep |
some ep
ep := policy.endpoints[_]
endpoint_matches_request(ep, input.network)
endpoint_has_extended_config(ep)
]

# Collect matching endpoint configs across all policies. Iterates over
# _matching_policy_names (a set, safe from regorus variable collisions)
# then collects per-policy configs via the helper function.
_matching_endpoint_configs := [cfg |
some pname
_matching_policy_names[pname]
cfgs := _policy_endpoint_configs(data.network_policies[pname])
cfg := cfgs[_]
]

matched_endpoint_config := _matching_endpoint_configs[0] if {
count(_matching_endpoint_configs) > 0
}
Expand Down
86 changes: 86 additions & 0 deletions crates/openshell-sandbox/src/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,92 @@ process:
assert_eq!(val, regorus::Value::from(true));
}

// ========================================================================
// Overlapping policies (duplicate host:port) — regression tests
// ========================================================================

/// Two network_policies entries covering the same host:port with L7 rules.
/// Before the fix, this caused regorus to fail with
/// "duplicated definition of local variable ep" in allow_request.
const OVERLAPPING_L7_TEST_DATA: &str = r#"
network_policies:
test_server:
name: test_server
endpoints:
- host: 192.168.1.100
port: 8567
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: "**"
binaries:
- { path: /usr/bin/curl }
allow_192_168_1_100_8567:
name: allow_192_168_1_100_8567
endpoints:
- host: 192.168.1.100
port: 8567
protocol: rest
enforcement: enforce
allowed_ips:
- 192.168.1.100
rules:
- allow:
method: GET
path: "**"
binaries:
- { path: /usr/bin/curl }
filesystem_policy:
include_workdir: true
read_only: []
read_write: []
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
"#;

#[test]
fn l7_overlapping_policies_allow_request_does_not_crash() {
let engine = OpaEngine::from_strings(TEST_POLICY, OVERLAPPING_L7_TEST_DATA)
.expect("engine should load overlapping data");
let input = l7_input("192.168.1.100", 8567, "GET", "/test");
// Should not panic or error — must evaluate to true.
assert!(eval_l7(&engine, &input));
}

#[test]
fn l7_overlapping_policies_deny_request_does_not_crash() {
let engine = OpaEngine::from_strings(TEST_POLICY, OVERLAPPING_L7_TEST_DATA)
.expect("engine should load overlapping data");
let input = l7_input("192.168.1.100", 8567, "DELETE", "/test");
// DELETE is not in the rules, so should deny — but must not crash.
assert!(!eval_l7(&engine, &input));
}

#[test]
fn overlapping_policies_endpoint_config_returns_result() {
let engine = OpaEngine::from_strings(TEST_POLICY, OVERLAPPING_L7_TEST_DATA)
.expect("engine should load overlapping data");
let input = NetworkInput {
host: "192.168.1.100".into(),
port: 8567,
binary_path: PathBuf::from("/usr/bin/curl"),
binary_sha256: String::new(),
ancestors: vec![],
cmdline_paths: vec![],
};
// Should return config from one of the entries without error.
let config = engine.query_endpoint_config(&input).unwrap();
assert!(
config.is_some(),
"Expected endpoint config for overlapping policies"
);
}

// ========================================================================
// network_action tests
// ========================================================================
Expand Down
Loading
Loading