Skip to content

Rule format

Rules decide what happens to a request that leaves the sandbox. You normally write them in the web app; a gate that is not logged in reads the same rules from a file (noset run --rules FILE). The gate and the web app’s test use one matcher, so a test shows what the gate does.

version: 1
rules:
- id: finance-delete-customer # required, unique, 1 to 128 characters
name: Delete a customer # optional: shown to people
description: Deleting a customer needs approval
decision: ask # allow | deny | ask
approvers: {team: finance, count: 2} # ask only: who approves
http:
method: DELETE # a method or a list; empty: any method
host: api.example.com # a host, "*.example.com" or a list; empty: any host
path: '/users/[^/]+' # a regular expression for the whole path; empty: any path

Unknown fields are errors, so a typo cannot quietly change what a rule matches.

  • method: an HTTP method, upper or lower case. *, ANY or empty mean any method.
  • host: a host name or IP address, without port; a rule matches every port. *.example.com covers every subdomain of example.com, not example.com itself. A pasted URL is reduced to its host.
  • path: an RE2 regular expression of at most 500 characters that must match the whole path, as if written ^(?:…)$. /admin(/.*)? covers /admin and everything below it. Add (?i) for servers that ignore case. The query string is not part of the path.
Decision The gate
allow lets the request through and records it
deny answers 403 and records it
ask holds the request until people decide
  • The strictest decision wins, whatever the order. When several rules match, deny beats ask beats allow. An allow rule can never switch off an ask or deny rule.
  • Several ask rules: when several ask rules match, each one’s approvals are needed, from its own team. One denial denies.
  • No rule matches: the request is allowed and not recorded.

The gate reads a request every way a server could, so a disguise does not get past a rule:

  • Methods: the request method and every override: the X-HTTP-Method-Override, X-HTTP-Method and X-Method-Override headers, and _method in the query string or the body. POST /users/1 with X-HTTP-Method-Override: DELETE matches a DELETE rule.
  • Paths: decoded once and until stable, with %2F decoded and kept, with ;params dropped and kept, cleaned of ., .. and repeated slashes, with and without a trailing slash, and the paths in X-Original-URL and similar headers.
  • Hosts: a rule on a host name also matches a request sent straight to that name’s addresses.

When unsure, the gate reads more into a request, never less: it asks more often, never less.

What rules do not see: a different endpoint that does the same thing (POST /users/1/erase), and headers that select an account or a tenant. Cover them with their own rules.

version: 2 adds what rules made from templates need. Each addition only narrows a match.

version: 2
rules:
- id: drive-delete
decision: ask
approvers: {team: data, count: 1}
http: # a list: the rule matches when any of them does
- method: DELETE
host: www.googleapis.com
path: '/drive/v[0-9]+/files/[^/]+'
- method: PATCH
host: www.googleapis.com
path: '/drive/v[0-9]+/files/[^/]+'
body: {json: {trashed: true}} # only when the JSON body sets trashed to true
- id: s3-batch-delete
decision: ask
approvers: {team: data, count: 1}
http:
method: POST
host: '*.amazonaws.com'
query: {delete: '*'} # the parameter is there, with any value
  • query: a parameter name and '*' (present) or a regular expression its whole value must match.
  • body.json: a dotted path and a value the JSON body must have there.
  • body.graphql: GraphQL mutation names, or ['*'] for any mutation.
  • except: requests inside a match that the rule does not cover.

When the gate cannot read a body (too large, an encoding it cannot undo), every body condition counts as met: the request is asked about rather than let through.

  • The gate holds an ask request for 60 seconds. Without a decision, the agent gets 403 and Noset: waiting for approval (id …), try again later.
  • Approved: a single-use approval for exactly that request, as it was sent, valid 15 minutes.
  • Denied: 403 with the approver and the reason. A deny rule answers 403 with Noset: blocked by rule "…".

Responses carry X-Noset-Decision (pending, denied, blocked) and X-Noset-Request headers, so tools can tell a held request from a real error.