NGINX 配置陷阱和常见错误

NGINX 配置陷阱和常见错误
Photo by Hal Gatewood / Unsplash

关于本指南

这是一篇来自 NGINX 官方 WIKI 的指南,由 Github 的 renyidong 翻译。

无论新老用户,都有可能掉进陷阱。
下面我们列出了常见的问题,并解释如何解决他们。
在 Freenode IRC 的 #nginx 频道,我们经常看到这些问题。

最常见的问题是有人从其他指南拷贝配置片段。
不能说所有的指南都是错的,但绝大部分是有问题的。
即使是 Linode library,也有部分极其劣质的信息,NGINX 社区成员正在努力去尝试更正。

本站文档由了解各种 NGINX 用户的社区成员创建并审核。
本文的纂写仅仅是鉴于社区成员所遇到的、大量的、常见的、反复出现的问题。

未列出我的问题

在这里,也许你找不到任何和你的问题直接相关的内容。
这是因为,我们并不是因为你遇到的具体问题而让你来这里。
不要仅仅瞥了一眼就断言我们无缘无故带你到此。
我们让你来这里,是因为你犯了某个这里提到的错误。

对于大量的用户遇到的大量的问题,社区成员不想去支持错误的配置。
请先改正你的配置,再请求帮助。
你要认真阅读本文才能改正你的配置。不要随便扫一遍了事。

Chmod 777

绝不使用 777。它可能是个用着相当顺手的数字。但就算你只是在测试,它意味着你根本不知道你在干什么。
你应该仔细检查整个路径的每个部分的权限,仔细思考它们会产生什么效果,要简单的检查路径的权限,可以使用:

namei -om /path/to/check

Location 段里的 Root 选项

BAD

server {
    server_name www.example.com;
    location / {
        root /var/www/nginx-default/;
        # [...]
      }
    location /foo {
        root /var/www/nginx-default/;
        # [...]
    }
    location /bar {
        root /var/www/nginx-default/;
        # [...]
    }
}

这确实能用。把root放在location区块可以生效而且合乎语法。
但是当需要增加location区块,你要逐个location区块添加root指令,没有匹配任何location的请求将没有root
下面看看正确的配置。

GOOD

server {
    server_name www.example.com;
    root /var/www/nginx-default/;
    location / {
        # [...]
    }
    location /foo {
        # [...]
    }
    location /bar {
        # [...]
    }
}

多重 Index 指令

BAD

http {
    index index.php index.htm index.html;
    server {
        server_name www.example.com;
        location / {
            index index.php index.htm index.html;
            # [...]
        }
    }
    server {
        server_name example.com;
        location / {
            index index.php index.htm index.html;
            # [...]
        }
        location /foo {
            index index.php;
            # [...]
        }
    }
}

为什么在不需要时重复那么多行。简单的使用一次 “index” 指令。仅仅需要添加到*http{}*区块,将会被继承。

GOOD

http {
    index index.php index.htm index.html;
    server {
        server_name www.example.com;
        location / {
            # [...]
        }
    }
    server {
        server_name example.com;
        location / {
            # [...]
        }
        location /foo {
            # [...]
        }
    }
}

使用 If

有几个页面介绍使用 if 语句。它叫做IfIsEvil,你真的应该看看。让我们看看使用 if 的坏处。

See also

If Is Evil

Server Name (if)

BAD

server {
    server_name example.com *.example.com;
        if ($host ~* ^www\.(.+)) {
            set $raw_domain $1;
            rewrite ^/(.*)$ $raw_domain/$1 permanent;
        }
        # [...]
    }
}

这里有3个问题。第一个是if,我们关心的。当 NGINX 接受一个请求不管是子域名 www.example.com
还是 example.com,if指令总是会判断。因为你要求 NGINX 检查每个请求的 Host 头。
这样效率低下,你应该避免。像下面一样使用2个server指令。

GOOD

server {
    server_name www.example.com;
    return 301 $scheme://example.com$request_uri;
}
server {
    server_name example.com;
    # [...]
}

除了配置文件更易于阅读,这种方法减少 NGINX 处理需求。我们摆脱了虚假的if,同时使用
$scheme,没有硬编码 URI 的结构,可以是 http 或者 https。

检查文件 (if) 存在

使用 if 去检查文件是否存在是糟糕的。如果你使用最新版本的 NGINX ,应该看看try_files,更
易使用。

BAD

server {
    root /var/www/example.com;
    location / {
        if (!-f $request_filename) {
            break;
        }
    }
}

GOOD

server {
    root /var/www/example.com;
    location / {
        try_files $uri $uri/ /index.html;
    }
}

