-
Notifications
You must be signed in to change notification settings - Fork 371
Description
Problem Statement
The policy engine's endpoints[].host field only supports case-insensitive exact matching (sandbox-policy.rego:98). There is no way to express "allow all subdomains of example.com" without listing every subdomain individually. This is a common need — CDNs, API gateways, and cloud services often use many subdomains under a single root.
Similarly, endpoints[].port is a single uint32 (sandbox.proto:59). Services that listen on multiple ports (e.g., HTTP + HTTPS, or a database with primary + read replica ports) require duplicating the entire endpoint block for each port.
Proposed Design
1. Host Wildcards
Add glob-style wildcard support to endpoints[].host, evaluated via glob.match in Rego (same engine already used for binary paths and L7 URL paths).
Supported patterns:
| Pattern | Matches | Does not match |
|---|---|---|
*.example.com |
api.example.com, cdn.example.com |
example.com, deep.sub.example.com |
**.example.com |
api.example.com, deep.sub.example.com |
example.com |
example.com |
example.com (exact, unchanged behavior) |
api.example.com |
Use "." as the glob delimiter (instead of "/" used for paths) so * matches a single DNS label and ** matches across labels.
Rego changes — add a third endpoint_allowed clause:
# Endpoint matching: glob host pattern + port.
endpoint_allowed(policy, network) if {
some endpoint
endpoint := policy.endpoints[_]
contains(endpoint.host, "*")
glob.match(endpoint.host, ["."], lower(network.host))
endpoint.port == network.port
}The existing exact-match clause (sandbox-policy.rego:95-100) stays unchanged — no wildcards means exact match, same as binary path matching.
Proto: No change needed — host is already a string.
2. Multi-Port Endpoints
Allow port to accept either a single integer or an array of integers in the YAML policy format while keeping the proto backwards compatible.
Option: keep proto as uint32 port + add repeated uint32 ports
message NetworkEndpoint {
string host = 1;
uint32 port = 2; // Single port (existing, backwards compat)
repeated uint32 ports = 9; // Multiple ports (new)
// ... rest unchanged
}At validation/normalization time:
- If
portis set andportsis empty → treat asports: [port] - If
portsis non-empty andportis 0 → useportsas-is - If both are set → reject with a validation error (mutually exclusive, same pattern as
accessvsrules)
YAML surface (handled by serde deserialization):
# Single port (existing syntax, still works)
endpoints:
- host: api.example.com
port: 443
# Multiple ports (new syntax)
endpoints:
- host: api.example.com
ports: [443, 8443]Rego changes — iterate over normalized ports array:
endpoint_allowed(policy, network) if {
some endpoint
endpoint := policy.endpoints[_]
lower(endpoint.host) == lower(network.host)
endpoint.ports[_] == network.port
}Normalization (port → ports array) should happen in Rust before passing data to the Rego engine, so Rego always sees ports as an array.
Alternatives Considered
Host wildcards via regex: Rejected — regex is more powerful than needed and inconsistent with the rest of the policy engine which uses globs everywhere. Glob with . delimiter maps naturally to DNS labels.
Host wildcards via a separate host_pattern field: Adds unnecessary surface area. The contains(host, "*") detection pattern is already established for binary paths and works well.
Multi-port via string port with comma syntax (e.g., "443,8443"): Fragile, requires string parsing, and doesn't compose well with proto typing. A proper repeated uint32 is cleaner.
Multi-port via port ranges (e.g., 443-8443): Over-broad — opens all intermediate ports. Explicit enumeration is safer for a security-critical field.
Agent Investigation
Current implementation references:
- Host matching (exact only):
crates/openshell-sandbox/data/sandbox-policy.rego:95-100 - Hostless endpoint matching:
sandbox-policy.rego:105-111 - Port field proto definition:
proto/sandbox.proto:59—uint32 port = 2 - Glob matching precedent (binary paths):
sandbox-policy.rego:132-141— usescontains(b.path, "*")to detect glob patterns, thenglob.match(pattern, ["/"], path) - Glob matching precedent (L7 URL paths):
sandbox-policy.rego:211-215 - Endpoint matching test coverage:
crates/openshell-sandbox/src/opa.rs:825-842(case-insensitive host),opa.rs:1823-1841(hostless endpoints) - Policy validation:
crates/openshell-policy/src/lib.rs:461-535
The glob infrastructure is already in the Rego policy — this feature extends the same pattern to a new field. The multi-port change is a normalization concern handled in Rust before Rego evaluation.