Nginx 配置中的误区与常见错误

最近有感于Nginx配置中的陷阱,以及网上搜出来的大多数Nginx配置的不靠谱,特地翻译了这篇官方WIKI上的文档,用于提醒自己。当然翻译时间仓促,其中难免有疏漏和与原文相比不准确的地方,如果英文能力足够最好阅读原文。

Nginx 配置中的误区与常见错误

原文:Pitfalls and Common Mistakes

翻译:hcl

无论新老用户都很有可能陷入一定的误区。下面就是一篇展示我们经常见到的问题以及如何避免它们的概要。在Freenode的#nginx IRC channel上,我们经常见到这些问题。

这篇教程说了什么?

我们最常见的问题就是一些人只是从其他教程那里复制粘贴一些配置片段。并不是所有外面的教程都是错的,但大部分都是。甚至连Linode知识库里面的信息的质量都低的可怕,一些NGINX社区的成员试图去纠正也变成了徒劳。

我们创建的这些被社区成员检查过的文档可以直接在所有类型的NGINX用户那里正常工作。这篇特定的文档只是因为社区成员看到了大量共同的、经常出现的问题。

我的问题没有被列在其中

你不会在这里看到针对你特定问题的东西。也许我们没有提到你的问题是因为你所遇到的问题太过于具体。不要匆匆扫过这个页面然后觉得你看到这个页面是没有任何原因的。你之所以被带到这个页面是因为你犯了一些这里列出的错误。

当为大量的用户的大量问题提供支持的时候,社区成员不想去理会那些蹩脚的配置。在提问前先修正你的配置。通过这篇文章来修正你的配置,千万不要跳过。

Chmod 777

绝对不要使用777。它看起来可能很顺眼,但是即使是在测试中也是一个意味着对你做的事情没有任何线索。查看完整路径的权限然后想想会发生什么。

你可以用下面的命令来快速地显示一个路径的全部权限:

namei -om /path/to/check

Location块中的root

反面例子:

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。让我们看看正确的配置。

正确配置:

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

多重Index指令

错误例子:

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{}块就可以继承到下面。

正确配置:

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的用法。

参见

If is Evil

服务器主机名

错误配置:

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

这里面其实有三个问题。第一个是用了if。这是我们现在关心的问题。为什么是错的的呢?你读没读IfIsEvil?当NGINX收到一个请求,无论请求的子域名是什么,或是www.example.com,或者就只是example.com,这个if判断总是会被执行。因为你在要求NGINX对每个请求的主机头进行检查。这非常低效。作为替代,你应该使用两个server指令,就像下面的例子那样。

正确配置:

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能让你的生活更加轻松一点。

错误配置:

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

正确配置:

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

我们所做改变的是避免使用if判断$uri是否存在。使用try_files的好处是你可以按一定的顺序判断。如果$uri不存在,那么判断$uri/,如果也不存在就尝试一个回退的地方。

Web应用的前段控制器模式

前端控制器模式设计非常的流行,并且在许多最流行的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结尾的请求发给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;
    # [...]
}

脚本文件名中的FastCGI路径

许多其他地方的教程喜欢依赖绝对路径来获取信息。这在PHP块里面十分常见。当你从软件仓库安装NGINX之后,你在配置中经常可以看到这样的配置语句include fastcgi_params;。这是一个放在NGINX根目录(一般是在/etc/nginx/)下的文件。

正确配置:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

错误配置:

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

$document_root被设置在了哪里呢?它应该是你server块里的root指令。你的root指令不在那里?会去看第一节。

繁杂的Rewrites

不要在这里感到不舒服,对于各种正则表达式很容易就会变得迷惑。事实上,我们很容易就能做到,而且也应该努力做到让它们保持整洁。非常简单,不要增加任何的繁杂。

错误配置:

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

正确配置:

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

更好的配置:

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

看看上面的配置,然后回到这里,再看看一遍。好的,第一条rewrite捕获了除了第一个斜杠之前的整个URI。通过使用内置变量$request_uri我们可以高效地完全避免任何捕获或者匹配。

Rewrite语句丢失http://

一个非常简单的原则是,rewrite语句默认是相互关联的除非你告诉NGINX不是。让一个rewrite语句更绝对很简单。只要加一个scheme就可以了。

错误配置:

rewrite ^ example.com permanent;

正确配置:

