Nginx 常见配置错误
涛叔本文整理了 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/;
}