Skip to content

feat(policy): add GraphQL L7 inspection#1083

Open
johntmyers wants to merge 16 commits intomainfrom
feat/1022-graphql-l7-inspection/johntmyers
Open

feat(policy): add GraphQL L7 inspection#1083
johntmyers wants to merge 16 commits intomainfrom
feat/1022-graphql-l7-inspection/johntmyers

Conversation

@johntmyers
Copy link
Copy Markdown
Collaborator

@johntmyers johntmyers commented Apr 30, 2026

🏗️ build-from-issue-agent

Summary

Adds GraphQL-aware L7 inspection for GraphQL-over-HTTP endpoints. The sandbox can classify operation type, operation name, root fields, and persisted-query identifiers before evaluating OPA policy.

Related Issue

Closes #1022

Changes

  • proto/sandbox.proto, crates/openshell-policy, crates/openshell-sandbox/src/opa.rs: add GraphQL policy fields, persisted-query registry fields, YAML/proto round trips, and OPA serialization.
  • crates/openshell-sandbox/src/l7/graphql.rs, crates/openshell-sandbox/src/l7/relay.rs: add GraphQL request parsing, bounded body buffering, batched operation classification, deny handling, and OCSF logging.
  • crates/openshell-sandbox/data/sandbox-policy.rego: enforce GraphQL allow/deny rules with deny precedence, all-operation batch checks, field glob matching, and trusted hash-only persisted-query handling.
  • L7 endpoint selection is path-aware, so REST and GraphQL endpoints can share the same host and port without treating GraphQL as a generic REST POST.
  • e2e/rust/tests/forward_proxy_graphql_l7.rs: cover forward-proxy and CONNECT-proxy GraphQL enforcement, duplicate GET control params, persisted queries, and chunked request bodies.
  • architecture/ and docs/: document policy syntax, real-world service policy shapes, and saved/hash-only query behavior.

Railway-Style Destructive Mutation UX

The Railway accident class was a direct GraphQL mutation against https://backboard.railway.app/graphql/v2 such as:

curl -X POST https://backboard.railway.app/graphql/v2 \
  -H "Authorization: Bearer $RAILWAY_TOKEN" \
  -d '{"query":"mutation { volumeDelete(volumeId: \"vol_123\") }"}'

A policy can make that failure mode explicit by allowing only known read operations and denying destructive root mutation fields before the request leaves the sandbox:

network_policies:
  railway_guardrails:
    endpoints:
      - host: backboard.railway.app
        port: 443
        path: /graphql/v2
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
              fields:
                - viewer
                - project
                - projects
                - environment
                - service
                - deployment
          - allow:
              operation_type: mutation
              fields:
                - deploymentRedeploy
                - deploymentCancel
        deny_rules:
          - operation_type: mutation
            fields:
              - volumeDelete
              - serviceDelete
              - projectDelete
              - environmentDelete
              - databaseDelete
    binaries:
      - path: /usr/bin/curl
      - path: /usr/local/bin/python*
      - path: /sandbox/.uv/python/*/bin/python*

The important reviewer-facing behavior is that policy authors do not need to identify every possible mutation name. They can combine narrow allow rules with explicit high-risk deny rules, and deny rules take precedence if a batch mixes safe reads with a destructive mutation.

Real-World Network Policy Shapes

These examples are intentionally service-specific. GraphQL is not a universal verb model like REST, so the useful policy unit is the app schema's root fields plus any trusted saved-query registry. The new endpoint path selector is what lets a policy distinguish /graphql from REST routes on the same host.

GitHub mixed REST and GraphQL shape on api.github.com:

network_policies:
  github_api_guardrails:
    endpoints:
      - host: api.github.com
        port: 443
        path: /repos/NVIDIA/OpenShell/**
        protocol: rest
        enforcement: enforce
        rules:
          - allow:
              methods: [GET]
              path: /repos/NVIDIA/OpenShell/**
          - allow:
              methods: [POST]
              path: /repos/NVIDIA/OpenShell/issues/*/comments
        deny_rules:
          - methods: [DELETE, PATCH, PUT]
            path: /repos/NVIDIA/OpenShell/**
      - host: api.github.com
        port: 443
        path: /graphql
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
              fields:
                - viewer
                - repository
                - organization
                - search
                - rateLimit
                - node
                - nodes
        deny_rules:
          - operation_type: mutation
            fields:
              - deleteRepository
              - deleteRef
              - updateBranchProtectionRule
              - createDeployment
              - mergePullRequest
    binaries:
      - path: /usr/bin/gh
      - path: /usr/local/bin/gh
      - path: /usr/bin/python*
      - path: /usr/local/bin/python*

Railway destructive-operation guardrail shape:

network_policies:
  railway_graphql_guardrails:
    endpoints:
      - host: backboard.railway.app
        port: 443
        path: /graphql/v2
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
              fields: [viewer, project, projects, environment, service, deployment]
          - allow:
              operation_type: mutation
              fields: [deploymentRedeploy, deploymentCancel]
        deny_rules:
          - operation_type: mutation
            fields: [volumeDelete, serviceDelete, projectDelete, environmentDelete, databaseDelete]
    binaries:
      - path: /usr/bin/curl
      - path: /usr/bin/python*
      - path: /usr/local/bin/python*

Shopify Admin GraphQL read-first shape:

network_policies:
  shopify_admin_graphql_review:
    endpoints:
      - host: my-store.myshopify.com
        port: 443
        path: /admin/api/*/graphql.json
        protocol: graphql
        enforcement: enforce
        rules:
          - allow:
              operation_type: query
              fields:
                - shop
                - products
                - product
                - orders
                - order
                - customers
                - customer
        deny_rules:
          - operation_type: mutation
            fields:
              - '*Delete'
              - bulkOperationRunMutation
              - inventoryAdjustQuantities
              - orderCancel
              - refundCreate
    binaries:
      - path: /usr/bin/node
      - path: /usr/local/bin/node
      - path: /usr/bin/python*
      - path: /usr/local/bin/python*

