Skip to content

Request Rules

Inside an allow_http block, allow_requests and deny_requests decide which requests may be sent.

allow_http "api.example.com" {
allow_requests ["GET /v1/**", "POST /v1/orders"]
deny_requests ["DELETE /**"]
}
METHOD /path/glob

The method is an upper-case token or *. The path is host-relative and starts with /.

GlobMatches
/v1/**/v1 itself and everything beneath it
/v1/*exactly one segment under /v1 — not /v1/a/b
/assets/*.json* also works within a segment
/**everything

** must be a whole segment: /v1/a** is an error.

  1. Any matching deny_requests refuses.
  2. A graphql block matching the path claims the request (see GraphQL).
  3. Any matching allow_requests permits.
  4. Otherwise refused — nothing granted it.

Blocks covering the same host are read as a union, so two fragments each granting one method grant both.

This is the part that matters for security. Before any rule is consulted the request target is:

  1. split from its query string and fragment
  2. collapsed for repeated slashes
  3. percent-decoded once
  4. stripped of ;-parameters
  5. resolved for . and ..

So all of these are gated as /admin, and a deny_requests ["* /admin"] catches every one:

/admin /./admin /foo/../admin
/%61dmin /admin;x=1 /admin%3Bignored

Anything ambiguous is refused, not guessed: an invalid escape, a byte sequence that is not UTF-8, a control character, or a .. that climbs above the root.

Two deliberate details:

  • A %2F is data, not a separator. /repos/octo%2Fcat is two segments, so /repos/* matches it and /repos/octo/cat does not.
  • Decoding happens once. /%2561dmin gates as /%61dmin, not /admin — decoding twice is how a filter and its origin end up seeing different paths.

Deliberately out of scope: query strings, request headers, response bodies, and WebSocket frames. A rule matches the method and the path, nothing else.

Start with what policy draft observed, then generalise by hand:

# observed
allow_requests ["GET /v1/orders", "GET /v1/orders/8813", "POST /v1/orders"]
# narrowed
allow_requests ["GET /v1/orders/**", "POST /v1/orders"]

Replace a path with a glob only where the tool will genuinely vary it between runs.