New — Iris AI drafts your firewall, routing & rate-limit rules. Explore AI features →
Back to Blog
api-gatewaysecurityproducttutorial

Turn Your OpenAPI Spec Into Edge Request Validation

Upload or paste an OpenAPI 3 spec and Ngris generates one body-validation rule per JSON operation — SSRF-safe parsing, a full preview, and rules staged disabled so an import can't break production.

9 min read

If you run an API, you probably already have an OpenAPI document that describes exactly which routes exist and what each request body is supposed to look like. That spec usually gets treated as documentation. Ngris can treat it as a source of enforcement instead: point the importer at your spec and it generates one request-body validation rule per JSON operation, scoped to the exact method and path, and attaches them to an endpoint as a single managed traffic policy. No hand-written rules, and no new rule type on the data plane — it reuses the validate-body rule that already runs at the edge.

What the importer actually does

The importer is a code generator, not a mock server and not a proxy. It reads an OpenAPI 3 document and, for every operation that declares an application/json request body, emits a validate-body rule whose JSON Schema comes straight from the spec. Those rules are materialized as one policy named openapi-import on the endpoint you choose. Everything downstream is the existing edge machinery: the generated rules run in the on_http_request phase, and each carries a CEL guard so it only fires on the operation it came from.

Concretely, an operation like POST /orders becomes a rule named POST /orders (body) with a message of Request body does not match OpenAPI spec for POST /orders. The schema is emitted as JSON Schema (draft 2020-12), which is exactly what the validate-body evaluator expects — so there is no translation layer that can drift.

Upload or paste — never a URL

There are exactly two ways to get a spec in: a multipart file upload in a form field named spec, or a JSON body with a spec_text field for a paste. There is deliberately no spec_url. A server-side "fetch my spec from this URL" field is a classic SSRF vector — it would let a caller make the API server reach internal addresses — so it simply does not exist.

Parsing is locked down the rest of the way, too. The importer builds its OpenAPI loader with external $ref resolution disabled, and the loader's URI reader unconditionally refuses every URL with external $ref resolution is disabled: refusing to read .... That covers both http(s):// and file:// references, so a spec cannot be used to read a local file or reach out over the network — only in-document references (#/components/...) resolve. The document is parsed from the uploaded bytes only, never from a path or URL, and any external $ref, malformed document, or failed validation rejects the whole import with a 422. It fails closed: you never get a half-parsed spec.

Preview before you write anything

The import is a two-call flow, and the first call writes nothing. POST to the preview endpoint and you get back the full plan: every rule it would create (method, path, name, phase, the generated CEL expression, the schema config, and whether it is enabled), every operation it skipped and why, the operation count, and a plan_hash.

# Dry run: generate the plan, write nothing to the database

curl -X POST https://api.ngris.com/v1/endpoints/$ENDPOINT_UUID/policies/import-openapi/preview \   -H "Authorization: Bearer $TOKEN" \   -F spec=@openapi.yaml \   -F mode=detect

# Or paste the spec inline as JSON, and exclude an operation

curl -X POST https://api.ngris.com/v1/endpoints/$ENDPOINT_UUID/policies/import-openapi/preview \   -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \   -d '{"spec_text": "...", "mode": "detect", "exclude": ["POST /internal/reset"]}'

Skipped operations are explicit, and skipping is never fatal. An operation is dropped with a reason such as no request body, no application/json schema, excluded, schema not convertible: ..., or schema exceeds 65536 bytes. A spec that mixes JSON and form endpoints just produces rules for the JSON ones and tells you exactly what it left out.

How rules are generated and scoped

Operations are walked in a deterministic order — literal paths before templated ones, methods sorted — and each rule is scoped with a CEL expression built from the method and path. A literal path is an equality check; a templated path becomes an anchored regex, with each {param} segment matching a single path segment:

# Literal path — exact match

req.method == "POST" && req.path == "/orders"

# Templated path — {orderId} becomes one path segment

req.method == "GET" && req.path.matches("^/orders/[^/]+/items$")

An optional base_path is prefixed to every matcher path, so a spec served under a prefix still lines up with the real URLs. Here is a generated rule as it appears in the preview plan for a detect-mode import — note enabled is false:

# One entry from the preview plan's rules[]

{   "method": "POST",   "path": "/orders",   "name": "POST /orders (body)",   "rule_type": "validate-body",   "phase": "on_http_request",   "expression": "req.method == \"POST\" && req.path == \"/orders\"",   "enabled": false,   "config": {     "schema": { "type": "object", "required": ["sku", "qty"],       "properties": { "sku": {"type": "string"},         "qty": {"type": "integer", "minimum": 1} } },     "message": "Request body does not match OpenAPI spec for POST /orders"   } }

