Repository navigation
deny rules in network policy schema #565
Description
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:
yamlrules:
# Allow everything by default
- allow:
method: "*"
path: "/**"
# Block specific dangerous paths
- deny:
method: POST
path: "/repos/*/pulls/*/reviews"
- deny:
method: PUT
path: "/repos/*/branches/*/protection"
- deny:
method: PUT
path: "/webapi/sites/*/accessrequest/*"
Proposed Design
- Add a deny rule type alongside the existing allow rule type.
- deny rules take precedence over allow rules (deny wins on conflict).
- Evaluation order: if a request matches any deny rule, it is blocked regardless of allow rules. If no deny rule matches, existing allow logic applies.
- The access: full shorthand combined with deny rules would cover the "allow-all-except" pattern without needing to enumerate individual allows.
For example:
endpoints:
- host: api.github.com
port: 443
protocol: rest
tls: terminate
enforcement: enforce
access: full # allow all methods and paths by default
deny_rules:
- deny:
method: POST
path: "/repos/*/pulls/*/reviews"
- deny:
method: "*"
path: "/repos/*/branches/*/protection"
- deny:
method: "*"
path: "/repos/*/branches/*/protection/**"
- deny:
method: "*"
path: "/repos/*/rulesets"
- deny:
method: "*"
path: "/repos/*/rulesets/*"
- deny:
method: POST
path: "/repos/*/actions/runs/*/approve"
- deny:
method: POST
path: "/repos/*/actions/runs/*/pending-deployments"
- deny:
method: POST
path: "/graphql"
- deny:
method: "PUT"
path: "/repos/*/actions/permissions"
- deny:
method: "PUT"
path: "/repos/*/actions/permissions/**"
- deny:
method: "*"
path: "/orgs/**"
except_methods: [GET, HEAD, OPTIONS]
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
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
johntmyers commentedon Mar 25, 2026
Hi, thank you. We'll get a plan together for this and implement over the next few days.
mjamiv commentedon Apr 6, 2026
+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:
This would unblock a significant portion of our security hardening without requiring us to enumerate every allowed API path.
mjamiv commentedon Apr 8, 2026
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.
mjamiv commentedon Apr 8, 2026
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.
nopcorn commentedon Apr 13, 2026
Seconded, any updates on whether this will be selected for dev @johntmyers ?
johntmyers commentedon Apr 13, 2026
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:Policy YAML Schema
Rego Evaluation
Deny rules are evaluated after allow rules. If a request matches any deny rule, it is blocked regardless of the access preset:
New
deny_requestrule added tosandbox-policy.rego.Scope
L7DenyRuleto proto and policy structs inopenshell-policydeny_rulesfield toNetworkEndpointDefserde structsdeny_requestRego rule with glob path matchingRelationship 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.
johntmyers commentedon Apr 13, 2026
Implementation Plan
Issue type:
featComplexity: Medium
Confidence: High — clear path, follows existing allow rule patterns exactly
Summary
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:
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 examplesImplementation Steps
L7DenyRulemessage (mirrorsL7Allow) anddeny_rulesfield onNetworkEndpointL7DenyRuleDefserde struct, adddeny_rulesfield toNetworkEndpointDef, wire upto_proto/from_protoconversionsdeny_rulesarray through to Rego data inproto_to_opa_data_jsonvalidate_l7_policies— require protocol, validate method/path/command fields, check glob syntax, reject empty deny_rules listdeny_requestrule that reuses existing matchers (method_matches,path_matches,query_params_match,command_matches). Modifyallow_requestto addnot deny_requestguard. Add deny-specificrequest_deny_reasondocs/reference/policy-schema.mdxwith deny rule object table and examples. Updategenerate-sandbox-policyskillTest Plan
Unit tests (openshell-policy):
deny_rulesand verify proto fieldsdeny_unknown_fieldsUnit tests (openshell-sandbox, Rego):
access: read-write*) blocks all methods on pathcommandmatchingUnit tests (openshell-sandbox, validation):
deny_rulesrequiresprotocol(L7-only feature)deny_ruleslist rejectedE2E tests: N/A — this is a policy schema change, testable through unit/integration tests against the OPA engine
Risks & Open Questions
request_deny_reasonuses incremental rules that must be mutually exclusive. Usedeny_request/not deny_requestguards to prevent "complete rule conflict" errors.EnforcementMode::Audit, deny rules are evaluated but logged rather than enforced — matches existing audit behavior for allows. No Rust-side changes needed.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 policiesRevision 1 — initial plan
feat(policy): add deny rules to network policy schema
johntmyers commentedon Apr 13, 2026
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
Docs updated
docs/reference/policy-schema.mdx: Deny Rule Object section.agents/skills/generate-sandbox-policy/SKILL.md: Deny rules guidanceThe issue will auto-close when the PR is merged.
feat(policy): add deny rules to network policy schema (NVIDIA#822)