478 lines
11 KiB
Markdown
478 lines
11 KiB
Markdown
# Proxy configuration
|
|
|
|
When you use a reverse proxy server (e.g. nginx), the value of `request.ip` will contain the IP of a proxy, typically `127.0.0.1`. Almost always, this is **not** what you will want.
|
|
|
|
Sanic may be configured to use proxy headers for determining the true client IP, available as `request.remote_addr`. The full external URL is also constructed from header fields _if available_.
|
|
|
|
|
|
.. tip:: Heads up
|
|
|
|
Without proper precautions, a malicious client may use proxy headers to spoof its own IP. To avoid such issues, Sanic does not use any proxy headers unless explicitly enabled.
|
|
|
|
|
|
|
|
.. column::
|
|
|
|
Services behind reverse proxies must configure one or more of the following [configuration values](/guide/deployment/configuration.md):
|
|
|
|
- `FORWARDED_SECRET`
|
|
- `REAL_IP_HEADER`
|
|
- `PROXIES_COUNT`
|
|
|
|
.. column::
|
|
|
|
```python
|
|
app.config.FORWARDED_SECRET = "super-duper-secret"
|
|
app.config.REAL_IP_HEADER = "CF-Connecting-IP"
|
|
app.config.PROXIES_COUNT = 2
|
|
```
|
|
|
|
## Forwarded header
|
|
|
|
In order to use the `Forwarded` header, you should set `app.config.FORWARDED_SECRET` to a value known to the trusted proxy server. The secret is used to securely identify a specific proxy server.
|
|
|
|
Sanic ignores any elements without the secret key, and will not even parse the header if no secret is set.
|
|
|
|
All other proxy headers are ignored once a trusted forwarded element is found, as it already carries complete information about the client.
|
|
|
|
To learn more about the `Forwarded` header, read the related [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) and [Nginx](https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/) articles.
|
|
|
|
## Traditional proxy headers
|
|
|
|
### IP Headers
|
|
|
|
When your proxy forwards you the IP address in a known header, you can tell Sanic what that is with the `REAL_IP_HEADER` config value.
|
|
|
|
### X-Forwarded-For
|
|
|
|
This header typically contains a chain of IP addresses through each layer of a proxy. Setting `PROXIES_COUNT` tells Sanic how deep to look to get an actual IP address for the client. This value should equal the _expected_ number of IP addresses in the chain.
|
|
|
|
### Other X-headers
|
|
|
|
If a client IP is found by one of these methods, Sanic uses the following headers for URL parts:
|
|
|
|
- x-forwarded-proto
|
|
- x-forwarded-host
|
|
- x-forwarded-port
|
|
- x-forwarded-path
|
|
- x-scheme
|
|
|
|
## Examples
|
|
|
|
In the following examples, all requests will assume that the endpoint looks like this:
|
|
```python
|
|
@app.route("/fwd")
|
|
async def forwarded(request):
|
|
return json(
|
|
{
|
|
"remote_addr": request.remote_addr,
|
|
"scheme": request.scheme,
|
|
"server_name": request.server_name,
|
|
"server_port": request.server_port,
|
|
"forwarded": request.forwarded,
|
|
}
|
|
)
|
|
```
|
|
|
|
.. column::
|
|
|
|
---
|
|
|
|
##### Example 1
|
|
Without configured FORWARDED_SECRET, x-headers should be respected
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=1.1.1.1, for=injected;host=", for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret,for=broken;;secret=b0rked, for=127.0.0.3;scheme=http;port=1234' \
|
|
-H "X-Real-IP: 127.0.0.2" \
|
|
-H "X-Forwarded-For: 127.0.1.1" \
|
|
-H "X-Scheme: ws" \
|
|
-H "Host: local.site" | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "127.0.0.2",
|
|
"scheme": "ws",
|
|
"server_name": "local.site",
|
|
"server_port": 80,
|
|
"forwarded": {
|
|
"for": "127.0.0.2",
|
|
"proto": "ws"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 2
|
|
FORWARDED_SECRET now configured
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=1.1.1.1, for=injected;host=", for="[::2]";proto=https;host=me.tld;path="/app/";secret=mySecret,for=broken;;secret=b0rked, for=127.0.0.3;scheme=http;port=1234' \
|
|
-H "X-Real-IP: 127.0.0.2" \
|
|
-H "X-Forwarded-For: 127.0.1.1" \
|
|
-H "X-Scheme: ws" \
|
|
-H "Host: local.site" | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "[::2]",
|
|
"scheme": "https",
|
|
"server_name": "me.tld",
|
|
"server_port": 443,
|
|
"forwarded": {
|
|
"for": "[::2]",
|
|
"proto": "https",
|
|
"host": "me.tld",
|
|
"path": "/app/",
|
|
"secret": "mySecret"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 3
|
|
Empty Forwarded header -> use X-headers
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H "X-Real-IP: 127.0.0.2" \
|
|
-H "X-Forwarded-For: 127.0.1.1" \
|
|
-H "X-Scheme: ws" \
|
|
-H "Host: local.site" | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "127.0.0.2",
|
|
"scheme": "ws",
|
|
"server_name": "local.site",
|
|
"server_port": 80,
|
|
"forwarded": {
|
|
"for": "127.0.0.2",
|
|
"proto": "ws"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 4
|
|
Header present but not matching anything
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H "Forwarded: nomatch" | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {}
|
|
}
|
|
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 5
|
|
Forwarded header present but no matching secret -> use X-headers
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H "Forwarded: for=1.1.1.1;secret=x, for=127.0.0.1" \
|
|
-H "X-Real-IP: 127.0.0.2" | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "127.0.0.2",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"for": "127.0.0.2"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 6
|
|
Different formatting and hitting both ends of the header
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: Secret="mySecret";For=127.0.0.4;Port=1234' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "127.0.0.4",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 1234,
|
|
"forwarded": {
|
|
"secret": "mySecret",
|
|
"for": "127.0.0.4",
|
|
"port": 1234
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 7
|
|
Test escapes (modify this if you see anyone implementing quoted-pairs)
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=test;quoted="\,x=x;y=\";secret=mySecret' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "test",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"for": "test",
|
|
"quoted": "\\,x=x;y=\\",
|
|
"secret": "mySecret"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 8
|
|
Secret insulated by malformed field #1
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=test;secret=mySecret;b0rked;proto=wss;' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "test",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"for": "test",
|
|
"secret": "mySecret"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 9
|
|
Secret insulated by malformed field #2
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=test;b0rked;secret=mySecret;proto=wss' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "",
|
|
"scheme": "wss",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"secret": "mySecret",
|
|
"proto": "wss"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 10
|
|
Unexpected termination should not lose existing acceptable values
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: b0rked;secret=mySecret;proto=wss' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "",
|
|
"scheme": "wss",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"secret": "mySecret",
|
|
"proto": "wss"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 11
|
|
Field normalization
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "mySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: PROTO=WSS;BY="CAFE::8000";FOR=unknown;PORT=X;HOST="A:2";PATH="/With%20Spaces%22Quoted%22/sanicApp?key=val";SECRET=mySecret' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "",
|
|
"scheme": "wss",
|
|
"server_name": "a",
|
|
"server_port": 2,
|
|
"forwarded": {
|
|
"proto": "wss",
|
|
"by": "[cafe::8000]",
|
|
"host": "a:2",
|
|
"path": "/With Spaces\"Quoted\"/sanicApp?key=val",
|
|
"secret": "mySecret"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
.. column::
|
|
|
|
##### Example 12
|
|
Using "by" field as secret
|
|
```python
|
|
app.config.PROXIES_COUNT = 1
|
|
app.config.REAL_IP_HEADER = "x-real-ip"
|
|
app.config.FORWARDED_SECRET = "_proxySecret"
|
|
```
|
|
```bash
|
|
$ curl localhost:8000/fwd \
|
|
-H 'Forwarded: for=1.2.3.4; by=_proxySecret' | jq
|
|
```
|
|
|
|
.. column::
|
|
|
|
```bash
|
|
# curl response
|
|
{
|
|
"remote_addr": "1.2.3.4",
|
|
"scheme": "http",
|
|
"server_name": "localhost",
|
|
"server_port": 8000,
|
|
"forwarded": {
|
|
"for": "1.2.3.4",
|
|
"by": "_proxySecret"
|
|
}
|
|
}
|
|
|
|
```
|
|
|