这里我们判断$uri是否存在不再需要if。使用try_files意味着可以依序测试。
如果$uri不存在,尝试$uri/,如果还不存在,尝试备用的location

在这个案例中,会先查看$uri文件是否存在,存在就返回。如果不存在,就检查该目录是否存在。
如果不存在,将返回index.html,你必须确定存在该文件。

Front Controller 模式 WEB 应用

“Front Controller 模式”设计是受欢迎的,且用在许多最流行的 PHP 软件包中。
很多实例比应有的更复杂。让 Drupal, Joomla 等运行,使用:

try_files $uri $uri/ /index.php?q=$uri&$args;

注解 - 参数名称根据你使用的包是不同的。例如

  • “q” 是 Drupal、Joomla、WordPress 使用的参数
  • “page” 是 CMS Made Simple 使用的参数

一些软件甚至不用查询字符串,而且可以从REQUEST_URI读取(WordPress支持,例如):

try_files $uri $uri/ /index.php;

当然你的情况可能不同,你可能根据需求添加复杂的规则,但一个基本的网站,这些将完美的工作。
你可以从简单的开始。

如果你不在乎目录是否存在,你也可以决定跳过目录检查,从中删除$uri/

把不受控制的请求给 PHP

很多对 PHP 的 NGINX 配置样例把每个以.php结尾的 URI 给 PHP 解释器。请注意在大多数 PHP
设置中,这将是个严重的安全问题,因为它允许任意第三方代码执行。

出问题的区域通常如下:

location ~* \.php$ {
    fastcgi_pass backend;
    # [...]
}

这里每一个以.php结尾文件的请求将会给FastCGI后端。
这里的问题是如果全路径并不指向文件系统上实际的文件,默认的 PHP 配置会试图猜测你需要执行哪个文件。

例如,如果请求不存在的文件/forum/avatar/1232.jpg/file.php,但/forum/avatar/1232.jpg存在,
PHP 解释器将处理/forum/avatar/1232.jpg 。如果它包含 PHP 代码,这段代码将相应的执行。

下面的选项可以避免上述情况:

  • 在php.ini中设置cgi.fix_pathinfo=0。使得 PHP 解释器仅尝试给出的路径,如果文件没有找到就停止处理。
  • 保证 NGINX 仅传给后端指定的 PHP 文件请求:
location ~* (file_a|file_b|file_c)\.php$ {
    fastcgi_pass backend;
    # [...]
}
  • 特别地在任何包含用户上传的目录中禁用 PHP 文件的执行
location /uploaddir {
    location ~ \.php$ {return 403;}
    # [...]
}
  • 使用try_files指令过滤
location ~* \.php$ {
    try_files $uri =404;
    fastcgi_pass backend;
    # [...]
}
  • 使用嵌套的 location 过滤
location ~* \.php$ {
    location ~ \..*/.*\.php$ {return 404;}
    fastcgi_pass backend;
    # [...]
}

Script Filename 中的 FastCGI Path

一些指南倾向使用绝对路径得到信息。这在 PHP 区块很常见。当你从源安装 NGINX ,通常你能够把
include fastcgi_params;加入到配置中。该文件通常在 NGINX 配置目录/etc/nginx/

GOOD

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

BAD

fastcgi_param  SCRIPT_FILENAME    /var/www/yoursite.com/$fastcgi_script_name;

$document_root在哪里设置?是通过 root 指令在 server 区块设置。root 指令没有配置?看看
第一个陷阱。

繁杂的 Rewrites

不要感觉糟糕,正则表达式容易让人困惑。实际上,我们应该努力保持整洁、简单,不增加繁琐的东西,这容易做到。

BAD

rewrite ^/(.*)$ http://example.com/$1 permanent;

GOOD

rewrite ^ http://example.com$request_uri? permanent;

BETTER

return 301 http://example.com$request_uri;

