Skip to content

Commit 6b21804

Browse files
authored
feat(policy): add GraphQL L7 inspection (#1083)
Support GraphQL L7 policies
1 parent 213025d commit 6b21804

22 files changed

Lines changed: 3452 additions & 152 deletions

File tree

Cargo.lock

Lines changed: 47 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ nix = { version = "0.29", features = ["signal", "process", "user", "fs", "term"]
6969
serde = { version = "1", features = ["derive"] }
7070
serde_json = "1"
7171
serde_yml = "0.0.12"
72+
apollo-parser = "0.8.5"
7273

7374
# HTTP client
7475
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }

architecture/sandbox.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ All paths are relative to `crates/openshell-sandbox/src/`.
2929
| `bypass_monitor.rs` | Background `/dev/kmsg` reader for iptables bypass detection events |
3030
| `sandbox/linux/netns.rs` | Network namespace creation, veth pair setup, bypass detection iptables rules, cleanup on drop |
3131
| `l7/mod.rs` | L7 types (`L7Protocol`, `TlsMode`, `EnforcementMode`, `L7EndpointConfig`), config parsing, validation, access preset expansion, deprecated `tls` value handling |
32+
| `l7/graphql.rs` | GraphQL-over-HTTP request classifier, body buffering, operation/root-field extraction, and persisted-query metadata handling |
3233
| `l7/inference.rs` | Inference API pattern detection (`detect_inference_pattern()`), HTTP request/response parsing and formatting for intercepted inference connections |
3334
| `l7/tls.rs` | Ephemeral CA generation (`SandboxCa`), per-hostname leaf cert cache (`CertCache`), TLS termination/connection helpers, `looks_like_tls()` auto-detection |
3435
| `l7/relay.rs` | Protocol-aware bidirectional relay with per-request OPA evaluation, credential-injection-only passthrough relay |
@@ -1028,22 +1029,22 @@ flowchart LR
10281029

10291030
| Type | Definition | Purpose |
10301031
|------|-----------|---------|
1031-
| `L7Protocol` | `Rest`, `Sql` | Supported application protocols |
1032+
| `L7Protocol` | `Rest`, `Graphql`, `Sql` | Supported application protocols |
10321033
| `TlsMode` | `Auto` (default), `Skip` | TLS handling strategy — `Auto` peeks first bytes and terminates if TLS is detected; `Skip` bypasses detection entirely |
10331034
| `EnforcementMode` | `Audit`, `Enforce` | What to do on L7 deny (log-only vs block) |
1034-
| `L7EndpointConfig` | `{ protocol, tls, enforcement, allow_encoded_slash }` | Per-endpoint L7 configuration |
1035+
| `L7EndpointConfig` | `{ protocol, path, tls, enforcement, allow_encoded_slash, graphql_max_body_bytes }` | Per-endpoint L7 configuration, including optional path scoping for shared host:port APIs |
10351036
| `L7Decision` | `{ allowed, reason, matched_rule }` | Result of L7 evaluation |
1036-
| `L7RequestInfo` | `{ action, target, query_params }` | HTTP method, path, and decoded query multimap for policy evaluation |
1037+
| `L7RequestInfo` | `{ action, target, query_params, graphql }` | HTTP method, path, decoded query multimap, and optional GraphQL classification for policy evaluation |
10371038

10381039
### Access presets
10391040

10401041
Policy data supports shorthand `access` presets that expand into explicit `rules` during preprocessing:
10411042

10421043
| Preset | Expands to |
10431044
|--------|-----------|
1044-
| `read-only` | `GET **`, `HEAD **`, `OPTIONS **` |
1045-
| `read-write` | `GET **`, `HEAD **`, `OPTIONS **`, `POST **`, `PUT **`, `PATCH **` |
1046-
| `full` | `* **` (all methods, all paths) |
1045+
| `read-only` | REST: `GET **`, `HEAD **`, `OPTIONS **`; GraphQL: `query` |
1046+
| `read-write` | REST: `GET **`, `HEAD **`, `OPTIONS **`, `POST **`, `PUT **`, `PATCH **`; GraphQL: `query`, `mutation` |
1047+
| `full` | REST: `* **`; GraphQL: `operation_type: "*"` |
10471048

10481049
Expansion happens in `expand_access_presets()` before the Rego engine loads the data. The `rules` and `access` fields are mutually exclusive (validated at startup).
10491050

@@ -1055,14 +1056,17 @@ Expansion happens in `expand_access_presets()` before the Rego engine loads the
10551056

10561057
- `rules` and `access` both specified on same endpoint
10571058
- `protocol` specified without `rules` or `access`
1059+
- unknown `protocol`
10581060
- `protocol: sql` with `enforcement: enforce` (SQL parsing not available in v1)
10591061
- Empty `rules` array (would deny all traffic)
1062+
- invalid GraphQL operation types, persisted-query mode, body limit, or rule shape
10601063

10611064
**Warnings** (logged):
10621065

10631066
- `tls: terminate` or `tls: passthrough` on any endpoint (deprecated — TLS termination is now automatic; use `tls: skip` to disable)
10641067
- `tls: skip` with L7 rules on port 443 (L7 inspection cannot work on encrypted traffic)
10651068
- Unknown HTTP method in rules
1069+
- GraphQL-specific fields on non-GraphQL endpoints
10661070

10671071
### TLS termination (auto-detect)
10681072

@@ -1237,13 +1241,21 @@ Implements `L7Provider` for HTTP/1.1:
12371241

12381242
- **`looks_like_http()`**: Protocol detection via first-byte peek -- checks for standard HTTP method prefixes (GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, CONNECT, TRACE).
12391243

1244+
### GraphQL protocol classifier
1245+
1246+
**File:** `crates/openshell-sandbox/src/l7/graphql.rs`
1247+
1248+
GraphQL inspection reuses the HTTP parser, then buffers the request body up to `graphql_max_body_bytes` for classification. It supports `GET` and `POST` GraphQL-over-HTTP envelopes, JSON batches, named operations, root fragment expansion, Apollo persisted-query hashes, and saved-query IDs (`id`, `documentId`, `queryId`). The classifier emits `GraphqlRequestInfo` with operation type, optional operation name, root fields, and persisted-query identifiers.
1249+
1250+
Hash-only or saved-query-only requests cannot be parsed into operation fields. They are denied unless the endpoint sets `persisted_queries: allow_registered` and provides a trusted `graphql_persisted_queries` entry for the hash or ID. Batch requests are fail-closed: any malformed, denied, or unregistered operation denies the whole HTTP request.
1251+
12401252
### Per-request L7 evaluation
12411253

12421254
`relay_with_inspection()` in `crates/openshell-sandbox/src/l7/relay.rs` is the main relay loop:
12431255

12441256
1. Parse one HTTP request from client via the provider. Parser and path-canonicalization failures close the connection and emit a denied OCSF network event with the rejection reason in `status_detail`.
12451257
2. Resolve credential placeholders in the request target via `rewrite_target_for_eval()`. OPA receives the redacted path (`[CREDENTIAL]` markers); the resolved path goes only to upstream. If resolution fails, return HTTP 500 and close the connection.
1246-
3. Build L7 input JSON with `request.method`, the **redacted** `request.path`, `request.query_params`, plus the CONNECT-level context (host, port, binary, ancestors, cmdline)
1258+
3. Build L7 input JSON with `request.method`, the **redacted** `request.path`, `request.query_params`, optional `request.graphql`, plus the CONNECT-level context (host, port, binary, ancestors, cmdline)
12471259
4. Evaluate `data.openshell.sandbox.allow_request` and `data.openshell.sandbox.request_deny_reason`
12481260
5. Log the L7 decision (tagged `L7_REQUEST`) using the redacted target — real credential values never appear in logs
12491261
6. If allowed (or audit mode): relay request to upstream via `relay_http_request_with_resolver()` (which rewrites all remaining credential placeholders in headers, query parameters, path segments, and Basic auth tokens) and relay the response back to client, then loop

0 commit comments

Comments
 (0)