> Raw Markdown twin (generated at build time from the source Markdown). Rendered page: https://docs.gatellm.io/en/howto/configure-header-acl · Doc index: https://docs.gatellm.io/en/llms.txt


# 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|wget|scrapy` |
| `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](/en/reference/header-acl-rules.md) 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-For` is 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 (with `TRUSTED_PROXIES`) over the forgeable `X-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 = `Key` and 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:

```bash
# 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"

```text
condition_key   = User-Agent
condition_type  = regex
condition_value = aaaa|bbb|^aaa
action          = deny
scope           = Global
```

- The `aaaa` branch: value **contains** `aaaa` (un-anchored = contains).
- The `bbb` branch: value **contains** `bbb`.
- The `^aaa` branch: value **starts with** `aaa` (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

```text
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:

```text
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`":

```text
condition_key   = X-Debug
condition_type  = regex
condition_value = debug
action          = deny
scope           = Global
```

For 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":

1. Set the key group's (or global) `acl_default_action` to `Deny`.
2. Add one allow rule:

```text
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**:

```text
# 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           = Global
```

`health-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:

1. `acl_default_action = Deny`.
2. 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

1. 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.
2. Sort by `sort_order` ascending (smaller numbers evaluated first; under the same order, the scope order Global → Key Group → Access Key applies), skipping rules with `enabled = false`.
3. Evaluate one by one: match by `condition_type` (`any` does not read a request header and is always true; the other types take the value of the request header `condition_key` to match).
4. The **first matching** rule decides the result: `deny` → immediately return 403; `allow` → pass to the next layer.
5. When no rule matches, fall back along the default_action chain: per-key `acl_default_action` → per-group `acl_default_action` → global `Allow`.

## 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](/en/reference/header-acl-rules.md) for the full reference on regex semantics and expressiveness limits; [Access key & key group fields](/en/reference/access-keys-groups-fields.md) for the key group dimension; [Audit & security config](/en/reference/audit-and-security-config.md) for rate limiting and IP blocking.
