Skip to main content
A policy is one rule the gateway applies to the requests you pick, such as “require an API key” or “100 requests per minute”. Each of an has its own list, so production and preview can have different rules.
Each keeps the policies it was created with. Adding, editing, turning off, or deleting a policy only affects the next deployment. Redeploy to apply a change.

Add a policy in the dashboard

  1. Open the app and click Policies in the sidebar.
  2. Click Add policy, enter a Name, and pick a Type: Key Auth, Rate Limit, Firewall, OpenAPI Validation, or Logging.
  3. In the Rule card, click Add condition for each match condition if the policy should only apply to some requests. With no conditions, the If row reads “Every request”. Open the Then row to set the type’s settings.
  4. Pick the Environment. The default, All environments, adds the same policy to both production and preview.
  5. Click Add policy, then redeploy.
Add policy panel with the Name and Type fields, the Rule card with If and Then rows, and the Environment select set to All environments
Click a policy’s row to edit it in the same panel. The type can’t change there. The Environments switches in that panel turn the policy on or off in production and preview. Switch changes apply when you click Save, together with any other edits in the panel. If you close the panel with unsaved switch changes, the dashboard asks you to discard them. How policies work on the page header explains the evaluation order and what each type rejects. You can’t have two policies with the same type and name in one environment. The dashboard shows a policy with the same type and name in both environments as one row, and suggests opening the existing policy to add it to the other environment instead.

Set policies with the API

Each endpoint takes the project, app, and environment by ID or slug.
  • gateway.listPolicies returns the list in the order it runs, with IDs.
  • gateway.setPolicies replaces the whole list with the policies array you send, in that order. If any policy is invalid, nothing is saved. An empty array removes every policy. Every policy gets a new ID.
  • gateway.updatePolicy changes one policy by policyId. Fields you leave out keep their values, and match: null removes all match expressions. Sending one of keyauth, ratelimit, firewall, openapi, or logging replaces the rule and can change its type.
