Nginx 常见配置错误

2024-06-08 ⏳6.1分钟(2.5千字) 🕸️

本文整理了 Nginx 常见的配置错误,供大家参考。如果你果你是初学者,可以先看我的另一篇基础文章

文件描述符耗尽

在 Unix 系统上,每打开一个文件都要消耗一个文件描述符,即 File Descriptor (FD)。新建一条 TCP 连接也要占用一个 FD。系统会限制每个用户同时打开的 FD 数量,默认值为 1024。如果同时打开的连接数太多或者打开的文件太多就会报错。

典型的报错如下:

2015/10/27 21:48:36 [crit] 2475#0: accept4() failed (24: Too many open files)

2015/10/27 21:48:36 [alert] 2475#0: *7163915 socket() failed (24: Too many open files) while connecting to upstream...

一般来说 Nginx 不会打开很多文件,FD 主要消耗在 TCP 连接上。我们可以使用中 worker_connections 来限制每个工作进程的 TCP 连接数量。比如:

event {
  worker_connections 1024;
}

上面的配置会限制每个进程最多开 1024 个连接。注意,这包含客户端到 Nginx 以及 Nginx 到上游服务器的全部连接。

但光设置 worker_connections 还不够,因为每个连接至少对应一个 FD,系统默认会限制当前打开的 FD 数量。

如果 Nginx 用来做静态 Web 服务器,客户端请求文件时,Nginx 会打开对应的文件,这会消耗一个 FD,所以每个客户端连接至少占用两个 FD。

如果用 Nginx 做代理服,工作进程跟客户端和上游服务器创建的每一个连接会消耗一个 FD。而且 Nginx 还有可能创建临时文件来缓存上游返回的内容,这会再消耗一个 FD。

如果用 Nginx 做缓存服务器,就可能同时发生以上两种情况,因为缓存服务器既要对客户端提供静态文件服务,又要在缓存失效或不存在时回源上游服务器。

另外 Nginx 的记录访问日志和错误日志也会消耗一部分 FD,主进程跟工作进程通信也会占用一部分 FD。

所以说,除了设置 worker_connections 之外,我们还应该关注系统的最大 FD 数量。

在 UNIX 中可以通过如下命令查看打开文件数上限值:

ulimit -n

我们可以通过 worker_rlimit_nofile 来调整 FD 数量:

worker_rlimit_nofile 4096;
event {
    worker_connections 1024;
}

worker_connections 和 worker_rlimit_nofile 都是进程级的限制。实际的消耗量需要乘以工作进程数。worker_rlimit_nofile x worker_num 如果太大也会有问题,因为 UNIX 系统还有一个 fs.file‑max 限制,也就是打开文件的数量上限。这个可以通过如下命令查看:

$ sysctl fs.file-max
fs.file-max = 9223372036854775807

fs.file-max 可以通过 /etc/sysctl.conf 来修改,比如添加如下一行:

fs.file-max = 70000

然后执行如下命令来应用配置:

$ sysctl -p

最后让 Nginx 重新加载配置就可以了。

无法关闭错误日志

一般不建议关闭错误日志。但在一些特殊场合,比如磁盘受限等,确实需要关闭。很多人会仿照 access_log off; 使用如下配置来关闭错误日志:

error_log off;

Nginx 不会关闭错误日志。相反,它会创建名为 off 的日志文件来保存报错内容😂

应该使用如下配置:

error_log /dev/null emerg;

Nginx 会把报错内容写入到 /dev/null 设备,系统会自动丢弃。

上游未开启长连接

Nginx 用作负载均衡连接上游服务器时,默认使用短连接。客户端每次发起请求,Nginx 都会选择一个上游服务器,建立 TCP 连接,发送请求,接收响应,再把响应转发给客户端,最后关闭与上游服务器的连接。

这种处理方式的优点是简单明了,但缺点是消耗资源。不但每次创建 TCP 连接需要三次握手通信,而且 Nginx 主动关闭连接之后,对应的套接字会进入 TIME‑WAIT 状态(依然占用 TCP 端口)。在 Linux 下 TIME-WAIT 状态默认会保持五分钟。如果系统有短时间有大量请求进来,很有可能导致本地端口不够用,这样 Nginx 就无法连接到上游服务器。

所以就需要给 upstream 配置开启 keepalive 功能。开启之后,Nginx 会使用 HTTP/1.1 与上游通信,底层 TCP 连接建立后会保持一段时间,期间的请求会复用同一条 TCP 连接,避免反复创建 TCP 连接带来的问题。

以下是一个 HTTP 代理配置示例:

upstream http_backend {
    server 127.0.0.1:8080;
    keepalive 16;
}

