Skip to content

feat: support host wildcards and multi-port endpoints in network policies #359

@johntmyers

Description

@johntmyers

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 port is set and ports is empty → treat as ports: [port]
  • If ports is non-empty and port is 0 → use ports as-is
  • If both are set → reject with a validation error (mutually exclusive, same pattern as access vs rules)

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:59uint32 port = 2
  • Glob matching precedent (binary paths): sandbox-policy.rego:132-141 — uses contains(b.path, "*") to detect glob patterns, then glob.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.

Metadata

Metadata

Assignees

Labels

state:agent-readyApproved for agent implementationstate:pr-openedPR has been opened for this issue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions