Traffic Policy Rules

Security Rules

Protect your endpoint with IP restrictions, rate limiting, authentication, and circuit breaking.

IP Restriction

Allow or deny requests from specific IP ranges (IPv4/IPv6 CIDR).

FieldDescription
allowList of allowed CIDRs/IPs
denyList of blocked CIDRs/IPs
messageCustom denial message

Rate Limit

Limit requests per time window. Protect your backend from abuse.

FieldDescription
limitMax requests in window (must be > 0)
windowTime window in seconds
scopeip (per-client) or endpoint (global)
messageCustom rate limit message

Verify Webhook

Validate incoming webhook signatures against a shared secret.

FieldDescription
secretHMAC shared secret
signature_headerHeader containing signature (default: X-Hub-Signature-256)
algorithmsha256 or sha1
signature_prefixPrefix to strip (e.g., sha256=)
messageCustom error message

The secret is write-only: reads answer ****. Send **** back on an update to keep it, or a new value to replace it.

JWT Validation

Validate JSON Web Tokens at the edge. Supports HMAC and RSA/ECDSA algorithms. Request phase only (on_http_request). This rule is how ngris checks JWTs — it is not an endpoint auth mode (those are none, basic and SSO, under Access Controls), so it works alongside any of them.

FieldDescription
algorithmHS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
secretHMAC secret (for HS* algorithms)
jwks_urlJWKS endpoint URL (for RS*/ES* algorithms)
token_locationWhere to find token: header, cookie, or query
token_nameHeader/cookie/query param name
token_prefixPrefix to strip (e.g., Bearer )
issuerExpected iss claim
audienceExpected aud claim
required_claimsList of claims that must be present

The secret is write-only: reads answer ****. Send **** back on an update to keep it, a new value to replace it, or "" to remove it (an RS*/ES* rule uses jwks_url).

API Key Validation

Validate API keys at the edge. Manage keys in the API Keys tab.

FieldDescription
key_locationheader, query, or cookie
key_nameHeader/param/cookie name (e.g., X-API-Key)
messageCustom 401 error message

Body Validation

Validate JSON request bodies against a JSON Schema (draft 2020-12). Only validates application/json requests.

FieldDescription
schemaJSON Schema (max 64KB)
max_body_sizeMax body bytes to read (0–5MB, default 1MB)
messageCustom error prefix

Circuit Breaker

Stop sending traffic to failing backends. The circuit opens when error rates exceed a threshold, and automatically recovers after a cooldown period.

FieldDescription
error_thresholdError percentage to trigger open (1–100, default 50)
window_sizeEvaluation window in seconds (1–300, default 30)
min_requestsMinimum requests before evaluating (1–10000, default 10)
cooldown_timeSeconds to stay open (1–600, default 60)
half_open_maxTest requests allowed in half-open state (default 3)
error_codesStatus codes considered errors (default: 500, 502, 503, 504)
messageCustom 503 message

Client certificates (mTLS)

Client certificates are not a traffic-policy rule. Turn them on in the endpoint's Mutual TLS settings (or with the agent's --mutual-tls flag): pick a mode and the client CAs you trust — see Mutual TLS. mTLS works on HTTP(S) endpoints only.

Traffic Rules

Control traffic flow with caching, redirects, custom responses, and webhooks.

Cache Response

Cache responses at the edge (Redis-backed). Serve cached content without hitting your backend.

FieldDescription
ttlCache TTL in seconds (1–86400)
path_patternRegex to match paths (empty = all)
methodsHTTP methods to cache (GET, HEAD)
status_codesCacheable statuses (200, 301, 302, 404)
max_body_sizeMax response body to cache (100KB–10MB)
vary_headersHeaders to include in cache key (max 10)
ignore_queryIgnore query string in cache key
bypass_headerHeader that bypasses cache (e.g., X-No-Cache)

Image Optimization

Resize and re-encode image responses at the edge — smaller, modern-format images without touching your backend. Runs in the response phase on any endpoint, both agent tunnels and hosted apps. Only image responses (Content-Type: image/*) are transformed; everything else passes through untouched.

Transforms are driven per-request by the URL: add ?w=/?width= to resize, ?q=/?quality= to set quality, and ?format=webp|avif|jpeg|png to force an output format. With no ?format=, the client's Accept header auto-negotiates a modern format (WebP/AVIF). Each optimized variant is cached at the edge (like Cache Response), and encoding never upscales past the original.

FieldDescription
default_qualityEncode quality 1–100 used when ?q= is omitted (default 82)
max_widthClamp the requested ?w= to this many pixels; never upscales (0 = system max)
allowed_formatsFormats a request may target — subset of webp, avif, jpeg, png (empty = all)
disable_auto_formatOpt out of Accept-based WebP/AVIF negotiation when no ?format= is given
cache_ttlSeconds to cache each optimized variant at the edge (default 86400)

Force HTTPS

Redirect all HTTP requests to HTTPS with a 301 status code. No configuration needed. Also available as a toggle on the Endpoint Overview tab.

Redirect

Redirect requests to a different URL with configurable status code.

FieldDescription
urlDestination URL (supports $1, $2 capture groups)
patternRegex to match URL path (optional)
statusHTTP status: 301, 302, 307, or 308

Custom Response

Return a custom response without forwarding to your backend. Useful for maintenance pages, health checks, or static content. Subject to AI content safety validation.

FieldDescription
statusHTTP status code (100–999)
bodyResponse body (max 100KB)
content_typetext/html, application/json, text/plain, text/xml, text/css, or application/javascript
headersAdditional response headers

Custom Error Page

Replace error responses from your backend with custom-branded error pages. Supports template tokens for dynamic content.

FieldDescription
status_codesSpecific codes to match (400–599)
status_rangeRange: 4xx, 5xx, 4xx-5xx
contentError page template (max 10KB)
content_typeContent type (default: text/html; charset=utf-8)
error_sourceFilter: origin, edge, or both
response_status_codeOverride status code (0 = keep original)
Template Tokens: {{.ClientIP}}, {{.StatusCode}}, {{.Reason}}, {{.Timestamp}}, {{.Hostname}}, {{.Method}}, {{.Path}}, {{.UserAgent}}, {{.RequestID}}

Webhook

Forward request or response data to an external HTTP endpoint. Useful for logging, notifications, or custom processing pipelines.

FieldDescription
urlDestination URL (must be HTTP/HTTPS)
methodHTTP method: GET, POST, PUT, PATCH, DELETE, HEAD
headersHeaders to include
bodyBody template (supports {{.method}}, {{.path}}, {{.host}})
timeoutTimeout in seconds (0–30, default 5)

Modification Rules

Modify request and response headers and rewrite URLs.

Add Headers

Inject custom headers into requests (before forwarding) or responses (before returning to client).

FieldDescription
headersKey-value map of headers to inject

Send the bot score to your origin. A request-phase Add Headers rule can pass each request's bot score to your app, so the app can decide for itself:

Example
{"type": "add-headers", "phase": "on_http_request",
 "config": {"headers": {"X-Ngris-Bot-Score": "${req.bot_score}"}}}

Your origin then receives a header such as X-Ngris-Bot-Score: 95 (1–99, low means likely automated). A value the visitor sends in that header is replaced. Nothing is sent until you add the rule; the Send the bot score to your origin switch on the endpoint's Under attack card adds or removes exactly this rule.

Remove Headers

Strip specific headers from requests or responses.

FieldDescription
headersList of header names to remove

URL Rewrite

Rewrite request URLs before forwarding to your backend using regex patterns with capture groups.

FieldDescription
patternRegex to match the selected target
replacementReplacement with $1, $2 capture groups
targetWhat to rewrite: path (default), query, host, or path_query
Terminal
# Example: Strip /api/v1 prefix
# Pattern: ^/api/v1/(.*)$
# Replacement: /$1

API Gateway Rules

Additional edge rule types for full API-gateway behavior. Add them from the same Traffic Policy editor.

CORS

Answer CORS preflight (OPTIONS) requests at the edge and add Access-Control-Allow-* headers to responses — no backend changes. Runs in the request phase.

FieldDescription
allow_originsExact origins (e.g. https://app.example.com) or a single *. * can't be combined with credentials.
allow_methods / allow_headersAllowed methods / request headers (blank = echo what was requested).
expose_headersResponse headers exposed to JavaScript.
allow_credentialsAllow cookies / Authorization on cross-origin requests.
max_ageSeconds a browser may cache the preflight.

Set Variables

Compute named variables from CEL expressions into a per-request bus. Later rules read them in conditions (vars.tier == "gold") and in header / redirect / custom-response values (${vars.tier}). Runs in the request phase. One name = expression per line.

Example
tier = req.headers["x-tier"]
is_admin = req.path.startsWith("/admin")
# then, in an Add Headers rule:  X-Plan: plan-${vars.tier}

Replace Body

Find/replace in the response body (literal or regex) — e.g. rewrite internal URLs or redact strings. Applies to complete, uncompressed responses up to a size cap; streamed and already-compressed responses pass through untouched.

FieldDescription
find / replaceText to find and its replacement ($1.. supported when regex).
regexTreat find as a regular expression.
max_bodySkip bodies larger than this (bytes). Default 1 MiB.

Compress

Compress eligible response bodies at the edge, negotiating br (Brotli, preferred) or gzip from the client's Accept-Encoding. Compressible text/JSON types only; never inflates or double-compresses.

FieldDescription
min_sizeDon't compress below this many bytes. Default 1024.
content_typesCompressible content-type prefixes. Blank = text/JSON/JS/XML/SVG defaults.

WAF

A web application firewall that inspects each request (URI, headers, and optionally an inline body) against an OWASP-style ruleset via the Coraza engine and blocks or logs matches. Runs in the request phase. The built-in core ruleset covers SQL injection and XSS (libinjection), path traversal / sensitive-file access, OS-command and template injection, Log4Shell/JNDI, and known scanner user-agents — no external OWASP CRS to manage. Advanced users can supply their own Coraza SecLang.

Mode defaults to detect (log only) so turning the WAF on can never break a live endpoint on day one — watch the logs, then switch to block. The compiled engine is cached per ruleset; endpoints without a WAF rule pay nothing.

FieldDescription
modedetect (default, log only) or block (deny matches with 403).
rulesetcore (built-in baseline, default), custom (your SecLang only), or core+custom.
custom_rulesCoraza SecLang directives (required when ruleset includes custom). Max 64 KB.
inspect_bodyAlso inspect the request body (bounded by max_body_kb, in-memory). Off = URI + headers only.
max_body_kbKB of request body inspected. Default 128, max 1024.

Bot Management

Match the client's TLS JA3 fingerprint — computed at the edge from the ClientHello — against allow/deny lists to block or log automated clients. Runs in the request phase. Mode defaults to detect (log only). Fingerprinting is only computed when a bot-management rule exists on some endpoint, so endpoints not using it pay nothing.

The JA3 is GREASE-normalized (RFC 8701 random values stripped) so it's stable per client. Run in detect mode first and copy the exact fingerprints the edge logs into your denylist — those match what the matcher computes. The fingerprint is also readable in any rule's CEL guard as conn.ja3 (e.g. conn.ja3 == "e7d705…" && req.path.startsWith("/api")).

FieldDescription
modedetect (default, log only) or block (deny with 403).
ja3_denylistJA3 hashes (32 hex chars) to block — known bad bots.
ja3_allowlistWhen non-empty, blocks any client whose JA3 is NOT listed (allow-known-clients-only).
block_missingBlock when no JA3 could be computed (non-TLS / parse failure). Off by default.
messageOptional custom block message.

Challenge (Under attack mode)

Make each visitor's browser pass a short automatic check before the request reaches your endpoint. The browser solves a small JavaScript proof-of-work (nothing is sent to a third party), and the edge then sets a signed clearance cookie so later requests go straight through. By default there is nothing to click; set interaction to checkbox or puzzle to also ask visitors to tick a box or slide a puzzle piece into place. It is a request-phase rule on the endpoint, so it protects agent tunnels and hosted apps alike, and it is available on every plan. The quickest way to turn it on is the Under attack mode switch on the endpoint's Overview tab.

The challenge runs before every other request rule and before the edge cache, so no cached page is served to a visitor who has not passed. The clearance cookie only works on the site that issued it, in the browser that solved the check and (by default) from the same network, so it cannot be copied to other machines.

What visitors see: a page naming your site in large type, with a progress line under it and the line “Making sure you're a person, not a bot”. Your message, if you set one, appears below it. With the automatic type the check needs no clicks and usually finishes in about a second; with checkbox or puzzle the visitor ticks the box or slides the piece first. Then they land on the page they asked for. If their browser blocks cookies or has JavaScript turned off, the page says so and how to fix it. The footer shows “Protected by ngris” and a Ref code: the request's ID (its X-Request-Id), so a visitor who is stuck can quote it to you or to ngris support. To find that request's log entry, filter the traffic logs by request_id. The page follows the visitor's light or dark setting.

What gets blocked: requests that cannot show the check page and have no clearance — API calls, fetch/XHR from other sites, POSTs, WebSocket upgrades, curl, webhooks from other services and crawlers we can't verify — get a 403 with the header X-Ngris-Challenge: required. Add your API and webhook prefixes to skip_paths, or your office and monitoring IPs to allow_ips. Use the rule's expression to challenge only part of the site (for example req.path.startsWith("/login")); a request whose path isn't in plain form (//login, /x/../login) is challenged whatever the expression says, so it can't slip past. Expressions compare paths exactly and case-sensitively (/ADMIN doesn't match startsWith("/admin")), so if your origin treats paths case-insensitively, challenge the whole site and use skip_paths for the exceptions, or lower-case the path in the expression.

Verified search engines and AI crawlers pass. A request from one of the crawlers below goes through without a check when its IP address is in the list its operator publishes for it, so your site stays in search results and AI answers while the rule is on. A bot that only copies one of these user agents is checked like anyone else (and scores fake_crawler). To check verified crawlers too, set allow_verified_bots to false, or turn off Let verified search engines and AI crawlers through on the Under attack card. Other crawlers are checked.

OperatorCrawlers (user agent)
GoogleGooglebot (all variants), Storebot-Google, Google-InspectionTool, GoogleOther, Google-CloudVertexBot; AdsBot-Google, APIs-Google, Mediapartners-Google, Google-Safety; and the fetchers people trigger: FeedFetcher-Google, Google-Site-Verification, Google-Read-Aloud, GoogleProducer, Google-CWS, Google-GeminiNotebook / Google-NotebookLM, GoogleMessages, Google-Pinpoint
Microsoft BingBingbot
AppleApplebot
DuckDuckGoDuckDuckBot, DuckAssistBot
OpenAIGPTBot, OAI-SearchBot, ChatGPT-User
PerplexityPerplexityBot, Perplexity-User

Fetchers such as ChatGPT-User, Perplexity-User or Google-Read-Aloud fetch a page when one of their users asks for it, so anyone can make them load a URL on your site. Turn allow_verified_bots off if that matters for the protected paths. The operators' IP lists are refreshed every hour; while a list can't be loaded, that operator's crawlers are checked like everyone else.

Challenge only suspicious visitors. By default (mode always) every visitor without a clearance is checked. Set mode to smart, or turn on Challenge only suspicious visitors on the Under attack card, and the rule decides by each request's bot score (1–99, low means likely automated): a score at or above pass_score (default 70) goes through without a check, a score below block_score (default 10) gets a 403 without a check, and every score in between gets the check. A verified crawler is never blocked by its score. With today's scores, a normal browser (about 95) is not checked; a browser from a cloud provider's network or a Tor exit, or one missing headers every browser sends, is checked; scripts, empty user agents, fake crawlers and blocklisted networks (5 or less) get the 403 straight away. The trade-off: a bot that imitates a browser well can score high and get through without the check, which always would have asked of it. Smart mode is off until you turn it on.

Re-checking busy visitors. Each clearance has its own ID, and the edge counts the requests made with it. A visitor who sends more than clearance_max_rpm requests in a minute (default 600) loses the clearance and is checked again, with a stronger check, so passing once doesn't buy a flood. A page with many images and scripts stays well under 600 a minute.

What it does not do: it stops scripts and tools that don't run JavaScript and makes every network pay for a check, but a headless browser can pass it. Pair it with a rate limit for floods from real browsers.

FieldDescription
interactionWhat the visitor does: automatic (default, nothing), checkbox (tick “I'm a person”) or puzzle (slide a piece into a gap, by mouse, touch or arrow keys). The browser check runs in every type, and for the puzzle every attempt costs a fresh one. Visitors cleared by a stronger type aren't asked again by a weaker one; switching a rule to a stronger type asks everyone again. The interactive types add a human step that generic bots skip; they don't stop a bot written for this page.
difficultylow, medium (default) or high. Medium takes a fraction of a second; high about a second on a phone.
clearance_ttlSeconds a visitor stays cleared after passing. 300–86400; default 3600.
bind_ipTie the clearance to the visitor's network (IPv4 /24, IPv6 /64) as well as their browser. Default true; set false if your visitors switch networks often.
skip_pathsPath prefixes that are never challenged (/api/, /webhooks/). They match whole path segments, so /api covers /api and /api/v1 but not /apix. Paths with .., // or an encoded slash are always challenged. Up to 50.
allow_ipsIPs or CIDRs that are never challenged. Up to 200.
allow_verified_botsLet verified search engines and AI crawlers (the table above) through without a check. Default true; set false to check them too.
modealways (default): check every visitor. smart: decide by bot score, see Challenge only suspicious visitors.
pass_scoreSmart mode: a bot score at or above this goes through without a check. 1–99; default 70.
block_scoreSmart mode: a bot score below this gets a 403 without a check (never a verified crawler). 1–99, lower than pass_score; default 10.
clearance_max_rpmRequests per minute per visitor before re-checking (see Re-checking busy visitors). Default 600; 0 means the default.
messageOptional text shown on the check page. Up to 300 characters.

Rate limit — flexible keying

The Rate Limit rule's Scope now supports per-IP, per-endpoint, and per header, cookie, or JWT claim value (with a key name). Counting is shared across edges. Header/cookie/JWT-claim scopes bucket by client-controlled values (the JWT claim is not signature-verified) — use them for fair sharing, and pair with a per-IP rule as the hard ceiling against abuse.

Impact preview (blast radius)

In the rule editor, replayable rule types show a Preview impact button. It replays your last ~2 hours of captured traffic through the unsaved rule and reports how many requests would be newly blocked / throttled / redirected, how many previously-successful requests would break, the client IPs affected, and a safe / caution / risky verdict — so you can check the blast radius before you save. Requires request capture enabled on the endpoint.

Configure from the agent CLI

You don't have to build a policy in the dashboard — the agent can attach one to its endpoint at connect time, from shorthand flags and/or a policy file. The rules are exactly the ones above; they're enforced at the edge, so nothing runs on your machine. Attaching a policy requires the traffic-policy entitlement on your plan; without it the agent logs a clear error and keeps serving traffic (the tunnel is never blocked).

Shorthand flags

The most common rules have a dedicated flag (examples on the right). Each compiles to one rule in the phase shown. The --cidr-* flags are comma-separated; the header flags are repeatable.

FlagRulePhase
--cidr-allow <cidr,…>restrict-ips (allow-list)connect
--cidr-deny <cidr,…>restrict-ips (deny-list)connect
--rate-limit <N/window>rate-limit, per IP (e.g. 100/1m)connect
--request-header-add <k:v>add-headersrequest
--request-header-remove <k>remove-headersrequest
--response-header-add <k:v>add-headersresponse
--response-header-remove <k>remove-headersresponse
--compressioncompress-response (gzip/br)response
--force-httpsforce-https (redirect HTTP→HTTPS)request
--circuit-breakercircuit-breaker (trip on origin 5xx; 50%/10s defaults)request
--verify-webhook <prov:secret>verify-webhook signatures (stripe/github/slack)request

Policy file

For anything beyond the shorthands, pass a YAML or JSON file with --traffic-policy <file> (see policy.yaml on the right). It can list rules two ways, which may be combined: a flat rules: list, or phase-keyed blocks (on_tcp_connect, on_udp_connect, on_http_request, on_http_response).

KeyDescription
typeRequired. Any rule type from the catalog above — restrict-ips, rate-limit, add-headers, remove-headers, force-https, compress-response, and the rest.
configRule-specific settings — the exact fields documented for that rule above (e.g. allow/deny for restrict-ips, limit/window/scope for rate-limit, headers for add-/remove-headers).
phaseOptional. on_tcp_connect, on_udp_connect, on_http_request, or on_http_response. See phase resolution below.
expressionOptional CEL condition — the rule only runs when it's true, e.g. req.path.startsWith("/api"). Blank = always. Every request also carries a bot score: req.bot_score (1–99; low means likely automated, high looks like a person's browser), req.bot_class (browser, automation, crawler or unknown), req.bot_reasons (why points were taken off, e.g. automation_ua, browser_no_accept_language, and from the client's network: datacenter and tor for a browser from a cloud provider or a Tor exit, spamhaus_drop for a blocklisted network, fake_crawler for a user agent that claims a known crawler from an IP its operator doesn't publish; and reputation when the client's IP recently failed checks or was blocked anywhere on ngris — failed challenges, WAF blocks, ngris-sentinel blocks and abuse blocks, 20 points per kind of event, wearing off within an hour, never below 1) and req.verified_bot (true for a search or AI crawler coming from its operator's published IP addresses — see Challenge for the list). For example, req.bot_score < 30 matches curl, scripts and browser look-alikes, and req.bot_class == "crawler" && !req.verified_bot matches crawlers that aren't verified. The cloud and Tor reasons lower only a browser's score: an API client or script from a cloud server is normal. Reputation is shared across ngris: only the score adjustment for an IP crosses accounts, never your traffic or settings, and it never blocks by itself — it acts only through a smart challenge or your own rules.
nameOptional label shown in the dashboard and logs. Defaults to the rule type.

Phase resolution. An explicit phase: on the rule wins; otherwise the phase-key block it's nested under; otherwise a default by type — restrict-ips and rate-limit attach at connect (on_udp_connect for UDP tunnels), compress-response at the response, everything else at the request.

The phase-keyed form is equivalent — nest rules under the phase block instead of writing phase: on each rule.

File + flags combine. If you pass both a --traffic-policy file and shorthand flags, the file's rules come first and the shorthands are appended — all into one managed policy named ngris-agent.
Applied once, at connect. The policy is attached right after the agent authenticates. Editing the file or flags takes effect on the next ngris start — a running agent does not re-read it. Re-running replaces the ngris-agent policy; any rules you build in the dashboard (under a different name) are left untouched.
Plan-gated. Attaching a policy requires the traffic-policy entitlement. If your plan lacks it, the agent logs the rejection and keeps forwarding traffic unfiltered rather than failing the tunnel.
These settings need a token. A key (Preview) logs in tunnels only. With --basic-auth, --mutual-tls / --client-ca, --traffic-policy or its shorthand flags, or webfm --rw (also in the config file), the agent logs in with its token; without a token it doesn't start.

Coming from ngrok?

Point --traffic-policy straight at your existing ngrok traffic-policy.yaml. ngris auto-detects the ngrok format (phase blocks holding rules with an actions: list) and converts it to native ngris rules on the fly — no rewrite. ngrok groups many actions under one rule; ngris splits them into one rule per action, carrying the rule's expressions onto each, preserving order and phase.

  • Map directly: add-headers, remove-headers, restrict-ips, url-rewrite, redirect, custom-response, compress-response.
  • Rewritten, with a note to review: rate-limit (ngrok's sliding window → a fixed window); deny → a custom-response (403 by default); response-body-find-replace → replace-body; owasp-crs-request → waf in detect mode; verify-webhook (github, slack, stripe or a generic HMAC).
  • Rewrite before use: jwt-validation, set-vars and circuit-breaker are copied as-is, but ngris uses different fields; request-body-find-replace, owasp-crs-response and deny under on_tcp_connect become rules ngris doesn't run in that phase. ngris refuses these as written, and the agent attaches its policy in one piece, so one refused rule keeps the whole file from attaching — the log line says what to change.
  • Endpoint-level on ngris: oauth / oidc / basic-auth are configured as the endpoint's access mode (SSO or basic), not a policy rule — the agent tells you so.
  • No silent drops: anything without an ngris equivalent (e.g. forward-internal, sse-find-replace) is skipped and logged with the reason.
It's free. Traffic policies — header rewrites, rate limits, IP rules, WAF — are included on ngris with no per-request metering. Bring your ngrok policy and run it: ngris http 3000 --traffic-policy your-ngrok-policy.yaml.
Shorthand flags
# Lock an endpoint to an office network, cap it, and force HTTPS
ngris http 3000 --cidr-allow 203.0.113.0/24 --rate-limit 100/1m --force-https

# Strip a leaky request header; add a security header + compress on the way out
ngris http 3000 --request-header-remove X-Powered-By \
                --response-header-add X-Frame-Options:DENY --compression
policy.yaml
# Attach with:  ngris http 3000 --traffic-policy policy.yaml
rules:
  # Allow only two networks (restrict-ips defaults to the connect phase)
  - type: restrict-ips
    name: office-only
    config:
      allow: [203.0.113.0/24, 198.51.100.10/32]
      message: "Not on the allow-list"

  # 100 requests / minute, per client IP
  - type: rate-limit
    config:
      limit: 100
      window: 60          # seconds
      scope: ip

  # Add security headers to every response
  - type: add-headers
    phase: on_http_response
    config:
      headers:
        X-Frame-Options: DENY
        Strict-Transport-Security: "max-age=31536000"

  # Force HTTPS, but only on the API path (optional CEL condition)
  - type: force-https
    expression: req.path.startsWith("/api")
    config: {}
policy.yaml (phase-keyed)
on_tcp_connect:
  - type: rate-limit
    config: { limit: 100, window: 60, scope: ip }
on_http_response:
  - type: compress-response
    config: {}
  - type: image-optimization
    config:
      default_quality: 82
      max_width: 2048
      allowed_formats: [webp, avif, jpeg, png]
Was this helpful?