配置Asp.Net Core程序使用反向代理

一般来讲, 部署在 Linux 下的 Asp.Net Core 应用都不会将Kestrel服务器的监听端口直接暴露在生产环境中, 而是经过一层Web服务器作反向代理, 这个服务器可以是caddy, nginxapache等.
但是, 经过反向代理之后, 从Request上下文中获得的源IP地址与源传输协议(HTTP/HTTPS)一般都会为127.0.0.1(或反向代理服务器的地址)与http, 这会导致基于IP的请求熔断器无法正常工作, 如果启用了HttpsRedirection, 还会导致进入无限重定向循环中.

为了解决这个问题, 反向代理服务器一般会通过X-Forwarded-ForX-Forwarded-Proto两个Http头来传递实际的请求IP与请求协议, 因此只要配置服务器使用这个请求头中的数据替换原始的源IP及协议即可.

对于只有一层反向代理服务器的情况来说, 解决这个问题的方式非常简单.

  • 在运行目标 Asp.Net Core 应用的环境中配置环境变量ASPNETCORE_FORWARDEDHEADERS_ENABLEDtrue即可. 这一般可以通过修改systemd服务配置等方法实现.

如果配置了这个环境变量, Asp.Net Core应用会无条件信任来自所有源的请求中的X-Forwarded-ForX-Forwarded-Proto头. 这有可能导致X-Forwarded-For头欺诈的情况发生, 从而导致IP熔断器失效. 因此必须保证Kerstrel服务器的监听端口不可被不受信任的来源访问, 建议通过配置防火墙等方式实现, 或改用下面的方法.

  • 可以在程序初始化配置中添加与ForwardedHeaders中间件相关的配置代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
// add at ConfigureServices() if you are using classic template
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});

// ...

// Add at Configure() if you are using classic template.
// Add before other middlewares!
app.UseForwardedHeaders();

这段配置代码默认只会信任localhost, 如需信任其它服务器来源, 可通过修改KnownProxies实现.

配合CloudFlare及其它反向代理/CDN服务器使用

配置Asp.Net Core服务器

配置其它外部CDN后, Http请求会经过两层反代才会到达Kestrel服务器. 但是默认配置下, ForwardedHeaders中间件最对只会处理一种反向代理的情况. 如果有多层反向代理, 它会将最后一层反代服务器所设定的值作为源IP与源协议值.

要改变这一行为, 只需要更改ForwardLimit的值即可(默认为1)

下面的配置代码可以从配置文件读入ForwardLimit值:

1
2
3
4
5
6
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardLimit = builder.Configuration.GetSection("transportSettings").GetValue("forwardLimit", 1);
});

然后在appsettings.json中添加…

1
2
3
4
5
6
7
{
// ...
"transportSettings": {
"forwardLimit": 2
},
// ...
}

配置Caddy服务器

信任外部反向代理IP

为了防止X-Forwarded-For头欺诈, Caddy服务器默认不会信任外部请求所提供的X-Forwarded-ForX-Forwarded-Proto请求头. 因此需要为Caddy配置Cloudflare的信任IP:

Cloudflare会通过文档api公布它们的CDN IP地址范围, 将这部分地址加入到Caddyfile的trust_proxy中即可.

1
2
3
4
5
# ...
reverse_proxy 127.0.0.1:5000 {
trusted_proxies 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
}
# ...

此处的Cloudflare IP地址仅作为配置示例且可能已经过时, 请以从Cloudflare中获取的最新地址为准.

Cloudflare的IP地址会不定时更新, 如果你不想手动维护这个信任地址列表的话, 可以参考下面的方法

自动维护信任的Cloudflare IP列表

这里我使用一个python脚本自动维护IP列表. 创建/etc/caddy/update_cloudflare.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/python
from urllib.request import urlopen
import json
import os

api_ips = "https://api.cloudflare.com/client/v4/ips"
etag_filename = ".cloudflare_etag"
config_filename = "cloudflare_proxies"
update_command = "systemctl reload caddy"

etag = open(etag_filename).read() if os.path.isfile(etag_filename) else ""
resp_json = json.loads(urlopen(api_ips).read())

if not resp_json or not resp_json['success']:
print("Request failed!")
exit(-1)

if resp_json['result']['etag'] == etag:
print("Ip not modified. Nothing was changed.")
exit(0)

print("Ip modified. new etag: " + resp_json['result']['etag'])

new_etag = open(etag_filename, 'w')
new_etag.write(resp_json['result']['etag'])
new_etag.close()

ips = resp_json['result']['ipv4_cidrs']
ips.extend(resp_json['result']['ipv6_cidrs'])


text = f"trusted_proxies {' '.join(ips)}"

print(text)

cffile = open(config_filename, 'w')
cffile.write(text)
cffile.close()

os.system(update_command)

完成后添加运行权限sudo chmod +x update_cloudflare.py, 然后以sudo权限执行一次

然后修改/etc/caddy/Caddyfile:

1
2
3
reverse_proxy 127.0.0.1:5000 {
import cloudflare_proxies
}

然后配置计划任务, 以sudo权限运行crontab:

1
sudo crontab -e

添加下面这一行即可.(下面这行代码会在每天0点从api更新cloudflare ips)

1
0 0 * * * cd /etc/caddy && ./update_cloudflare.py

配置Cloudflare

此节摘自Authelia docs

当传入请求中已经存在X-Forwarded-For头时, cloudflare会直接在原有的请求头中追加一个IP. 这意味着用户可能可以通过伪造X-Forwarded-For头来伪造IP, 即X-Forwarded-For欺诈. 因此, 强烈建议在Cloudflare中配置删除X-Forwarded-For头的操作.

方法:

  1. 进入Cloudflare控制台, 选择Rules
  2. 选择Transform Rules
  3. 选择Create transform rules
  4. 选择Modify Request Header
  5. Rule name更改为Remove X-Forwarded-For Header或其它名字
  6. Field设置为X-Forwarded-For
  7. Operator设置为does not equal
  8. Value留空
  9. Then操作设置为Remove
  10. Header name设置为X-Forwarded-For
  11. 保存Save


Reference