Skip to content

Commit 30bb3e7

Browse files
committed
fix(policy): require explicit mcp methods by default
Signed-off-by: Kris Hicks <khicks@nvidia.com>
1 parent 156c26b commit 30bb3e7

9 files changed

Lines changed: 48 additions & 36 deletions

File tree

crates/openshell-policy/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile {
750750
|| e.mcp
751751
.as_ref()
752752
.and_then(|options| options.allow_all_known_mcp_methods)
753-
.unwrap_or(true);
753+
.unwrap_or(false);
754754
let rules = e
755755
.rules
756756
.iter()

crates/openshell-supervisor-network/data/sandbox-policy.rego

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ jsonrpc_family_endpoint(endpoint) if {
494494
}
495495

496496
mcp_allow_all_known_mcp_methods(endpoint) if {
497-
object.get(endpoint, "mcp_allow_all_known_mcp_methods", true)
497+
object.get(endpoint, "mcp_allow_all_known_mcp_methods", false)
498498
}
499499

500500
mcp_tool_call_narrowed_by_policy(endpoint, method) if {

crates/openshell-supervisor-network/src/l7/mod.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,7 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
10101010
if jsonrpc_family && !access.is_empty() {
10111011
if protocol == "mcp" {
10121012
errors.push(format!(
1013-
"{loc}: protocol {protocol} does not support access presets; use rules/deny_rules or omit rules for the default allow-all MCP policy"
1013+
"{loc}: protocol {protocol} does not support access presets; use rules/deny_rules or set mcp.allow_all_known_mcp_methods: true for an allow-all MCP policy"
10141014
));
10151015
} else {
10161016
errors.push(format!(
@@ -1128,7 +1128,7 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
11281128
let mcp_allow_all_known_mcp_methods = ep
11291129
.get("mcp_allow_all_known_mcp_methods")
11301130
.and_then(serde_json::Value::as_bool)
1131-
.unwrap_or(true);
1131+
.unwrap_or(false);
11321132
if protocol == "mcp"
11331133
&& !has_rules
11341134
&& access.is_empty()
@@ -1470,7 +1470,7 @@ pub fn expand_access_presets(data: &mut serde_json::Value) {
14701470
let mcp_allow_all_known_mcp_methods = ep
14711471
.get("mcp_allow_all_known_mcp_methods")
14721472
.and_then(serde_json::Value::as_bool)
1473-
.unwrap_or(true);
1473+
.unwrap_or(false);
14741474

14751475
if protocol == "mcp"
14761476
&& access.is_empty()
@@ -1992,6 +1992,7 @@ mod tests {
19921992
"port": 443,
19931993
"path": "/mcp",
19941994
"protocol": "mcp",
1995+
"mcp_allow_all_known_mcp_methods": true,
19951996
"rules": [{
19961997
"allow": {
19971998
"tool": { "any": ["read_status", "submit_*"] }
@@ -2055,7 +2056,7 @@ mod tests {
20552056
}
20562057

20572058
#[test]
2058-
fn validate_mcp_tool_selectors_require_method_when_method_profile_disabled() {
2059+
fn validate_mcp_tool_selectors_require_method_by_default() {
20592060
let data = serde_json::json!({
20602061
"network_policies": {
20612062
"test": {
@@ -2064,7 +2065,6 @@ mod tests {
20642065
"port": 443,
20652066
"path": "/mcp",
20662067
"protocol": "mcp",
2067-
"mcp_allow_all_known_mcp_methods": false,
20682068
"rules": [{
20692069
"allow": {
20702070
"tool": "read_status"
@@ -2411,16 +2411,15 @@ mod tests {
24112411
}
24122412

24132413
#[test]
2414-
fn validate_mcp_requires_rules_when_method_profile_disabled() {
2414+
fn validate_mcp_requires_rules_by_default() {
24152415
let data = serde_json::json!({
24162416
"network_policies": {
24172417
"test": {
24182418
"endpoints": [{
24192419
"host": "mcp.example.com",
24202420
"port": 443,
24212421
"path": "/mcp",
2422-
"protocol": "mcp",
2423-
"mcp_allow_all_known_mcp_methods": false
2422+
"protocol": "mcp"
24242423
}],
24252424
"binaries": []
24262425
}

crates/openshell-supervisor-network/src/opa.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ fn normalize_l7_rules_aliases(ep: &mut serde_json::Map<String, serde_json::Value
906906
let mcp_allow_all_known_mcp_methods = ep
907907
.get("mcp_allow_all_known_mcp_methods")
908908
.and_then(serde_json::Value::as_bool)
909-
.unwrap_or(true);
909+
.unwrap_or(false);
910910
if let Some(rules) = ep.get_mut("rules").and_then(|v| v.as_array_mut()) {
911911
for rule in rules {
912912
if let Some(allow) = rule
@@ -3285,10 +3285,12 @@ network_policies:
32853285
max_body_bytes: 131072
32863286
rules:
32873287
- allow:
3288+
method: tools/call
32883289
tool:
32893290
any: [read_status, submit_*]
32903291
deny_rules:
3291-
- tool: blocked_action
3292+
- method: tools/call
3293+
tool: blocked_action
32923294
binaries:
32933295
- { path: /usr/bin/curl }
32943296
"#;
@@ -3337,11 +3339,11 @@ network_policies:
33373339
assert!(!eval_l7(&engine, &blocked));
33383340

33393341
let list_tools = l7_jsonrpc_input("mcp.params.test", 8000, "/mcp", "tools/list");
3340-
assert!(eval_l7(&engine, &list_tools));
3342+
assert!(!eval_l7(&engine, &list_tools));
33413343
}
33423344

33433345
#[test]
3344-
fn l7_mcp_endpoint_defaults_to_allow_all_tools() {
3346+
fn l7_mcp_method_profile_allows_all_tools() {
33453347
let data = r#"
33463348
network_policies:
33473349
mcp_default:
@@ -3352,6 +3354,8 @@ network_policies:
33523354
path: /mcp
33533355
protocol: mcp
33543356
enforcement: enforce
3357+
mcp:
3358+
allow_all_known_mcp_methods: true
33553359
binaries:
33563360
- { path: /usr/bin/curl }
33573361
"#;
@@ -3424,6 +3428,7 @@ network_policies:
34243428
enforcement: enforce
34253429
rules:
34263430
- allow:
3431+
method: tools/call
34273432
tool: read_status
34283433
binaries:
34293434
- { path: /usr/bin/curl }

docs/reference/policy-schema.mdx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Each endpoint defines a reachable destination and optional inspection rules.
158158
| `protocol` | string | No | Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, `graphql` for GraphQL-over-HTTP operation inspection, `mcp` for MCP Streamable HTTP request inspection, or `json-rpc` for generic JSON-RPC-over-HTTP method inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Omit for TCP passthrough. |
159159
| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. |
160160
| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. |
161-
| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. Not valid on `protocol: mcp` or `protocol: json-rpc`; MCP endpoints can use explicit rules or omit rules for the default allow-all MCP policy, and generic JSON-RPC endpoints must use explicit rules. |
161+
| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. Not valid on `protocol: mcp` or `protocol: json-rpc`; MCP uses explicit rules unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile, and JSON-RPC always uses explicit rules. |
162162
| `rules` | list of allow rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. |
163163
| `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. |
164164
| `allowed_ips` | list of string | No | CIDR or IP allowlist for SSRF override. Exact user-declared hostname endpoints may resolve to RFC 1918 private addresses without this field, but wildcard, hostless, and policy-advisor-proposed endpoints still require `allowed_ips` for private resolved IPs. Entries overlapping loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), or unspecified (`0.0.0.0`) are rejected at load time. |
@@ -171,14 +171,14 @@ Each endpoint defines a reachable destination and optional inspection rules.
171171
| `mcp` | object | No | MCP endpoint options for `protocol: mcp`. MCP endpoints must set a concrete `host` and `port` or `ports`; `protocol: mcp` alone is invalid and is not treated as a wildcard endpoint. |
172172
| `mcp.max_body_bytes` | integer | No | Maximum MCP JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. |
173173
| `mcp.strict_tool_names` | bool | No | Defaults to `true`. Requires `tools/call` `params.name` values to match `^[A-Za-z0-9_.-]{1,128}$` before policy evaluation. Set to `false` only for compatibility with MCP servers that intentionally use non-recommended tool names. Wildcard `tool` matchers require this to remain enabled. |
174-
| `mcp.allow_all_known_mcp_methods` | bool | No | Defaults to `true`. Enables the endpoint MCP method profile: omitted `rules` allow all MCP-family methods and all tools before `deny_rules`, and omitted rule `method` uses that profile. Set to `false` to require explicit MCP method rules; rules with `tool` or `params.name` must then set `method: tools/call`. |
174+
| `mcp.allow_all_known_mcp_methods` | bool | No | Defaults to `false`. When `true`, enables the endpoint MCP method profile: omitted `rules` allow all MCP-family methods and all tools before `deny_rules`, and omitted rule `method` uses that profile. When unset or `false`, explicit MCP method rules are required; rules with `tool` or `params.name` must set `method: tools/call`. |
175175
| `json_rpc` | object | No | JSON-RPC endpoint options. For `protocol: json-rpc`, `json_rpc.max_body_bytes` sets the maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. |
176176

177177
Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials.
178178

179179
#### Access Levels
180180

181-
The `access` field accepts one of the following values on REST, WebSocket, and GraphQL endpoints. MCP and JSON-RPC endpoints reject `access` because HTTP method/path presets cannot authorize JSON-RPC safely; use explicit MCP or JSON-RPC rules instead, or omit MCP rules for the default allow-all MCP policy.
181+
The `access` field accepts one of the following values on REST, WebSocket, and GraphQL endpoints. MCP and JSON-RPC endpoints reject `access` because HTTP method/path presets cannot authorize JSON-RPC safely. Use explicit MCP rules, set `mcp.allow_all_known_mcp_methods: true` for the MCP method profile, or use explicit JSON-RPC rules.
182182

183183
| Value | REST expansion | WebSocket expansion | GraphQL expansion | MCP / JSON-RPC expansion |
184184
|---|---|---|---|---|
@@ -283,15 +283,15 @@ Do not combine `method`, `path`, or `query` with `operation_type`, `operation_na
283283

284284
##### MCP Allow And Deny Rules (`protocol: mcp`)
285285

286-
MCP rules match sandbox-to-server MCP Streamable HTTP request bodies by optional MCP method and optional tool selectors. OpenShell parses the underlying JSON-RPC 2.0 envelope, validates known MCP request and notification params, and preserves unknown extension methods as policy-addressable literal method strings. Until OpenShell exposes explicit MCP version profiles, `mcp.allow_all_known_mcp_methods` defaults to `true`; with that default, rules can omit `method` and tool selectors are normalized to `tools/call` internally. Set `mcp.allow_all_known_mcp_methods: false` to require explicit MCP method rules; in that mode, rules with `tool` or `params.name` must set `method: tools/call`. By default, `tools/call` `params.name` must match the MCP-recommended tool-name pattern `^[A-Za-z0-9_.-]{1,128}$`; configure `mcp.strict_tool_names: false` on the endpoint only to allow a server that intentionally uses names outside that pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain enabled. JSON-RPC responses and server-to-client MCP messages on response bodies or SSE streams are relayed but are not currently parsed for policy enforcement.
286+
MCP rules match sandbox-to-server MCP Streamable HTTP request bodies by MCP method and optional tool selectors. OpenShell parses the underlying JSON-RPC 2.0 envelope, validates known MCP request and notification params, and preserves unknown extension methods as policy-addressable literal method strings. Until OpenShell exposes explicit MCP version profiles, `mcp.allow_all_known_mcp_methods` defaults to `false`, so endpoints require explicit MCP method rules. Set `mcp.allow_all_known_mcp_methods: true` to enable the endpoint method profile; in that mode, rules can omit `method`, and tool selectors are normalized to `tools/call` internally. By default, `tools/call` `params.name` must match the MCP-recommended tool-name pattern `^[A-Za-z0-9_.-]{1,128}$`; configure `mcp.strict_tool_names: false` on the endpoint only to allow a server that intentionally uses names outside that pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain enabled. JSON-RPC responses and server-to-client MCP messages on response bodies or SSE streams are relayed but are not currently parsed for policy enforcement.
287287

288-
Use `rules` for MCP allow rules and `deny_rules` for MCP deny rules. Deny rules take precedence over allow rules. If an MCP endpoint omits `rules` while `mcp.allow_all_known_mcp_methods` is unset or `true`, OpenShell allows all MCP-family methods and all tools, then applies any `deny_rules`. If `mcp.allow_all_known_mcp_methods` is `false`, the endpoint must define explicit rules. A broad allow or deny rule whose method matcher includes `tools/call` cannot be combined with tool-specific allow rules because it would bypass or erase the tool filter; add `tool` or `params.name` to scope `tools/call`, or remove the tool-specific rules. In a batch request, one denied call denies the full batch.
288+
Use `rules` for MCP allow rules and `deny_rules` for MCP deny rules. Deny rules take precedence over allow rules. If an MCP endpoint sets `mcp.allow_all_known_mcp_methods: true` and omits `rules`, OpenShell allows all MCP-family methods and all tools, then applies any `deny_rules`. Otherwise, the endpoint must define explicit rules. A broad allow or deny rule whose method matcher includes `tools/call` cannot be combined with tool-specific allow rules because it would bypass or erase the tool filter; add `tool` or `params.name` to scope `tools/call`, or remove the tool-specific rules. In a batch request, one denied call denies the full batch.
289289

290290
| Field | Type | Required | Description |
291291
|---|---|---|---|
292-
| `method` | string | No | MCP method name, such as `initialize`, `tools/list`, `tools/call`, or an unknown extension method. Globs are accepted only for the `tools/` method family, such as `tools/*`. Required when `mcp.allow_all_known_mcp_methods` is `false`; otherwise omitted method uses the endpoint method profile. Do not use `method: "*"` for MCP; omit `method` to use the allow-all MCP method profile. |
293-
| `tool` | string or matcher | No | Convenience matcher for `tools/call` `params.name`. Supports a glob string or `{ any: [...] }`. When `mcp.allow_all_known_mcp_methods` is `false`, `method` must also be set to `tools/call`; validation fails otherwise. Omit to match every tool. |
294-
| `params` | map | No | MCP currently accepts only `params.name` as a lower-level tool-name matcher. When `mcp.allow_all_known_mcp_methods` is `false` and `params.name` is present, `method` must also be set to `tools/call`; validation fails otherwise. Tool argument matching is not supported yet; allowed tools accept all argument payloads by default. |
292+
| `method` | string | No | MCP method name, such as `initialize`, `tools/list`, `tools/call`, or an unknown extension method. Globs are accepted only for the `tools/` method family, such as `tools/*`. Required unless `mcp.allow_all_known_mcp_methods` is `true`; when that option is true, omitted method uses the endpoint method profile. Do not use `method: "*"` for MCP; omit `method` only when using the allow-all MCP method profile. |
293+
| `tool` | string or matcher | No | Convenience matcher for `tools/call` `params.name`. Supports a glob string or `{ any: [...] }`. Requires `method: tools/call` unless `mcp.allow_all_known_mcp_methods` is `true`; validation fails otherwise. Omit to match every tool. |
294+
| `params` | map | No | MCP currently accepts only `params.name` as a lower-level tool-name matcher. Requires `method: tools/call` unless `mcp.allow_all_known_mcp_methods` is `true`; validation fails otherwise. Tool argument matching is not supported yet; allowed tools accept all argument payloads by default. |
295295

296296
Example MCP rules:
297297

@@ -306,13 +306,17 @@ endpoints:
306306
max_body_bytes: 131072
307307
rules:
308308
- allow:
309+
method: tools/call
309310
tool: search_web
310311
- allow:
312+
method: tools/call
311313
tool:
312314
any: [create_issue, list_issues]
313315
deny_rules:
314-
- tool: send_email
315-
- tool: execute_code
316+
- method: tools/call
317+
tool: send_email
318+
- method: tools/call
319+
tool: execute_code
316320
```
317321

318322
##### JSON-RPC Allow Rule (`protocol: json-rpc`)

0 commit comments

Comments
 (0)