server {
    listen 80;

    location /http/ {
        proxy_pass http://http_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

这里在 http_backend 中加入了 keepalive 16; 一行,表示 Nginx 同时跟上游服务器保持 16 个连接。这里还有另外一个配置 keepalive_requests,它表示每一个长连接最多能处理的请求数量,默认为 1000 个。虽然 Nginx 不会立即关闭与上游的 TCP 连接,但也不会一直保持。同一个连接上处理的请求数量超过了 keepalive_requests 后 Nginx 就会自动关闭当前连接,再建立新的 TCP 连接。这样回收为上一个连接分配的内存资源~

另外一个要注意的点是需要在 location 中指定 HTTP 协议的版本号为 1.1 并且将 Connection 头设为空(默认为 close),这样上游服务器就不会根据 HTTP/1.0 的要求主动关闭连接。

除了 HTTP 协议外,Nginx 还支持 fastcgi 上游长连接(主要是 PHP 服务),不过要设置 fastcgi_keep_conn 参数:

location /fastcgi {
    fastcgi_pass fastcgi_backend;
    fastcgi_keep_conn on;
}

混淆配置继承规则

Nginx 配置中有很多不同层级的配置块 (block),如 http/server/location 等。很多配置可以同时出来在多个块中,比如 root 配置。层级越低的配置块优先级越高。比如:

http {
    root /foo;
    server {
        listen 8000;
    }
    server {
        listen 8080;
        root /bar;
        location /baz {
            root /baz;
        }
    }
}

网站 8000 没有配置 root 指令,所以它的 root 从 http 块「继承」值为 /foo。网站 8080 配置了自己的 root 指令,所以它的 root 是 /bar。但 8080 网站的 /baz 路径有自己单独的配置,值为 /bar。所以说,对于同一个配置项,Nginx 始终以层级最低的配置为准。

这里与其说是「继承」配置,例不如说是「覆盖」配置。也就是说低层级的配置会覆盖高层级的配置。

对于 root 配置,这好像是显然的~但对于另外的配置就没那么直观了,比如 add_header 指令。

大家看下面的配置:

http {
    add_header X-HTTP-LEVEL-HEADER 1;
    add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;

    server {
        listen 8080;
        location / {
            return 200 "OK";
        } 
    }

    server {
        listen 8081;
        add_header X-SERVER-LEVEL-HEADER 1;

        location / {
            return 200 "OK";
        }

        location /test {
            add_header X-LOCATION-LEVEL-HEADER 1;
            return 200 "OK";
        }
    }
}

这里在 http 层级上添加了两个 header,所以访问 8080 网站的请求会返回这两个 header。但 8081 网站因为自己配置了 add_header 指令,这就相当于覆盖了 http 层级的配置,所以访问 8081 的根目录只会返回 X-SERVER-LEVEL-HEADER 一个 header。同样,8081 网站的 /test 目录又设置了自己的 add_header,所以请求 /test 路径就只会返回 X-LOCATION-LEVEL-HEADER 一个 header。以上配置并不会产生继承效果,也就是说同导级的 add_header 配置并不会出现在底层级的路径请求中。

如果想实现上述效果,我们需要把所有 header 都加到最底层的配置中:

location /correct {
    add_header X-HTTP-LEVEL-HEADER 1;
    add_header X-ANOTHER-HTTP-LEVEL-HEADER 1;
    
    add_header X-SERVER-LEVEL-HEADER 1;
    add_header X-LOCATION-LEVEL-HEADER 1;

    return 200 "OK";
} 

未保护系统状态信息

Nginx 可以通过 Stub Status 模块展示内部统计信息,比如:

location /status {
    stub_status;
}

展示效果如下:

$ curl 127.0.0.1:8080/status
Active connections: 1
server accepts handled requests
 3 3 3
Reading: 0 Writing: 1 Waiting: 0

这些信息可能会给攻击者提供参考,所以应该保护起来。

最简单的办法就是给 /status 路径加上 HTTP Basic 认证:

location /status {
    stub_status;
    auth_basic "closed site";
    auth_basic_user_file path/to/.htpasswd;
}

auth_basic_user_file 指定保存用户名和密码的认证文件,该文件需要使用 htpasswd 生成。

比如我们要创建 foo 用户:

$ htpasswd -nB foo
New password:
Re-type new password:
foo:$2y$05$hsNjSiih6ZjHkujQ4D6SJOFd7Z8FCFy/zAn.j01lKQLbQEN2F/D5C

最后一行就是认证数据,保存到 .htpasswd 文件就好。这样所有访问 /status 的请求都需要添加 Authentication 关信息才行。

除了使用 HTTP Basic Authentication,我们还可以直接限制访问者的 IP 网段:

location /status {
    allow 10.0.0.0/8;
    allow 2001:db8::/32;
    deny  all;

    stub_status;
}

这样只允许 10.0.0.0/8 和 2001:db8::/32 这两个网段的客户端来访问,其他都拒绝。

最后我们还可以通过 satisfy 把这两种方法结合起来:

location /status {
    satisfy any;

    auth_basic "closed site";
    auth_basic_user_file path/to/.htpasswd;

    allow 10.0.0.0/8;
    allow 2001:db8::/32;
    deny  all;

    stub_status;
}

不论是 IP 网段还是 HTTP Basic Authentication 认证,只要有一样通过了就能正常访问。

Alias 安全风险

在 Nginx 中,除了用 root 指定网站目录外,还可以用 alias 指令。在有些情况下,alias 会更加方便。

假设我们所有的图片路径为 /pic 但目录为 /data/pic。如果用 root 需要写成:

location /pic {
    root /data/;
}

如果用 alias 则可以写成

location /pic {
    alias /data/pic/;
}

甚至可以写成

location /pic {
    alias /data/pic2/;
}

root 指定的目录里面的目录结构必须跟请求路径一一对应,alias 则没有这个要求。

alias 方便归方便,如果使用不当可能会有安全风险。

以上面的配置为例:

location /pic {
    alias /data/pic/;
}

用户访问 http://example.com/pic/a.jpg 会返回 /data/pic/a.jpg 图片内容。但如果用户访问 http://example.com/pic../b.txt 呢?Nginx 会尝试读取 /data/pic/../b.txt 文件的内容并返回。注意,这里用户就绕开了 alias 指定的目录限制。

正确的配置应该是:

location /pic/ {
    alias /data/pic/;
}

参考链接