rewrite ^ http://example.com permanent;

代理所有东西

错误配置:

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根本不会参与这个过程,也就意味着更快。特别是当你放上一个1MB的图片,通过PHP返回和直接返回上几千次来比较一下。下面来看如何做到。

正确配置:

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;
    }
}

同样正确的:

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的时候你的代理才参与进来。

考虑一下你的服务器请求中有多少是静态资源(图片,CSS或者JavaScript等等)。这可能使你可以节约的一大笔开销。

配置不能生效

浏览器缓存。你的配置可能非常完美,但是你可能一个月都坐在那里用头撞墙。出错的是你浏览器的缓存。当你下载了什么,浏览器会存储它。浏览器还会存储这些文件是怎么被提供的。如果你使用了types{}块,这是你可能遇到的。

解决方案:

  • 在Firefox中按Ctrl+Shift+Delete,检查缓存,点击“立即清理”。其他的浏览器中的操作,去问你最喜欢的搜索引擎吧。在每次修改后重复一遍(除非你十分清楚不需要),你会从一大堆的头疼中解放出来的。

  • 使用curl。

VirtualBox

如果上面的解决方法不行的话,然后你又在Virtual Box中运行NGINX,有可能是sendfile()导致的问题。你只要简单地注释掉sendfile指令或者将其设置为“off”,这条指令应该在你的nginx.conf中出现。

sendfile off;

消失的HTTP头

如果你没有明确地设置underscores_in_headers on;,NGINX就会丢弃掉带有下划线的HTTP头(根据HTTP标准这完全是可行的)。这是为了防止在将HTTP头映射到CGI变量时,横杠和下划线都会被映射为下划线所造成的歧义。

不使用标准根目录位置

在任何文件系统中都有一些目录永远不应该用于存放网站文件。这里面就包括/root。你绝对不应该把这些目录用于你的文档根目录。

这么做会让你的隐私数据对一个访问你所期望区域之外的请求完全地敞开大门。

绝对不要这么做(是的,我们曾经见过)

server {
    root /;

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

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

当发出一个对/foo请求,这个请求被传到php因为文件不存在。这倒是可以工作地很好,但是如果发出的是一个对/etc/passwd的请求呢?是的,你就列出了服务器上所有用户的列表。在一些案例中,NGINX服务器甚至把worker设置成了root。得,我们现在我们现在有了你的用户列表、密码的Hash以及密码是怎么Hash的。你的服务器已经对我们完全敞开了大门。

使用默认的网站根目录

在Ubuntu、Debian或者其他操作系统都有打包好的NGINX,作为一个一键安装的包经常会提供一个默认的配置文件作为配置方法的范例,通常也会包含一个放有基本HTML文件的网站根目录。

大多数这些软件包都不会检查这个默认网站目录下的文件是否被修改或是是否存在,这就导致当软件包升级之后里面的代码就丢失了。有经验的系统管理员知道默认网站根目录里面基本上不会放什么数据,于是在升级期间也不会去动里面的东西。

你不应该使用默认的网站根目录来存放任何和你网站相关的重要文件。不应该认为默认网站根目录下面会被系统留着不动,而是有很大的可能性你的重要网站相关的重要文件就会在升级你操作系统NGINX软件包的时候丢失。

使用主机名来指定地址

错误配置:

upstream {
    server http://someserver;
}

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

你永远不应该在listen指令中使用主机名。虽然能正常工作,但是会带来一大堆的问题。一个问题就是主机名在启动的时候或是服务重启的时候可能并未指定。这就导致NGINX不能绑定所需的TCP套接字,也就导致NGINX根本启动不了。

一个更安全的实践是知道需要绑定的IP地址来代替主机名。这也就避免了NGINX需要去查询地址,也去掉了对于内部或外部解释器的依赖。

同样的问题在upstream块中也存在。虽然可能不能总是避免在upstream块使用主机名,但是这确实是一个糟糕的实践,并且需要仔细地考虑来避免问题。

正确配置:

upstream {
    server http://10.48.41.12;
}

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

在HTTPS中使用SSLv3

由于SSLv3的POODLE弱点攻击,建议不要在你启用HTTPS的站点使用SSLv3。你可以通过下面的配置简单地关闭SSLv3,并且只提供TLS协议作为替代;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

发布者

hcl

TechOtaku 站长。

《Nginx 配置中的误区与常见错误》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注