NGINX 配置最佳实践

2023-09-04, 星期一, 10:07

DevOpsSystem AdministrationCyber Security培训

什么时候应当阅读本文?首先是初始化一个新的服务器实例时。其次,如果一个实例改变了用途,例如实例原先是内部测试使用的,现在要小范围地公开试运行了,则亦有必要按本文的清单检查一遍。

使用主线版本的 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 变量。通过分析日志中的 PASSEDDELAYEDREJECTEDDELAYED_DRY_RUNREJECTED_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 地址的访问控制

allowdeny 指令在 httpserverlocation 上下文以及 TCP/ UDP 的 streamserver 上下文中有效。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 有三种执行模式,enforcingpermissivedisabled。在 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 run semanage 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 绑定到一批特定的端口。当你在 httpstream 等模块中尝试 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