You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(policy): support host wildcards and multi-port endpoints (#366)
* feat(policy): support host wildcards and multi-port endpoints
Add glob-style host wildcards to endpoints[].host using OPA's
glob.match with "." as delimiter — *.example.com matches a single
DNS label, **.example.com matches across labels. Validation rejects
bare * and requires *. prefix; warns on broad patterns like *.com.
Add repeated uint32 ports field to NetworkEndpoint for multi-port
support. Backwards compatible: existing port scalar is normalized to
ports array. Both the proto-to-JSON and YAML-to-JSON conversion paths
emit a ports array; Rego always references endpoint.ports[_].
Fix OpaEngine::reload() to route through the full preprocessing
pipeline instead of bypassing L7 validation and port normalization.
Closes#359
* fix(policy): reject configs with both port and ports set
---------
Co-authored-by: johntmyers <johntmyers@users.noreply.github.com>
| `host` | `string` | _(required)_ | Hostname to match (case-insensitive). Optional when `allowed_ips` is set (see [Hostless Endpoints](#hostless-endpoints-allowed_ips-without-host)). |
409
-
| `port` | `integer` | _(required)_ | TCP port to match |
| `host` | `string` | _(required)_ | Hostname or glob pattern to match (case-insensitive). Supports wildcards (`*.example.com`). Optional when `allowed_ips` is set (see [Hostless Endpoints](#hostless-endpoints-allowed_ips-without-host)). See [Host Wildcards](#host-wildcards). |
410
+
| `port` | `integer` | _(required)_ | TCP port to match. Mutually exclusive with `ports` — if both are set, `ports` takes precedence. See [Multi-Port Endpoints](#multi-port-endpoints). |
411
+
| `ports` | `integer[]`| `[]` | Multiple TCP ports to match. When non-empty, the endpoint covers all listed ports. Backwards compatible with `port`. See [Multi-Port Endpoints](#multi-port-endpoints). |
410
412
| `protocol` | `string` | `""` | Application protocol for L7 inspection. See [Behavioral Trigger: L7 Inspection](#behavioral-trigger-l7-inspection). |
@@ -463,6 +465,135 @@ The `access` field provides shorthand for common rule sets. During preprocessing
463
465
464
466
See `crates/openshell-sandbox/src/l7/mod.rs` -- `expand_access_presets()`.
465
467
468
+
#### Host Wildcards
469
+
470
+
The `host` field supports glob patterns for matching multiple subdomains under a common domain. Wildcards use OPA's `glob.match` function with `.` as the delimiter, consistent with TLS certificate wildcard semantics.
- `*`matches exactly one DNS label (does not cross `.` boundaries). `*.example.com` matches `api.example.com` but not `deep.sub.example.com`.
481
+
- `**`matches across label boundaries. `**.example.com` matches both `api.example.com` and `deep.sub.example.com`.
482
+
- Matching is case-insensitive — both the pattern and the incoming hostname are lowercased before comparison.
483
+
- The bare domain is never matched. `*.example.com` does not match `example.com` (there must be at least one label before the domain).
484
+
485
+
**Validation rules**:
486
+
487
+
- **Error**: Bare `*` or `**` (matches all hosts) is rejected. Use a specific pattern like `*.example.com`.
488
+
- **Error**: Patterns must start with `*.` or `**.` prefix. Malformed patterns like `*com` are rejected.
489
+
- **Warning**: Broad patterns like `*.com` (only two labels) trigger a warning about covering all subdomains of a TLD.
490
+
491
+
See `crates/openshell-sandbox/src/l7/mod.rs` -- `validate_l7_policies()` for validation, `sandbox-policy.rego` -- `endpoint_allowed` for the Rego glob matching rule.
492
+
493
+
**Rego implementation**: The Rego rules detect host wildcards via `contains(endpoint.host, "*")` and dispatch to `glob.match(lower(endpoint.host), ["."], lower(network.host))`. Exact-match hosts use a separate, faster `lower(endpoint.host) == lower(network.host)` rule. See `crates/openshell-sandbox/data/sandbox-policy.rego`.
494
+
495
+
**Example**: Allow any subdomain of `example.com` on port 443:
496
+
497
+
```yaml
498
+
network_policies:
499
+
example_wildcard:
500
+
name: example_wildcard
501
+
endpoints:
502
+
- host: "*.example.com"
503
+
port: 443
504
+
binaries:
505
+
- { path: /usr/bin/curl }
506
+
```
507
+
508
+
Host wildcards compose with all other endpoint features — L7 inspection, TLS termination, multi-port, and `allowed_ips`:
509
+
510
+
```yaml
511
+
network_policies:
512
+
wildcard_l7:
513
+
name: wildcard_l7
514
+
endpoints:
515
+
- host: "*.example.com"
516
+
port: 8080
517
+
protocol: rest
518
+
tls: terminate
519
+
enforcement: enforce
520
+
rules:
521
+
- allow:
522
+
method: GET
523
+
path: "/api/**"
524
+
binaries:
525
+
- { path: /usr/bin/curl }
526
+
```
527
+
528
+
#### Multi-Port Endpoints
529
+
530
+
The `ports` field allows a single endpoint entry to cover multiple TCP ports. This avoids duplicating endpoint definitions that differ only in port number.
531
+
532
+
**Normalization**: Both YAML loading paths (file mode and gRPC mode) normalize `port` and `ports` before the data reaches the OPA engine:
533
+
534
+
- If `ports` is non-empty, it takes precedence. `port` is ignored.
535
+
- If `ports` is empty and `port` is set, the scalar is promoted to `ports: [port]`.
536
+
- The scalar `port` field is removed from the JSON fed to OPA. Rego rules always reference `endpoint.ports[_]`.
537
+
538
+
This normalization happens in `crates/openshell-sandbox/src/opa.rs` -- `normalize_endpoint_ports()` (YAML path) and `proto_to_opa_data_json()` (proto path).
539
+
540
+
**Backwards compatibility**: Existing policies using `port: 443` continue to work without changes. The scalar is silently promoted to `ports: [443]` at load time.
541
+
542
+
**YAML serialization**: When serializing policy back to YAML (e.g., `nav policy get --full`), a single-element `ports` array is emitted as the compact `port: N` scalar form. Multi-element arrays are emitted as `ports: [N, M]`. See `crates/openshell-policy/src/lib.rs` -- `from_proto()`.
543
+
544
+
**Example**: Allow both standard HTTPS and a custom TLS port:
545
+
546
+
```yaml
547
+
network_policies:
548
+
multi_port:
549
+
name: multi_port
550
+
endpoints:
551
+
- host: api.example.com
552
+
ports:
553
+
- 443
554
+
- 8443
555
+
binaries:
556
+
- { path: /usr/bin/curl }
557
+
```
558
+
559
+
This is equivalent to two separate endpoint entries:
560
+
561
+
```yaml
562
+
endpoints:
563
+
- { host: api.example.com, port: 443 }
564
+
- { host: api.example.com, port: 8443 }
565
+
```
566
+
567
+
Multi-port endpoints compose with host wildcards, L7 rules, and all other endpoint fields:
568
+
569
+
```yaml
570
+
network_policies:
571
+
wildcard_multi_port:
572
+
name: wildcard_multi_port
573
+
endpoints:
574
+
- host: "*.example.com"
575
+
ports: [443, 8443]
576
+
protocol: rest
577
+
tls: terminate
578
+
enforcement: enforce
579
+
access: read-only
580
+
binaries:
581
+
- { path: /usr/bin/curl }
582
+
```
583
+
584
+
Hostless endpoints also support multi-port:
585
+
586
+
```yaml
587
+
network_policies:
588
+
private_multi:
589
+
name: private_multi
590
+
endpoints:
591
+
- ports: [80, 443]
592
+
allowed_ips: ["10.0.0.0/8"]
593
+
binaries:
594
+
- { path: /usr/bin/curl }
595
+
```
596
+
466
597
---
467
598
468
599
### Inference Routing
@@ -793,6 +924,8 @@ The following validation rules are enforced during policy loading (both file mod
793
924
|`tls: terminate` without `protocol`|`TLS termination requires a protocol for L7 inspection`|
794
925
|`protocol: sql` with `enforcement: enforce`|`SQL enforcement requires full SQL parsing (not available in v1). Use enforcement: audit.`|
795
926
|`rules: []` (empty list) |`rules list cannot be empty (would deny all traffic). Use access: full or remove rules.`|
927
+
| Host wildcard is bare `*` or `**`|`host wildcard '*' matches all hosts; use specific patterns like '*.example.com'`|
928
+
| Host wildcard does not start with `*.` or `**.`|`host wildcard must start with '*.' or '**.' (e.g., '*.example.com'), got '{host}'`|
796
929
| Invalid HTTP method in REST rules |_(warning, not error)_|
797
930
798
931
### Errors (Live Update Rejection)
@@ -812,6 +945,7 @@ These errors are returned by the gateway's `UpdateSandboxPolicy` handler and rej
|`NetworkEndpoint`|`host`, `port`, `protocol`, `tls`, `enforcement`, `access`, `rules`, `allowed_ips`| Same field names|
1276
+
|`NetworkEndpoint`|`host`, `port`, `ports`, `protocol`, `tls`, `enforcement`, `access`, `rules`, `allowed_ips`| Same field names. `port`/`ports` normalized during loading (see [Multi-Port Endpoints](#multi-port-endpoints)).|
0 commit comments