If the document is OpenAPI 3.0, a few 3.0-only idioms are normalized so a draft-2020-12 validator handles them: {"nullable": true, "type": "T"} becomes {"type": ["T", "null"]}, and the boolean forms of exclusiveMinimum / exclusiveMaximum become their numeric forms. OpenAPI 3.1 schemas already use draft 2020-12 and pass through unchanged. Swagger / OpenAPI 2.0 is not supported.

Detect first: rules are staged disabled

This is the part that keeps an import from breaking production. The importer defaults to mode=detect, and in detect mode every generated rule is created disabled. The policy attaches, you can read every rule and its schema in the dashboard, but nothing is enforcing yet. Only when you re-run the import with mode=enforce are the rules created enabled and actually rejecting bodies that do not match.

This lines up with the WAF and bot-management rule types, which also default to a "detect" posture rather than blocking on day one — though there "detect" means log-only, and here it means the rules are staged disabled. Either way, the default never turns on blocking behind your back.

# Apply for real: enforce mode enables the rules, echo the plan_hash from preview

curl -X POST https://api.ngris.com/v1/endpoints/$ENDPOINT_UUID/policies/import-openapi \   -H "Authorization: Bearer $TOKEN" \   -F spec=@openapi.yaml \   -F mode=enforce \   -F plan_hash=$PLAN_HASH

The apply call is the only writer. Its response tells you what happened: policy_id, rules_created, rules_deleted, the mode, whether a route allow-list was included, and the plan_hash.

Idempotent re-import

Re-importing is a full replace of one managed policy, in a single transaction. The writer is keyed on (endpoint, "openapi-import"): it upserts that one policy, deletes its existing rules, and inserts the fresh set. An operation you removed from the spec loses its rule; a re-import never duplicates. Hand-made policies and rules on the same endpoint are never read or touched — only the one policy named openapi-import. And if you had disabled that managed policy at the policy level, a re-import preserves that; only a brand-new policy starts enabled.

To make the preview meaningful, the plan is deterministic: the same bytes and options always produce the same rules and the same plan_hash. If you echo the hash from your preview into the apply call and the spec changed underneath you, the server recomputes the hash, sees the mismatch, and returns 409 Conflictspec changed since preview (plan hash mismatch); re-preview before applying. Omit the hash and you get a direct apply. It is a small TOCTOU guard so you never apply a plan you did not actually review.

Optionally 404 undocumented routes

By default the import only validates documented routes; anything not in the spec is left alone. If you opt in with generate_route_allow_list, the importer adds one extra rule — a custom-response whose CEL is the negation of every documented route matcher — that returns a 404 with body Not documented in API spec for any request that matches none of them. It is a positive-security-model toggle: only what is in the spec gets through. Because the combined expression has to fit the 2048-byte CEL limit, a very large route set disables the allow-list with a note (route allow-list disabled: N routes exceed the 2048-byte expression limit) instead of generating a broken rule; a scalable, data-driven version is a planned follow-up.

Limits and honest tradeoffs

  • JSON request bodies only. Operations with no request body, or with a non-JSON body, get no rule — they are skipped with a reason. This validates request bodies; it does not check query parameters, headers, or responses.
  • It generates rules; it does not mock your API. The importer produces traffic-policy validation rules and nothing else. It never serves, stubs, or proxies the endpoints in the spec.
  • Hard caps. A spec is capped at 2 MiB, 500 operations, and 500 generated rules; a single operation's schema is capped at 64 KiB (oversize schemas are skipped, not fatal); and parsing runs under an 8-second deadline. Anything over the hard caps is rejected up front with a 413 or 422.
  • Plan gating. Applying needs the traffic-policies entitlement plus the validate-body rule type on your plan; opting into the route allow-list also needs custom-response. If your plan does not include a rule type, you get a 403 rather than a silently-dropped rule.
  • OpenAPI 3 only. Both 3.0.x and 3.1 parse; Swagger / OpenAPI 2.0 does not.

The shape of the flow is the point: you already wrote the contract once, in your spec. The importer turns that contract into edge rules you can preview in full, stage without risk, and re-apply idempotently every time the spec changes.

Enforce your API contract at the edge

Import your OpenAPI spec, preview the exact rules, and stage them disabled until you are ready to enforce.

Create a free account →
Ask an AI to summarise this page
Product
API Gateway Secure Tunnels WAF & Firewall Traffic Inspector
AI
Iris AI AI Gateway
More
Solutions Developers Pricing Enterprise Sign in Get Started Free
Iris