第一个 rewrite 捕获在第一个*/*之后的 URI。通过使用内建变量$request_uri,我们可以有效的避免做任何捕获或者匹配。

Rewrite 丢失 http://

rewrite 是简单,记得添加scheme

BAD

rewrite ^ example.com permanent;

GOOD

rewrite ^ http://example.com permanent;

在上面可以看到,我们在 rewrite 添加了http://。简单有效。

代理一切

BAD

server {
    server_name _;
    root /var/www/site;
    location / {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/tmp/phpcgi.socket;
    }
}

令人讨厌的,在这种情况下把所有的请求都给 PHP 处理。为什么?Apache 也许是这样做,但你不必。让我这么说吧...
try_files指令存在的原因,它按特定的顺序尝试文件。意味着 NGINX 可以先尝试提供静态内容。如果不能,继续尝试。
这样 PHP 根本不参与,快的多。特别是如果你通过 PHP 提供 1MB 图片的时间是直接提供的1000倍,让我们看看该怎么做。

GOOD

server {
    server_name _;
    root /var/www/site;
    location / {
        try_files $uri $uri/ @proxy;
    }
    location @proxy {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/tmp/phpcgi.socket;
    }
}

Also GOOD

server {
    server_name _;
    root /var/www/site;
    location / {
        try_files $uri $uri/ /index.php;
    }
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/tmp/phpcgi.socket;
    }
}

很简单,是不是?如果请求的 URI 存在,NGINX 直接提供。如果不存在,检查目录是否存在。如果不存在,传递给你的代理。
只有当 NGINX 不能直接服务请求的 URI,你的代理将参与开销。

现在想想你的多少请求是静态资源,比如 images、css、javascript 等。你可能节省很多开销。

配置更改没有生效

浏览器缓存。你的配置也许是很棒的,但你会坐在那,撞墙一个月。问题出在你的浏览器缓存。
当你下载了东西,你的浏览器会保存下来。同时也保存了文件是如何提供的。如果你配置有 types 区块,你会遇到这情况。

处理方法:

  • 在 Firefox 浏览器中按 Ctrl+Shift+Delete,检查缓存,点击 Clear Now。其他浏览器,可搜索下方法。
    每次更改之后都清除下,会为你节省不少麻烦。
  • 使用 curl

VirtualBox

如果这不起作用,而且你在 VirtualBox 中的虚拟机中运行 NGINX,也许是sendfile()引起的问题。
简单的注释掉sendfile指令,或者设置为“off”。

sendfile off;

缺失的 HTTP 头

如果你没有明确的设置underscores_in_headers on,NGINX 将会默默地忽略掉带有下划线的头信息(根据 HTTP 标准是完全有效的)。
这样做为了防止头信息映射到 CGI 变量过程中破折号和下划线都映射成下划线引起歧义。

没有使用标准的 Document Root Locations

有些目录在任何文件系统中不应该用于托管数据。其中包括//root。你不应该使用这些作为网站根目录。

这样作让你的隐私数据毫无预期的返回给请求。

永远不要这样做!!!

server {
    root /;

    location / {
        try_files /web/$uri $uri @php;
    }

    location @php {
        # [...]
    }
}

当一个请求/foo,因为文件找不到将代理到 php 。这看起来正常,直到请求/etc/passwd。是的,你给了服务器上所有用户列表。
在某些情况下,Nginx 服务器进程被设置以 root 用户运行。是的,我们现在有你的用户列表以及密码 hash 密码,以及如何 hash 的。

文件系统层级标准定义了数据应该存在哪里。你应该看看。
简而言之,你的 web 内容可以存放在/var/www//srv/usr/share/www

使用默认的 Document Root

Ubuntu、Debian 等其他操作系统中NGINX包,作为易于安装的包会提供默认的配置文件用作一个例子,而且会包含网站根目录存储基本的
HTML 文件。

多数系统包不检查默认的Document Root中文件是否存在或者修改。这样会在软件包升级时导致代码丢失。经验丰富的系统管理员没有期望在
升级期间默认的document root内容保持不变。

你不应该对将网站的关键文件使用默认的document root。在你的系统升级或者更新 NGINX 包时有很大可能性会导致默认的document root
内容会改变。

使用 Hostname 去解析地址

BAD

upstream {
    server http://someserver;
}

server {
    listen myhostname:80;
    # [...]
}

你不应该在 listen 指令中使用 hostname。也许这样有效,也将会有大量的问题。
其中一个问题是在服务启动或者重启时 hostname 不能解析。这会引起 NGINX 无法绑定到特定的 TCP 端口,最终 NGINX 启动失败。

更安全的做法是知道需要绑定的 IP 地址,并使用 IP 而不是 hostname。 阻止 NGINX 需要查找 IP ,不依赖内部或者外部的解析。

upstream 区块也有同样的问题。并不是总要在 upstream 区块避免使用 hostname,这不是好的做法,使用时要考虑清楚。

upstream {
    server http://10.48.41.12;
}

server {
    listen 127.0.0.16:80;
    # [...]
}

HTTPS 中使用 SSLv3

由于 SSLv3 的POODLE漏洞,建议在 SSL 的网站不启用 SSLv3。你可以很简单的禁用 SSLv3,只提供 TLS 协议。

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;