To replace the list, your root key needs both permissions on the target environment’s gateway policies:
  • unkey:v1:<workspace_id>:projects/<project_id>/apps/<app_id>/environments/<environment_id>/gateway/policies/*#write
  • unkey:v1:<workspace_id>:projects/<project_id>/apps/<app_id>/environments/<environment_id>/gateway/policies/*#delete
Replace the placeholders with resource IDs. Wildcard grants can cover multiple resources, but access remains limited to the root key’s workspace. To list policies, your root key needs environment.*.read_policies, or environment.<environment_id>.read_policies for a specific environment. To change one policy, your root key needs unkey:v1:<workspace_id>:projects/<project_id>/apps/<app_id>/environments/<environment_id>/gateway/policies/<policy_id>#write. Use * in place of a resource ID to grant access to all resources at that level. See root key permissions. A keyspace in an authentication policy must be in the same workspace, or the call fails with err:unkey:data:key_space_not_found. Every change, from the dashboard, API, or CLI, is recorded in the audit log with the full policy list.

Set policies with the CLI

unkey gateway policies list, unkey gateway policies set, and unkey gateway policies update do the same as the three endpoints. Each takes --project, --app, and --environment. unkey gateway policies set takes the full list as --policies '<json array>', and unkey gateway policies update takes --policy-id and --policy '<json object>'.

What’s in a policy

string
required
Shown in the dashboard. 1 to 256 characters.
boolean
required
A turned-off policy is kept but skipped.
MatchExpr[]
Up to 10 expressions. A request must match all of them for the policy to apply. Leave it out to apply the policy to every request.
object
required
Set exactly one. It picks the policy type and holds its settings. See API key authentication, Rate limit, Firewall, OpenAPI validation, and Logging.
Saved policies also have an id. It changes every time gateway.setPolicies replaces the list.

Pick which requests a policy applies to

Each match expression sets exactly one of path, method, header, queryParam, or remoteIp. A request must match every expression in the policy. To match one thing or another, create two policies.
StringMatch
Matches the request path, ignoring case. Set exactly one of exact, prefix, or regex (1 to 1024 characters). Regular expressions use RE2 syntax, and one that doesn’t compile is rejected when you save. ignoreCase is accepted but has no effect on paths. See how paths and methods match.
string[]
One or more of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Matches any of them, ignoring case. GET also matches HEAD.
FieldMatch
name (1 to 256 characters) plus exactly one of present: true or value (a StringMatch like path). With value, it matches if any value of that header matches.
FieldMatch
Same as header, for a query parameter. Names are case-sensitive.
RemoteIpMatch
Matches the remote IP. Set exactly one of in (the remote IP is in one of the ranges) or notIn (the remote IP is in none of them). Each is a list of 1 to 100 IP addresses or CIDRs, such as 203.0.113.7, 203.0.113.0/24, or 2001:db8::/32. A single address is saved as /32 or /128. Ranges with host bits set, such as 10.1.2.3/8, are rejected. The gateway accepts IPv4 connections today, so IPv6 entries match no request yet. In the dashboard this is Client IP, with is in or is not in.
Example: block POST and DELETE on internal paths

How paths and methods match

Many app frameworks send several spellings of a path to the same handler. The gateway matches those spellings too, so a policy on /admin can’t be skipped by requesting /ADMIN. Before matching, the gateway normalizes the path:
  • It ignores case, so /ADMIN matches /admin.
  • It ignores a trailing slash, so /admin/ matches /admin, and a prefix of /admin/ matches /admin.
  • It removes ; parameters from each segment, so /admin;jsessionid=1 matches /admin.
  • It collapses repeated slashes, so /a//admin matches /a/admin.
  • It rejects paths with encoded dot segments such as %2e%2e, and double-encoded slashes such as %252F, with ambiguous_path. If an enabled policy matches paths with a regex, it also rejects %2F (an encoded /) and %5C (an encoded \). Frameworks disagree on how to read these, so the gateway can’t match them to policies reliably.
Patterns match the normalized path, never the raw one. A regex written against ; or .. text doesn’t match. Your app still receives the original, unmodified request. A method match on GET also matches HEAD, because most frameworks answer HEAD with the GET handler.
Express, Rails, Laravel, and other frameworks can turn a POST into another method with an X-HTTP-Method-Override header or a _method field. The gateway can’t see that change. If an API key or rate limit policy matches a method other than POST, add POST to it too. Don’t add POST to a firewall policy, because that blocks every POST to the path. Turn off method override in your app instead.

Protect broadly, then narrow

Matching on a list of protected paths is fragile. A route you forget, or a spelling your framework accepts that the gateway doesn’t, stays open. Put API key authentication on a broad scope instead: every request, or a prefix such as /api/ that holds all your private routes. Serve public routes outside that prefix, or from a separate app. If a narrower path needs a stricter API key policy, such as /api/admin/ with a different keyspace, put that policy above the broad one. The first API key policy that succeeds decides who the caller is, so a stricter policy below a broad one never runs.

Put policies in the right order

The gateway runs matching, enabled authentication, rate limit, firewall, and OpenAPI validation policies from top to bottom. In the dashboard, drag a row by its handle to reorder. Order matters in four ways:
  • IP blocks before authentication. Put a firewall policy that blocks by remoteIp above your API key policies, so blocked clients can’t try keys.
  • Authentication first. The first API key policy that succeeds decides who the caller is. Later API key policies are skipped.
  • Rate limits by caller go after authentication. A rate limit that counts by authenticatedSubject or principalField needs to know the caller, so it needs an enabled API key policy above it. Saving one without that policy fails with a validation error.
  • A rejection stops enforcement. After a firewall block, a failed key check, a used-up rate limit, or an OpenAPI error, no later authentication, rate limit, firewall, or validation check runs.
Logging policies apply to accepted and rejected requests, regardless of their position. If several match, their settings combine. Body redaction from matching, enabled OpenAPI policies also applies when a request is rejected before validation.

Limits

  • 50 policies per environment.
  • 10 match expressions per policy.
  • 100 entries per remoteIp list.
  • 5 keyspaces and 10 key rate limits per API key policy.
  • 5 identifiers per rate limit policy.
  • Match strings up to 1024 characters, and names up to 256.

Next steps

API key authentication policy

Verify Unkey keys and forward the principal.

Rate limit policy

Limit by IP, header, path, subject, or principal field.

Firewall policy

Deny the requests your match expressions select.

Logging policy

Capture headers, query data, and bodies for matched requests.
Last modified on November 11, 2026