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
27 changes: 18 additions & 9 deletions crates/openshell-sandbox/data/sandbox-policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ deny_reason := reason if {
input.network
input.exec
not network_policy_for_request
endpoint_misses := [r |
some name
policy := data.network_policies[name]
not endpoint_allowed(policy, input.network)
r := sprintf("endpoint %s:%d not in policy '%s'", [input.network.host, input.network.port, name])
]
not endpoint_policy_for_request
count(data.network_policies) > 0
reason := sprintf("endpoint %s:%d is not allowed by any policy", [input.network.host, input.network.port])
}

deny_reason := reason if {
input.network
input.exec
not network_policy_for_request
endpoint_policy_for_request
ancestors_str := concat(" -> ", input.exec.ancestors)
cmdline_str := concat(", ", input.exec.cmdline_paths)
binary_misses := [r |
Expand All @@ -49,9 +53,8 @@ deny_reason := reason if {
not binary_allowed(policy, input.exec)
r := sprintf("binary '%s' not allowed in policy '%s' (ancestors: [%s], cmdline: [%s]). SYMLINK HINT: the binary path is the kernel-resolved target from /proc/<pid>/exe, not the symlink. If your policy specifies a symlink (e.g., /usr/bin/python3) but the actual binary is /usr/bin/python3.11, either: (1) use the canonical path in your policy (run 'readlink -f /usr/bin/python3' inside the sandbox), or (2) ensure symlink resolution is working (check sandbox logs for 'Cannot access container filesystem')", [input.exec.path, name, ancestors_str, cmdline_str])
]
all_reasons := array.concat(endpoint_misses, binary_misses)
count(all_reasons) > 0
reason := concat("; ", all_reasons)
count(binary_misses) > 0
reason := concat("; ", binary_misses)
}

deny_reason := "network connections not allowed by policy" if {
Expand Down Expand Up @@ -91,6 +94,12 @@ network_policy_for_request if {
binary_allowed(data.network_policies[name], input.exec)
}

endpoint_policy_for_request if {
some name
data.network_policies[name]
endpoint_allowed(data.network_policies[name], input.network)
}

# Endpoint matching: exact host (case-insensitive) + port in ports list.
endpoint_allowed(policy, network) if {
some endpoint
Expand Down
19 changes: 19 additions & 0 deletions crates/openshell-sandbox/src/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4585,6 +4585,25 @@ network_policies:
);
}

#[test]
fn deny_reason_collapses_endpoint_misses() {
let engine = test_engine();
let input = NetworkInput {
host: "not-configured.example.com".into(),
port: 443,
binary_path: PathBuf::from("/usr/local/bin/claude"),
binary_sha256: "unused".into(),
ancestors: vec![],
cmdline_paths: vec![],
};
let decision = engine.evaluate_network(&input).unwrap();
assert!(!decision.allowed);
assert_eq!(
decision.reason,
"endpoint not-configured.example.com:443 is not allowed by any policy"
);
}

/// Check if symlink resolution through `/proc/<pid>/root/` actually works.
/// Creates a real symlink in a tempdir and attempts to resolve it via
/// the procfs root path. This catches environments where the probe path
Expand Down
Loading