Configure Header ACL access rules
Access rules perform admission control based on HTTP request headers, with three scopes: global, key group, and access key. Rules are matched in ascending sort_order; the first match decides allow/deny.
Entry point: Console → Access Keys → Access Rules tab.
Note: access rules apply to both the inference routes (
/v1/chat/completions,/v1/messages, etc.) and the model list routes (/v1/models,/v1beta/models). When a header pattern is denied, model-list queries are also blocked. Operators must be aware of this behavior to avoid misconfiguration that prevents clients from fetching the model list.
Rule fields
| Field | Description |
|---|---|
| Rule Name | Unique identifier for the rule, globally unique. Use a scope prefix (e.g. key:mykey__rule1) to avoid naming collisions. Cannot be changed after creation. |
| Order | Sort weight, matched in ascending order; smaller numbers are evaluated first. Rules with the same order are evaluated in scope order Global → Key Group → Access Key (i.e. under the same order a global rule matches first); therefore a key-level or group-level rule that wants to override a same-order global rule must use a smaller order. |
| Header Name | The HTTP request header name to match (e.g. User-Agent, X-Forwarded-For). Case-insensitive. Leave blank when Match Type is any (that type does not check a request header). |
| Match Type | The match method, see the table below. |
| Match Value | The value to match. Not needed for the exists / absent / any types. |
| Action | allow or deny (deny returns 403). |
| Scope | The scope: Global (all requests), Key (one or more access keys), Group (one or more key groups). Key and Group are mutually exclusive, and both support multi-select. |
| Scope value | When Scope is Key or Group, select the target key or key group from the dropdown. |
| Enabled | The rule's on/off switch. A disabled rule is ignored at load time and does not affect other rules. |
The table above shows console UI labels; the configuration examples below use the underlying field names, which map as: Header Name =
condition_key, Match Type =condition_type, Match Value =condition_value, Action =action, Scope =scope, Order =sort_order.
Match types
| Type | Description | Example |
|---|---|---|
exact | The header value exactly equals Match Value | User-Agent = BadBot/1.0 |
prefix | The header value starts with Match Value | User-Agent starts with python-requests/ |
regex | The header value matches a regular expression | User-Agent matches `(?i)curl |
exists | The header is present (regardless of value) | Check whether X-Custom-Header is present |
absent | The header is absent | Check whether Authorization is missing |
any | The condition is always true; no header is checked, and the rule only takes effect via its Scope | "Allow/deny all" for a key: Match Type=any + Scope=Key, Header Name left blank |
See Header ACL rule fields for the full reference on field semantics, match types, regex matching semantics, and evaluation logic.
Common usages
- Block crawlers:
condition_key = User-Agent,condition_type = regex,condition_value = (?i)scrapy|crawl|spider,action = deny,scope = Global. - Restrict a specific IP range:
condition_key = X-Forwarded-For,condition_type = prefix,condition_value = 192.168.,action = deny,scope = Global. ⚠️X-Forwarded-Foris a client-forgeable header — it is only reliable when there is a trusted reverse proxy in front of the gateway that overwrites it; otherwise a malicious client can fill in that header arbitrarily to bypass the restriction. For real IP admission control, prefer the real source IP seen directly by the gateway (withTRUSTED_PROXIES) over the forgeableX-Forwarded-For. - Only allow requests with a custom header:
condition_key = X-Api-Token,condition_type = absent,action = deny,scope = Global(deny when the header is missing). - Restrict a specific key group: set
scope = Group, select the target key group; the rule only affects keys within that group. - Allow/deny all for a key (no header condition): set Match Type =
any, Scope =Keyand select the target key, leave Header Name blank — the rule applies to all requests from that key. To use it to override a global deny, give it a smaller order than the global rule (under the same order, global wins).
Verifying that a rule takes effect
After configuring a rule, verify with paired curl requests. Using the "block crawlers" rule above (User-Agent containing scrapy → deny) as an example:
# Control group: normal UA, expected to pass (200)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:7890/v1/chat/completions \
-H "Authorization: Bearer <your access key>" \
-H "User-Agent: my-app/1.0" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'
# Test group: UA matches, expected to be blocked (403)
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:7890/v1/chat/completions \
-H "Authorization: Bearer <your access key>" \
-H "User-Agent: scrapy/2.11" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'Returning 200 and 403 respectively means the rule works. If the test group still returns 200, check: ① whether the rule's Enabled is on; ② whether it is matched first by an allow rule with a smaller order; ③ whether the scope covers this key.
Complex configuration examples
The following examples demonstrate how to combine regex with first-match-wins semantics to build more complex policies. Fields are given as key = value lines; smaller order is evaluated first.
Example 1: one rule expressing "contains A or contains B or starts with C"
condition_key = User-Agent
condition_type = regex
condition_value = aaaa|bbb|^aaa
action = deny
scope = Global- The
aaaabranch: value containsaaaa(un-anchored = contains). - The
bbbbranch: value containsbbb. - The
^aaabranch: value starts withaaa(the leading anchor applies only to this branch). - The three branches are joined with
|; matching any one of them denies. Equivalent to merging three rules into one.
Example 2: case-insensitive multi-keyword containment
condition_key = User-Agent
condition_type = regex
condition_value = (?i:scrapy|crawl|spider|bot)
action = deny
scope = Global(?i:...) makes the alternation inside the parentheses case-insensitive, so Scrapy, CRAWL, and Spider all match. Note that (?i) only affects the range it wraps, and does not affect the rest of the rule.
Example 3: multi-value exact matching (a case exact cannot handle)
exact can only compare against a single value at a time. To express "the value is exactly one of v1, v2, v3", use an alternation with leading and trailing anchors:
condition_key = X-Api-Version
condition_type = regex
condition_value = ^(v1|v2|v3)$
action = allow
scope = Global^...$ tightens the search semantics to a full match: v1, v2, v3 match; v10, xv1x do not.
Example 4: use contains semantics to block values containing a specific substring
The engine has no standalone contains type, but an un-anchored regex is itself a contains. To block "values of the X-Debug header that contain the substring debug":
condition_key = X-Debug
condition_type = regex
condition_value = debug
action = deny
scope = GlobalFor example debug matches debug, my-debug-flag, but does not match DEBUG (value matching is case-sensitive); write (?i)debug if you need case-insensitivity.
Example 5: allowlist mode (default_action deny as fallback + a single allow)
Requirement: a key group should only allow clients with a trusted prefix, denying everything else. Use "fall back to default_action when no rule matches":
- Set the key group's (or global)
acl_default_actiontoDeny. - Add one allow rule:
order = 10
condition_key = User-Agent
condition_type = regex
condition_value = ^trusted-
action = allow
scope = Group (select the target key group)Effect: value starting with trusted- → matches the rule → allow; other values have no matching rule → fall to default_action → deny. There is no need to write a "deny everything" fallback rule.
Example 6: allow takes precedence over deny (order orchestration)
Requirement: deny common crawlers, but allow your own monitoring probe (whose UA also contains bot, which the crawler rule would wrongly hit). With first-match-wins, the allow rule's order must be smaller:
# Rule A: allow your own probe first
order = 10
condition_key = User-Agent
condition_type = regex
condition_value = ^health-probe/
action = allow
scope = Global
# Rule B: then deny crawlers
order = 20
condition_key = User-Agent
condition_type = regex
condition_value = (?i:scrapy|crawl|spider|bot)
action = deny
scope = Globalhealth-probe/bot-1 is matched by rule A first → allow, rule B is not evaluated; a normal crawler UA does not match A and falls to B → deny. If the two orders are reversed, the probe is blocked by B first.
Example 7: "does not contain a substring" requires reverse orchestration
The regex engine does not support lookahead, so "allow only if the value does not contain internal" cannot be written as a single regex. Reverse-orchestrate it:
acl_default_action = Deny.- Add a rule "allow if it contains
internal":condition_type = regex,condition_value = internal,action = allow.
The semantics become "contains internal → allow; otherwise (contains other values or nothing) → no match → default deny". If the requirement is "deny when it contains internal, allow the rest", set default_action to Allow and add a single internal → deny rule instead.
Evaluation logic
- Merge all rules in the current request's scope: global rules + the current key's per-key rules + the current group's per-group rules.
- Sort by
sort_orderascending (smaller numbers evaluated first; under the same order, the scope order Global → Key Group → Access Key applies), skipping rules withenabled = false. - Evaluate one by one: match by
condition_type(anydoes not read a request header and is always true; the other types take the value of the request headercondition_keyto match). - The first matching rule decides the result:
deny→ immediately return 403;allow→ pass to the next layer. - When no rule matches, fall back along the default_action chain: per-key
acl_default_action→ per-groupacl_default_action→ globalAllow.
FAQ
Q: The client gets 403 but the key is correct? Check whether it is blocked by Header ACL. Rules apply to both inference routes and model-list routes; the client may be blocked as early as the first /v1/models call. Temporarily disable the relevant rule or raise its order to troubleshoot.
Q: The rule changed but doesn't take effect? A rule that fails to compile is skipped and a warning is logged. Check whether condition_value is syntactically valid (lookahead / backreferences are not supported). condition_value has a 1024-byte length limit.
Q: How do I let a key group's rule override a global rule? Under the same order, global matches first. To override, use a smaller order, or set the global rule to enabled = false.
Q: How do I "allow/deny unconditionally" for a specific key? Match Type=any + Scope=Key + Header Name left blank. The rule takes effect only via its scope, applying to all requests from that key.
Q: Are model-list calls blocked too? Yes, rules apply to both /v1/models and /v1beta/models. Operators must be aware of this behavior to avoid misconfiguration that prevents clients from fetching the model list.
Next: Header ACL rule fields for the full reference on regex semantics and expressiveness limits; Access key & key group fields for the key group dimension; Audit & security config for rate limiting and IP blocking.