Hash-only / saved-query shape with a trusted registry:

network_policies:
  railway_registered_graphql:
    endpoints:
      - host: backboard.railway.app
        port: 443
        path: /graphql/v2
        protocol: graphql
        enforcement: enforce
        persisted_queries: allow_registered
        graphql_persisted_queries:
          railwayProjectReadV1:
            operation_type: query
            operation_name: Project
            fields: [project]
          abc123trustedhash:
            operation_type: query
            operation_name: Viewer
            fields: [viewer]
        rules:
          - allow:
              operation_type: query
              fields: [project, projects, environment, service, viewer]
        deny_rules:
          - operation_type: mutation
            fields: [volumeDelete, serviceDelete, projectDelete, environmentDelete]
    binaries:
      - path: /usr/bin/python*
      - path: /usr/local/bin/python*

Behavior to review:

  • Full-document GraphQL requests are classified from the HTTP body or GET query string.
  • When a host has both REST and GraphQL L7 endpoints, OpenShell selects the most specific matching endpoint path before applying protocol-specific policy.
  • For GitHub, POST https://api.github.com/graphql is evaluated by the /graphql GraphQL endpoint; GET https://api.github.com/repos/NVIDIA/OpenShell/issues is evaluated by the /repos/NVIDIA/OpenShell/** REST endpoint.
  • Duplicate GraphQL GET control params are rejected fail-closed.
  • Hash-only or saved-query-only requests are denied unless persisted_queries: allow_registered is set and the hash or saved-query ID appears in graphql_persisted_queries.
  • Registered persisted queries are evaluated as the registered operation metadata, not as an opaque allow-all bypass.
  • Batched requests are fail-closed: one malformed, denied, destructive, or unregistered operation denies the whole HTTP request.

Deviations from Plan

  • Implemented operation type, operation name, root fields, and persisted-query registry together rather than splitting Phase 1/Phase 2.
  • Added endpoint path matching so protocol: rest and protocol: graphql can coexist under the same host and port.

Testing

  • mise run pre-commit passes
  • Unit tests added/updated
  • E2E tests added/updated

Tests added:

  • Unit: GraphQL classifier tests in crates/openshell-sandbox/src/l7/graphql.rs; policy validation/access preset tests in crates/openshell-sandbox/src/l7/mod.rs; GraphQL OPA allow/deny/persisted-query tests in crates/openshell-sandbox/src/opa.rs; YAML/proto round-trip test in crates/openshell-policy/src/lib.rs.
  • Integration: N/A.
  • E2E: e2e/rust/tests/forward_proxy_graphql_l7.rs covers GraphQL L7 enforcement over both forward-proxy and CONNECT-proxy paths, including duplicate GET control params, persisted-query registry behavior, and chunked bodies.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

Documentation updated:

  • architecture/sandbox.md: GraphQL classifier, endpoint path, and L7 flow updates.
  • architecture/security-policy.md: GraphQL schema, endpoint path, and enforcement behavior.
  • docs/reference/policy-schema.mdx: policy schema fields, including endpoint path.
  • docs/sandboxes/policies.mdx: user policy examples, service shapes, and mixed REST/GraphQL host examples.
  • docs/get-started/tutorials/github-sandbox.mdx: GitHub REST and GraphQL endpoints split by path.
  • docs/security/best-practices.mdx: GraphQL L7 recommendations.

Closes #1022

Adds GraphQL-over-HTTP classification, policy schema fields, OPA enforcement, persisted-query registry handling, tests, and documentation.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
@johntmyers johntmyers requested a review from a team as a code owner April 30, 2026 17:45
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented Apr 30, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions
Copy link
Copy Markdown

@johntmyers johntmyers added the test:e2e Requires end-to-end coverage label May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Label test:e2e applied, but pull-request/1083 is at {"messa while the PR head is 2595827. A maintainer needs to comment /ok to test 25958274210cf8763c570e0b6b169bd6555c4e11 to refresh the mirror. Once the mirror catches up, re-run Branch E2E Checks from the Actions tab.

@johntmyers
Copy link
Copy Markdown
Collaborator Author

/ok to test 2595827

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GraphQL-aware L7 inspection: operation-type and field-level policy rules

1 participant