- Nginx Playground
- plentz / nginx.conf
- 提高安全性的最佳 NGINX 配置
- NGINX 配置 HTTPS 最佳实践
- Jerry Qu
- Using NGINX and NGINX Plus with SELinux
- Difference between SELinux booleans “httpd_can_network_relay” and “httpd_can_network_connect”
- (13: Permission denied) while connecting to upstream:[nginx]
- NGINX Cookbook 2E Simplified Chinese Edition
什么时候应当阅读本文?首先是初始化一个新的服务器实例时。其次,如果一个实例改变了用途,例如实例原先是内部测试使用的,现在要小范围地公开试运行了,则亦有必要按本文的清单检查一遍。
使用主线版本的 NGINX
安全通报里通报的 NGINX 漏洞有时候在 mainline
版本中才会得到修复,我们可以自行添加 NGINX 的 mainline
源。RHEL 及其衍生发行版(CentOS, Oracle Linux, Rocky Linux, AlmaLinux, AnolisOS)可以新建 /etc/yum.repos.d/nginx.repo
文件:
[nginx-mainline]
name=nginx mainline repo
baseurl=https://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=0
enabled=1
module_hotfixes=true
更新到 mainline
版本可能需要先卸载已安装的 NGINX。
> dnf config-manager --add-repo /etc/yum.repos.d/nginx.repo
> dnf update
> nginx -v
nginx version: nginx/1.23.3
使用清晰的配置结构
如果你使用包管理器安装 NGINX,那么主配置文件将位于 /etc/nginx/nginx.conf
。每个子站点应当在 /etc/nginx/conf.d/
中新建配置文件,文件名使用站点域名或三级域名的第一段(如果服务器匹配有一个泛域名的话)。
可以把泛域名 SSL 证书配置写在 http {}
配置块中,否则应当写入独立的 {host}.conf
配置。
HTTP/2
HTTP/2 可以有效提升对网络的利用效率。
listen 443 ssl;
http2 on;
1.25.1 之前的版本使用:
listen 443 ssl http2;
支持 HTTP/2 服务器推送:
location = /demo.html {
http2_push /style.css;
http2_push /image1.jpg;
如果代理应用包含名为 Link
的 HTTP 响应头,则 NGINX 也可以自动将资源推送到客户端,不过 Chrome 浏览器可能已经移除了支持。
流量管理
高性能负载均衡
使用负载均衡时,NGINX 默认开启被动式健康检查,max_fails
为 1,fail_timeout
为 10 秒。要改变这一行为,可使用:
upstream backend {
server backend1.example.com:1234 max_fails=3 fail_timeout=3s;
server backend2.example.com:1234 max_fails=3 fail_timeout=3s;
}
这些参数在 stream(TCP/UDP)负载均衡中作用相同。
A/B 测试
借助 split_clients
模块把 33.3% 的流量指向 backend1
,剩余流量指向 backend2
。流量使用 remote_addr
标识。
http {
split_clients "${remote_addr}" $upstream {
33.3% "backend1.example.com:1234";
* "backend2.example.com:1234";
}
server {
listen 80 _;
location / {
proxy_pass http://$upstream
}
}
}
加固 NGINX
禁止直接通过服务器 IP 访问站点
添加一个默认的 Server {}
配置防止通过 IP 直接访问服务器。
server {
listen 443 default_server ssl;
listen 80 default_server;
server_name _;
return 403;
}
加密协议与加密套件
SSL 已经是不安全的了,TLSv1.0 与 TLSv1.1 虽然没有被证明不安全,但作为老旧的协议即将过时。TLSv1.3 作为最新的协议,在性能和安全性上都有提升。
# 兼顾兼容性
ssl_protocols TLSv1.2 TLSv1.3;
# 只要是现代浏览器都是 ok 的
ssl_protocols TLSv1.3;
后来发现有些同学还在用 macOS Catalina,LibreSSL 2.8.3 还不支持 TLSv1.3 导致 Git 不能通过 HTTPS 拉取代码。
至于加密套件,RC4、DES 等等都不安全,放一个 linode 的配置:
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_prefer_server_ciphers on;
BEAST (CVE-2011-3389) 是一种主要针对 TLSv1.0 和更早协议的明文攻击。设置 ssl_prefer_server_ciphers on
可以让服务器决定使用的加密套件。参考 Setting ssl_prefer_server_ciphers directive in nginx config - serverfault。
限制客户端请求
限制客户端请求可以防止一些客户端占据太多的服务器资源,某些情况下也可用来防止暴力破解登录页面。
需注意由于 NAT 等技术,来源于同一网络的用户流量可能会使用相同的 IP 地址,导致访问受限。在一个运行中的系统上可以设置 limit_req_dry_run on
指令,然后在实时日志中打印 $limit_req_status
变量。通过分析日志中的 PASSED
、DELAYED
、REJECTED
、DELAYED_DRY_RUN
和 REJECTED_DRY_RUN
分布判断限制策略的效果。
限制并发连接数
创建名为 limitbyaddr
的共享内存区,大小设置为 10 MB。使用 binary_remote_addr
标记客户端 IP 地址。limit_conn_status 429
定义连接状态被限制时返回 429 Too Many Request
。
http {
limit_conn_zone $binary_remote_addr zone=limitbyaddr:10m;
limit_conn_status 429;
通过 limit_conn
设置使用该共享内存区,并设置连接上限为 40。
server {
limit_conn limitbyaddr 40
限制请求产生速率
http {
limit_req_zone $binary_remote_addr zone=limitbyaddr:10m rate=3r/s;
limit_req_status 429;
创建名为 limitbyaddr
的共享内存区,大小设置为 10 MB。客户端每秒可以发送 3 个请求。该功能默认返回 503 Service Unavailable
,可以通过 limit_req_status 429
设置返回 429 Too Many Request
声明这是客户端的问题。
server {
limit_req zone=limitbyaddr;
limit_req
也可以使用一种两级配置,burst
参数允许客户端超过速率限制时不拒绝其请求,而 delay
描述了窗口的计数器策略。
server {
location / {
limit_req zone=limitbyaddr burst=12 nodelay;
由于我们已经设置了 3r/s
的速率限制,burst=12
允许用户一次性发送 12 个请求,由于这些请求消耗了 4 秒的时间窗口,在初始请求的 4 秒后才能继续发送请求。
限制带宽
向客户端响应的速率在传输 10 MB 数据后限制为每秒 1 MBps。带宽限制针对每个连接生效。
location /download/ {
limit_rate_after 10m;
limit_rate 1m;
设置缓存区容量上限
client_body_buffer_size 100k;
client_header_buffer_size 1k;
client_max_body_size 100k;
large_client_header_buffers 2 1k;
在本例中:
- 限制请求头不超过 1024 字节
- 限制请求体不超过 102400 字节
可以根据自身项目需求(例如有文件上传需求)调整参数。
缓解慢速 HTTP 攻击
攻击者可以持续发送多个慢速 HTTP 请求,使服务器资源难以释放,从而造成拒绝服务。一般有如下几种攻击方式:
- Slow headers:Web 应用在处理 HTTP 请求前会先接收完整的 HTTP 头部。服务器在接收到 2 个连续的
\r\n
(表示请求头部分结束)前会持续的等待客户端数据。 - Slow body:攻击者发送一个 HTTP POST 请求,该请求的
Content-Length
值很大,使服务器认为客户端要发送很大的数据。服务器会保持连接准备接收数据。 - Slow read:客户端以很低的速度读取服务的 HTTP Response 或不读取任何数据。通过发送 Zero Window 到服务器,让服务器误以为客户端很忙,直到连接快超时前才读取一个字节,以消耗服务器的连接和内存资源。
这个配置示例来自本世纪头个十年的 OWASP Foundation 建议,考虑到目前网络基础设施的情况,笔者认为还可以压缩时间。
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 5s 5s;
send_timeout 10s;
client_body_timeout
设置两个连续的读操作之间的超时时间,超时后返回408 Request Time-out
。该配置不会计算数据传输的时间。client_header_timeout
设置读取请求头的时间,如果客户端不能在这个时间内发送完整的请求头,将会收到408 Request Time-out
keepalive_timeout
的第一个参数设置服务器端维持客户端连接的超时时间,设置为 0 将禁用该功能。第二个参数将体现在服务端响应头的Keep-Alive: timeout={timeout}
参数中,不过客户端不一定会遵循(尤其是恶意客户端)send_timeout
设置向客户端发送数据的超时时间,而不是为整个响应的传输设置超时
基于 IP 地址的访问控制
allow
和 deny
指令在 http
、server
和 location
上下文以及 TCP/ UDP 的 stream
、server
上下文中有效。NGINX 按顺序检查规则,直到找到与地址匹配的规则。
location {
allow 61.130.51.238;
allow 10.0.0.0/20;
allow 2001:0db8::/32;
deny all;
隐藏版本信息
在 http {}
配置块中添加 server_tokens off;
,这样由 NGINX 返回默认的 403 / 404 / 500 等页面时不会返回版本号,HTTP 响应头中的 Server
字段也会隐藏版本信息。
http {
...
+ server_tokens off;
...
}
此外还应当修改 CGI 配置,在 NGINX 1.23.3 中这个文件是 /etc/nginx/fastcgi_params
:
- fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
+ fastcgi_param SERVER_SOFTWARE nginx;
日志
在 http
上下文中创建一个名为 trace
的日志格式:
http {
log_format trace
'[$time_local] $remote_addr '
'$realip_remote_addr $remote_user '
'$proxy_protocol_server_addr $proxy_protocol_server_port '
'$request_method $server_protocol '
'$scheme $server_name $uri $status '
'$request_time $body_bytes_sent '
'$upstream_status $upstream_response_time '
'"$http_referer" "$http_user_agent"';
在配置 access_log
时可以指定日志格式:
server {
access_log /var/log/nginx/access.log trace;
日志的轮转可以交给 Logrotate,如果 NGINX 是通过包管理器安装的,那大概率是开箱即用的。
当系统处于高负载状态时,需要启用日志缓冲,以降低 NGINX worker 进程发生阻塞的可能性。日志数据在写入磁盘之前可保存在内存缓冲区中,buffer
参数表示内存缓冲区的大小,flush
参数设置日志可在缓冲区中留存的最长时间。
access_log /var/log/nginx/access.log trace buffer=32k flush=1m;
SELinux
云服务商提供的操作系统镜像一般默认会关闭 SELinux。不过如果操作系统是你自己上传的 Fedora / CentOS / AlmaLinux 等 RHEL 系发行版,则推荐让 SELinux 保持开着。
你可能会发现 NGINX 无法正常启动,查看 systemctl status nginx.service
发现 nginx: [emerg] bind() to 0.0.0.0:443 failed (13: Permission denied)
;也有可能发现 upstream
未起作用。
SELinux 有三种执行模式,enforcing、permissive 和 disabled。在 permissive 模式中,一些被 SELinux 默认禁止的动作会被放行,然后记录到审计日志中。你可以先将 httpd_t
添加到 permissive 组,测试所有需要配置的功能。
# add to permissive domains
semanage permissive -a httpd_t
# remove from permissive domains
semanage permissive -d httpd_t
允许 NGINX 连接到远程的 HTTP 或其他服务
如果你使用 NGINX 来实现某种负载均衡或反向代理,这是个常见的需求。
setsebool -P httpd_can_network_relay 1
setsebool -P httpd_can_network_connect 1
Option [
httpd_can_network_relay
] is used in an reverse proxy scenario in which your httpd is relaying requests to some backend httpd in behalf of the client. Option [httpd_can_network_connect
] allows httpd modules and scripts to make outgoing connections to ports which are associated with the httpd service. To see a list of those ports runsemanage port -l | grep -w http_port_t
无法访问目录或文件
默认的 SELinux 配置只允许 NGINX 访问一些常用的目录。
你可以调整文件的 label,允许含有 httpd_t
标记的进程访问该文件(例如 NGINX 的进程):
semanage fcontext -a -t httpd_sys_content_t /www/t.txt
restorecon -v /www/t.txt
要改一堆文件,使用:
semanage fcontext -a -t httpd_sys_content_t '/www(/.*)?'
restorecon -Rv /www
无法绑定到特定端口
SELinux 只允许 NGINX 绑定到一批特定的端口。当你在 http
,stream
等模块中尝试 listen
指令时,可能会抛出 nginx: [emerg] bind() to 0.0.0.0:443 failed (13: Permission denied)
这样的错误。
你可以通过 semanage port -l | grep http_port_t
查询可用端口列表。要允许 NGINX 监听特定端口(如 8001),使用:
semanage port -a -t http_port_t -p tcp 8001