Repository navigation
deny rules in network policy schema #565
Description
Activity
Hi, thank you. We'll get a plan together for this and implement over the next few days.
+1 — we have a concrete use case for deny rules.
We run 4 OpenClaw AI agent sandboxes with GitHub API access (
api.github.com,access: full). Our security hardening plan calls for restricting dangerous GitHub API operations — e.g., denyPUT /repos/{owner}/{repo}/branches/{branch}/protection(modify branch protection), denyPOST /repos/{owner}/{repo}/merges(force merge), denyDELETE /repos/{owner}/{repo}/branches/*— while allowing all other API paths.Similarly, for Slack we want to allow most API methods but deny sensitive ones like
admin.conversations.delete,admin.users.remove, etc.Currently this is blocked because OpenShell policies are allow-only with no path/method filtering. We documented this as "Blocked on OpenShell L7" in our security hardening plan (items #8, #9, #1.1).
With deny rules at the HTTP path/method level, we could:
github: endpoints: - host: api.github.com port: 443 access: full deny: - path: "/repos/*/branches/*/protection" method: PUT - path: "/repos/*/merges" method: POST
This would unblock a significant portion of our security hardening without requiring us to enumerate every allowed API path.
Hey @johntmyers — friendly follow-up on this. You mentioned a plan was coming together around Mar 25. Any update on timing? No rush, just checking in since we're scoping our next security hardening pass and deny rules would let us tighten GitHub API access across our 4 sandboxes.
Hey @johntmyers — friendly follow-up on this. You mentioned a plan was coming together around Mar 25. Any update on timing? No rush, just checking in since we're scoping our next security hardening pass and deny rules would let us tighten GitHub API access across our 4 sandboxes.
Reacted by nopcornSeconded, any updates on whether this will be selected for dev @johntmyers ?
Implementation Plan (extracted from Provider v2 RFC)
This is being pulled out of the Provider v2 RFC as a standalone deliverable. Deny rules are a pure policy schema feature -- once they exist, provider profiles get them for free since provider-generated rules use the same
NetworkEndpointschema.Proto Changes
Add
deny_rulestoNetworkEndpointand defineL7DenyRule:message NetworkEndpoint { // ... existing fields ... repeated L7DenyRule deny_rules = 20; } message L7DenyRule { string method = 1; string path = 2; }
Policy YAML Schema
endpoints: - host: api.github.com port: 443 access: read-write protocol: rest enforcement: enforce deny_rules: - method: POST path: "/repos/*/pulls/*/reviews" - method: PUT path: "/repos/*/branches/*/protection"
Rego Evaluation
Deny rules are evaluated after allow rules. If a request matches any deny rule, it is blocked regardless of the access preset:
1. Check access preset (read-only, read-write, full) -> generates allow rules 2. Check deny rules -> if any match, block (deny wins)New
deny_requestrule added tosandbox-policy.rego.Scope
- Add
L7DenyRuleto proto and policy structs inopenshell-policy - Add
deny_rulesfield toNetworkEndpointDefserde structs - Add
deny_requestRego rule with glob path matching - Unit tests for deny evaluation (deny overrides allow, glob matching, multiple deny rules)
- E2E test: policy with deny rule blocks specific method+path while allowing others
Relationship to Provider v2
Provider v2 PR 1 (Profile Registry) will ship YAML profiles with deny rules pre-populated (e.g., GitHub profile blocks branch protection changes, PR approvals). This issue delivers the underlying schema and evaluation; providers consume it.
- Add
🏗️ build-plan
Implementation Plan
Issue type:
feat
Complexity: Medium
Confidence: High — clear path, follows existing allow rule patterns exactlySummary
Add deny rules to the L7 network policy schema. Deny rules mirror the full capability set of allow rules (method, path, query params, SQL command) but with inverted effect. Deny rules are evaluated after allow rules and take precedence — if a request matches any deny rule, it is blocked regardless of allow rules. This lets users express "allow everything except these specific operations" without enumerating every allowed endpoint.
Key Design Decision: Full Feature Parity with Allow Rules
Deny rules support all the same matching capabilities as allow rules:
// Mirrors L7Allow exactly — same fields, same semantics, inverted effect message L7DenyRule { string method = 1; // HTTP method or "*" string path = 2; // URL path glob string command = 3; // SQL command or "*" map<string, L7QueryMatcher> query = 4; // Query parameter matchers (REST) }
This means you can deny by method + path + query params for REST, and by command for SQL — same as allows. The Rego evaluation reuses the existing
method_matches,path_matches,query_params_match, andcommand_matcheshelpers for both allow and deny paths.L4-only endpoints (no
protocolset) cannot have deny rules — deny rules require L7 inspection to evaluate method/path/command.Scope
proto/sandbox.proto: AddL7DenyRulemessage anddeny_rulesrepeated field onNetworkEndpointcrates/openshell-policy/src/lib.rs: AddL7DenyRuleDefserde struct, adddeny_rulestoNetworkEndpointDef, updateto_proto/from_protoconversionscrates/openshell-sandbox/src/opa.rs: Passdeny_rulesthrough to Rego inproto_to_opa_data_jsoncrates/openshell-sandbox/src/l7/mod.rs: Validatedeny_rulesinvalidate_l7_policies(require protocol, validate method/path/command, glob syntax checks)crates/openshell-sandbox/data/sandbox-policy.rego: Adddeny_requestrule, modifyallow_requestto checknot deny_request, updaterequest_deny_reasonfor deny-specific messagingdocs/reference/policy-schema.mdx: Document deny rules, add deny rule object table, add examples
Implementation Steps
- Proto schema: Add
L7DenyRulemessage (mirrorsL7Allow) anddeny_rulesfield onNetworkEndpoint - Rust policy types: Add
L7DenyRuleDefserde struct, adddeny_rulesfield toNetworkEndpointDef, wire upto_proto/from_protoconversions - JSON preprocessing: Pass
deny_rulesarray through to Rego data inproto_to_opa_data_json - L7 validation: Validate deny rules in
validate_l7_policies— require protocol, validate method/path/command fields, check glob syntax, reject empty deny_rules list - Rego evaluation: Add
deny_requestrule that reuses existing matchers (method_matches,path_matches,query_params_match,command_matches). Modifyallow_requestto addnot deny_requestguard. Add deny-specificrequest_deny_reason - Tests: Unit tests for policy parsing round-trip with deny rules. Rego tests for deny-wins-over-allow, query param deny matching, wildcard method deny, non-matching requests still allowed, deny reason populated. Validation tests for deny_rules requiring protocol, empty list rejection
- Documentation: Update
docs/reference/policy-schema.mdxwith deny rule object table and examples. Updategenerate-sandbox-policyskill
Test Plan
-
Unit tests (openshell-policy):
- Parse YAML with
deny_rulesand verify proto fields - Round-trip: serialize → parse → verify deny_rules survive
- Reject unknown fields in deny rule via
deny_unknown_fields
- Parse YAML with
-
Unit tests (openshell-sandbox, Rego):
- Deny rule blocks request that would be allowed by
access: read-write - Deny rule with query param matching blocks specific query values
- Deny rule with wildcard method (
*) blocks all methods on path - Non-matching requests are still allowed
- Deny reason string populated when deny rule matches
- Multiple deny rules: first match wins
- Deny rule with SQL
commandmatching
- Deny rule blocks request that would be allowed by
-
Unit tests (openshell-sandbox, validation):
deny_rulesrequiresprotocol(L7-only feature)- Empty
deny_ruleslist rejected - Unknown HTTP method in deny rule produces warning
- Valid deny config accepted
-
E2E tests: N/A — this is a policy schema change, testable through unit/integration tests against the OPA engine
Risks & Open Questions
- Rego rule conflict avoidance:
request_deny_reasonuses incremental rules that must be mutually exclusive. Usedeny_request/not deny_requestguards to prevent "complete rule conflict" errors. - Audit mode: In
EnforcementMode::Audit, deny rules are evaluated but logged rather than enforced — matches existing audit behavior for allows. No Rust-side changes needed. - SQL deny rules: The proto and Rego support SQL deny rules, but the SQL L7 runtime is not yet implemented (falls back to passthrough). SQL deny rules will only take effect once the SQL provider ships. This is consistent with how SQL allow rules work today.
Documentation Impact
docs/reference/policy-schema.mdx: Add deny rule object table, update endpoint object table withdeny_rulesfield, add examples.agents/skills/generate-sandbox-policy/SKILL.md: Update to include deny_rules in generated policies
Revision 1 — initial plan
- addedstate:review-readyReady for human reviewReady for human reviewstate:agent-readyApproved for agent implementationApproved for agent implementationstate:in-progressWork is currently in progressWork is currently in progress
on Apr 13, 2026 - added a commit that references this issue
on Apr 13, 2026 🏗️ build-from-issue-agent
Implementation Complete
PR: #822
What was built
L7 deny rules added to the network policy schema with full feature parity with allow rules (method, path, query params, SQL command). Deny rules are evaluated after allow rules and take precedence. This enables the "allow everything except these specific operations" pattern requested in the issue.
Tests
- Unit (openshell-policy): 4 tests added
- Unit (openshell-sandbox/Rego): 10 tests added
- E2E: N/A
Docs updated
docs/reference/policy-schema.mdx: Deny Rule Object section.agents/skills/generate-sandbox-policy/SKILL.md: Deny rules guidance
The issue will auto-close when the PR is merged.
- addedstate:pr-openedPR has been opened for this issuePR has been opened for this issueand removedstate:in-progressWork is currently in progressWork is currently in progressstate:review-readyReady for human reviewReady for human review
on Apr 13, 2026 - added a commit that references this issue
on Apr 15, 2026
Problem Statement
The current policy schema only supports allow rules. Every permitted method+path combination must be explicitly enumerated. This works well for simple use cases (e.g., read-only access to one API), but becomes unmanageable when the goal is to block a small set of dangerous operations on services with large API surfaces.
Real-world example: We run AI agents that need full developer-level access to GitHub and internal Teleport servers, but must be prevented from performing admin and strictly-human operations (approving PRs, changing rulesets, etc).
For GitHub's REST API, blocking ~10 admin endpoints (branch protection, rulesets, org settings, PR approvals, workflow approvals) requires enumerating ~60+ allow rules for every legitimate developer operation. Any new GitHub API endpoint is blocked by default until we update the policy, which means agent workflows break silently on GitHub API additions.
For services like Teleport, the problem is worse. Teleport's web API has hundreds of routes that change with every release, served alongside gRPC-web on the same port. Maintaining an exhaustive allow-list is not feasible — we currently have to choose between verbose allow-lists that break on Teleport upgrades, or falling back to TCP passthrough with no L7 protection at all.
With deny rules, both policies collapse to something like:
Proposed Design
For example:
Alternatives Considered
Exhaustive allow-list (current approach)
This is what we're doing today. For GitHub, it requires ~60 allow rules to cover normal developer operations while omitting ~10 admin paths. For Teleport, it requires enumerating every safe POST endpoint across a web API with hundreds of routes.
Why it's insufficient: The policy is fragile. When GitHub or Teleport adds a new non-admin API endpoint, it's blocked by default until we update the allow-list. This creates silent failures in agent workflows that are hard to diagnose. For Teleport specifically, the API surface changes with every release — maintaining an exhaustive allow-list means coupling our policy update cycle to every vendor upgrade. The policy file also becomes extremely long (300+ lines for just two services), making review and auditing harder.
Rely solely on service-side RBAC
Trust that GitHub's permissions and Teleport's role-based access control will block unauthorized operations, and don't attempt L7 enforcement in OpenShell at all.
Why it's insufficient: Our threat model specifically covers the case where the user has admin permissions (granted via Terraform elevation) and forgets to de-escalate before starting an agent session. Service-side RBAC says "yes, this token is authorized" — the whole point of the OpenShell policy layer is to enforce a narrower scope than the token allows. Defense-in-depth requires that the sandbox restrict what the agent can do even when the credential is over-privileged.
Agent Investigation
No response
Checklist