# ip_restriction
Allow- or deny-list access by client IP address or CIDR range.
- **Step:** `request` (fixed)
- **Registered as:** `ip_restriction`
## Configuration
| `category` | string | — | Must be `ip_restriction`. |
| `ip_list` | string[] | `[]` | IPv4/IPv6 addresses and CIDR ranges. An entry that is neither fails configuration validation. |
| `type` | string | `allow` | `allow` permits **only** listed IPs; `deny` blocks them. Case-insensitive; any other value is a configuration error. |
| `message` | string | `Request is forbidden` | Body of the 403 response. |
Leaving `type` unset means `allow`, so an empty `ip_list` blocks every request.
Set it explicitly.
## Examples
Allow-list an internal admin path:
```toml
[plugins.internalOnly]
category = "ip_restriction"
type = "allow"
ip_list = ["127.0.0.1", "10.0.0.0/8", "192.168.1.0/24", "::1"]
message = "Internal access only"
[locations.admin]
upstream = "admin"
path = "/admin"
plugins = ["internalOnly"]
```
Deny-list a few abusive ranges:
```toml
[plugins.blockList]
category = "ip_restriction"
type = "deny"
ip_list = ["1.2.3.4", "203.0.113.0/24"]
```
## Client IP resolution
The list is checked against an address the client cannot choose. Directly
connected, that is the address of the connection. Behind a load balancer or a
CDN it is the address that proxy reports in `X-Forwarded-For` / `X-Real-IP` —
but only when the proxy is named in `basic.trusted_proxies`:
```toml
[basic]
trusted_proxies = ["10.0.0.0/8"]
```
With `trusted_proxies` set:
- A connection that does not come from a trusted proxy is identified by its
peer address; its forwarded headers are ignored.
- Through a trusted proxy, `X-Forwarded-For` is read from the right. Each proxy
appends the address it received the request from, so entries that are
trusted proxies themselves are skipped and the first one that is not is the
client. Whatever stands to the left of it was sent by that client and is not
looked at: for `X-Forwarded-For: 6.6.6.6, 9.9.9.9` arriving from `10.0.0.2`
the client is `9.9.9.9`. Several `X-Forwarded-For` lines are read as one
list, and an entry may carry a port (`9.9.9.9:4321`, `[2001:db8::1]:4321`).
- Without `X-Forwarded-For` the address is `X-Real-IP`, then the peer.
The trusted proxy has to do its part: append to `X-Forwarded-For` (nginx:
`proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for`), or, if it does
not send that header at all, set `X-Real-IP` itself. A proxy that passes either
header through as the client sent it leaves the client in charge of its
address.
Without `trusted_proxies` the forwarded headers are not looked at and the
address is the peer's own. Behind a proxy that is not listed there, every
request therefore has the proxy's address: an allow list lets everyone in or
no one, a deny list never matches a client. List the proxy.
(The headers used to be believed from anyone when no proxy was configured: an
allow list for `10.0.0.0/8` let in whoever sent `X-Forwarded-For: 10.1.2.3`.
The same rule now holds for [`limit`](limit.md) by ip,
[`geo_restriction`](geo_restriction.md) and the `ip_list` of
[`combined_auth`](combined_auth.md). The access log's client ip and
`hash:ip` are not checks and still read the headers as before.)
An IPv4 client of a dual-stack listener (`[::]:80`) has the address
`::ffff:1.2.3.4`. It is matched, logged and counted as `1.2.3.4`, so IPv4
entries in `ip_list` and in `trusted_proxies` apply to it.
## Responses
| Blocked by policy | 403 | `message` |
| Client IP is unparseable | 400 | Parse error text |
## Usage notes
- The result is a plain 403 with no `Retry-After` or challenge; use
[`limit`](limit.md) if you want throttling rather than blocking.
- For country-level rules see [`geo_restriction`](geo_restriction.md).