如何在Ubuntu14.04上使用Nginx和Php-fpm安全地托管多个网站
介绍
众所周知,LEMP 堆栈(Linux、nginx、MySQL、PHP)为运行 PHP 站点提供了无与伦比的速度和可靠性。 不过,这种流行堆栈的其他好处(例如安全性和隔离性)不太受欢迎。
在本文中,我们将向您展示使用不同 Linux 用户在 LEMP 上运行站点的安全性和隔离优势。 这将通过为每个 nginx 服务器块(站点或虚拟主机)创建不同的 php-fpm 池来完成。
先决条件
本指南已在 Ubuntu 14.04 上进行了测试。 所描述的安装和配置在其他操作系统或操作系统版本上会类似,但配置文件的命令和位置可能会有所不同。
它还假设您已经设置了 nginx 和 php-fpm。 如果没有,请按照文章 如何在 Ubuntu 14.04 上安装 Linux、nginx、MySQL、PHP (LEMP) 堆栈中的第一步和第三步进行操作。
本教程中的所有命令都应以非 root 用户身份运行。 如果该命令需要 root 访问权限,它将在前面加上 sudo
。 如果您还没有设置,请按照本教程进行操作:使用 Ubuntu 14.04 进行初始服务器设置。
除了默认的 localhost
之外,您还需要一个指向 Droplet 的完全限定域名 (fqdn) 以进行测试。 如果手头没有,可以使用site1.example.org
。 使用您喜欢的编辑器编辑 /etc/hosts
文件,例如 sudo vim /etc/hosts
并添加这一行(如果您正在使用,请将 site1.example.org
替换为您的 fqdn):
/etc/hosts
... 127.0.0.1 site1.example.org ...
另外获得 LEMP 的原因
在一个常见的 LEMP 设置下,只有一个 php-fpm 池可以为同一用户下的所有站点运行所有 PHP 脚本。 这带来了两个主要问题:
- 如果一个 nginx 服务器上的 web 应用程序阻塞,即 如果子域或单独的站点受到攻击,则此 Droplet 上的所有站点也会受到影响。 攻击者能够读取其他站点的配置文件,包括数据库详细信息,甚至更改它们的文件。
- 如果您想让用户访问您的 Droplet 上的某个站点,您实际上将授予他对所有站点的访问权限。 例如,您的开发人员需要在暂存环境中工作。 但是,即使具有非常严格的文件权限,您仍然可以让他访问同一 Droplet 上的所有站点,包括您的主站点。
上述问题在 php-fpm 中通过创建一个不同的池来解决,该池在每个站点的不同用户下运行。
第 1 步 — 配置 php-fpm
如果您已经满足了先决条件,那么您应该已经在 Droplet 上拥有一个功能性网站。 除非你为它指定了自定义的 fqdn,否则你应该可以在本地的 fqdn localhost
下访问它,或者通过 droplet 的 IP 远程访问它。
现在我们将创建第二个站点 (site1.example.org),它有自己的 php-fpm 池和 Linux 用户。
让我们从创建必要的用户开始。 为了最好的隔离,新用户应该有自己的组。 所以首先创建用户组site1
:
sudo groupadd site1
然后请创建一个属于该组的用户 site1:
sudo useradd -g site1 site1
到目前为止,新用户 site1 没有密码,无法登录 Droplet。 如果您需要让某人直接访问该站点的文件,那么您应该使用命令 sudo passwd site1
为该用户创建一个密码。 使用新的用户/密码组合,用户可以通过 ssh 或 sftp 远程登录。 有关更多信息和安全详细信息,请查看文章 设置具有受限目录访问权限的辅助 SSH/SFTP 用户 。
接下来,为 site1 创建一个新的 php-fpm 池。 php-fpm 池本质上只是一个普通的 Linux 进程,它在特定用户/组下运行并侦听 Linux 套接字。 它也可以侦听 IP:port 组合,但这需要更多的 Droplet 资源,而且它不是首选方法。
默认情况下,在 Ubuntu 14.04 中,每个 php-fpm 池都应配置在目录 /etc/php5/fpm/pool.d
内的文件中。 此目录中每个扩展名为 .conf
的文件都会自动加载到 php-fpm 全局配置中。
因此,对于我们的新站点,让我们创建一个新文件 /etc/php5/fpm/pool.d/site1.conf
。 您可以使用您最喜欢的编辑器执行此操作,如下所示:
sudo vim /etc/php5/fpm/pool.d/site1.conf
该文件应包含:
/etc/php5/fpm/pool.d/site1.conf
[site1] user = site1 group = site1 listen = /var/run/php5-fpm-site1.sock listen.owner = www-data listen.group = www-data php_admin_value[disable_functions] = exec,passthru,shell_exec,system php_admin_flag[allow_url_fopen] = off pm = dynamic pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 chdir = /
在上述配置中,请注意这些特定选项:
[site1]
是池的名称。 对于每个池,您必须指定一个唯一的名称。user
和group
代表 Linux 用户和新池将在其下运行的组。listen
应该指向每个池的唯一位置。listen.owner
和listen.group
定义了监听器的所有权,即 新的 php-fpm 池的套接字。 Nginx 必须能够读取此套接字。 这就是为什么使用运行 nginx 的用户和组创建套接字的原因 -www-data
。php_admin_value
允许您设置自定义 php 配置值。 我们用它来禁用可以运行 Linux 命令的功能 -exec,passthru,shell_exec,system
。php_admin_flag
与php_admin_value
类似,但只是布尔值的开关,即 开和关。 我们将禁用 PHP 函数allow_url_fopen
,该函数允许 PHP 脚本打开远程文件并可能被攻击者使用。
注: 上面的php_admin_value
和php_admin_flag
值也可以全局应用。 但是,站点可能需要它们,这就是默认情况下未配置它们的原因。 php-fpm 池的美妙之处在于它允许您微调每个站点的安全设置。 此外,这些选项可用于安全范围之外的任何其他 php 设置,以进一步自定义站点的环境。
pm
选项不在当前安全主题范围内,但您应该知道它们允许您配置池的性能。
chdir
选项应该是 /
这是文件系统的根。 除非您使用另一个重要选项 chroot
,否则不应更改此设置。
选项 chroot
故意不包含在上述配置中。 它将允许您在被监禁的环境中运行池,即 锁定在目录中。 这对安全性非常有用,因为您可以将池锁定在站点的 Web 根目录内。 然而,这种最终的安全性对于任何依赖系统二进制文件和应用程序(如 Imagemagick)的体面的 PHP 应用程序都会造成严重问题,这些应用程序将不可用。 如果您对此主题进一步感兴趣,请阅读文章 如何使用 Firejail 在 Jailed 环境中设置 WordPress 安装。
完成上述配置后,重新启动 php-fpm 以使新设置通过以下命令生效:
sudo service php5-fpm restart
通过搜索其进程来验证新池是否正常运行,如下所示:
ps aux |grep site1
如果您已按照此处的确切说明进行操作,您应该会看到类似于以下内容的输出:
site1 14042 0.0 0.8 133620 4208 ? S 14:45 0:00 php-fpm: pool site1 site1 14043 0.0 1.1 133760 5892 ? S 14:45 0:00 php-fpm: pool site1
红色是运行进程或 php-fpm 池的用户 - site1。
此外,我们将禁用 opcache 提供的默认 php 缓存。 这个特殊的缓存扩展可能对性能很有好处,但它不适合我们稍后将看到的安全性。 要禁用它,请使用超级用户权限编辑文件 /etc/php5/fpm/conf.d/05-opcache.ini
并添加以下行:
/etc/php5/fpm/conf.d/05-opcache.ini
opcache.enable=0
然后再次重启 php-fpm (sudo service php5-fpm restart
) 使设置生效。
第 2 步 — 配置 nginx
一旦我们为我们的站点配置了 php-fpm 池,我们将在 nginx 中配置服务器块。 为此,请使用您喜欢的编辑器创建一个新文件 /etc/nginx/sites-available/site1
,如下所示:
sudo vim /etc/nginx/sites-available/site1
该文件应包含:
/etc/nginx/sites-available/site1
server { listen 80; root /usr/share/nginx/sites/site1; index index.php index.html index.htm; server_name site1.example.org; location / { try_files $uri $uri/ =404; } location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/var/run/php5-fpm-site1.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
上面的代码显示了 nginx 中服务器块的常见配置。 注意有趣的突出部分:
- Web 根目录为
/usr/share/nginx/sites/site1
。 - 服务器名称使用本文先决条件中提到的 fqdn
site1.example.org
。 fastcgi_pass
指定 php 文件的处理程序。 对于每个站点,您应该使用不同的 unix 套接字,例如/var/run/php5-fpm-site1.sock
。
创建网络根目录:
sudo mkdir /usr/share/nginx/sites sudo mkdir /usr/share/nginx/sites/site1
要启用上述站点,您必须在目录 /etc/nginx/sites-enabled/
中创建指向它的符号链接。 这可以通过以下命令完成:
sudo ln -s /etc/nginx/sites-available/site1 /etc/nginx/sites-enabled/site1
最后,重新启动 nginx 以使更改生效,如下所示:
sudo service nginx restart
第三步——测试
为了运行测试,我们将使用众所周知的 phpinfo 函数,它提供有关 php 环境的详细信息。 在名称 info.php
下创建一个新文件,其中仅包含 <?php phpinfo(); ?>
行。 您将首先在默认 nginx 站点及其 Web 根目录 /usr/share/nginx/html/
中需要此文件。 为此,您可以使用如下编辑器:
sudo vim /usr/share/nginx/html/info.php
之后将文件复制到另一个站点 (site1.example.org) 的 Web 根目录,如下所示:
sudo cp /usr/share/nginx/html/info.php /usr/share/nginx/sites/site1/
现在您已准备好运行最基本的测试来验证服务器用户。 您可以使用浏览器或从 Droplet 终端和命令行浏览器 lynx 执行测试。 如果您的 Droplet 上还没有 lynx,请使用命令 sudo apt-get install lynx
安装它。
首先检查默认站点中的 info.php
文件。 它应该可以在 localhost 下访问,如下所示:
lynx --dump http://localhost/info.php |grep 'SERVER\["USER"\]'
在上面的命令中,我们使用 grep 过滤输出,仅针对代表服务器用户的变量 SERVER["USER"]
。 对于默认站点,输出应显示默认的 www-data
用户,如下所示:
_SERVER["USER"] www-data
同样,接下来检查 site1.example.org 的服务器用户:
lynx --dump http://site1.example.org/info.php |grep 'SERVER\["USER"\]'
您应该在 site1
用户的输出中看到这次:
_SERVER["USER"] site1
如果您在每个 php-fpm 池的基础上进行了任何自定义 php 设置,那么您还可以通过过滤您感兴趣的输出以上述方式检查它们的对应值。
到目前为止,我们知道我们的两个站点在不同的用户下运行,但现在让我们看看如何保护连接。 为了演示我们在本文中解决的安全问题,我们将创建一个包含敏感信息的文件。 通常这样的文件包含到数据库的连接字符串,并包含数据库用户的用户和密码详细信息。 如果有人发现该信息,则该人可以对相关站点进行任何操作。
使用您最喜欢的编辑器在您的主站点 /usr/share/nginx/html/config.php
中创建一个新文件。 该文件应包含:
/usr/share/nginx/html/config.php
<?php $pass = 'secret'; ?>
在上面的文件中,我们定义了一个名为 pass
的变量,它保存了值 secret
。 自然,我们想要限制对该文件的访问,因此我们将其权限设置为 400,这将授予文件所有者只读访问权限。
要将权限更改为 400,请运行以下命令:
sudo chmod 400 /usr/share/nginx/html/config.php
此外,我们的主站点在应该能够读取此文件的用户 www-data
下运行。 因此,将文件的所有权更改为该用户,如下所示:
sudo chown www-data:www-data /usr/share/nginx/html/config.php
在我们的示例中,我们将使用另一个名为 /usr/share/nginx/html/readfile.php
的文件来读取机密信息并打印出来。 该文件应包含以下代码:
/usr/share/nginx/html/readfile.php
<?php include('/usr/share/nginx/html/config.php'); print($pass); ?>
将此文件的所有权也更改为 www-data
:
sudo chown www-data:www-data /usr/share/nginx/html/readfile.php
要确认 Web 根目录中的所有权限和所有权都是正确的,请运行命令 ls -l /usr/share/nginx/html/
。 您应该看到类似于以下内容的输出:
-r-------- 1 www-data www-data 27 Jun 19 05:35 config.php -rw-r--r-- 1 www-data www-data 68 Jun 21 16:31 readfile.php
现在使用命令 lynx --dump http://localhost/readfile.php
在您的默认站点上访问后一个文件。 您应该能够在输出 secret
中看到打印的内容,这表明可以在同一站点内访问包含敏感信息的文件,这是预期的正确行为。
现在将文件 /usr/share/nginx/html/readfile.php
复制到您的第二个站点 site1.example.org,如下所示:
sudo cp /usr/share/nginx/html/readfile.php /usr/share/nginx/sites/site1/
要保持站点/用户关系井然有序,请确保在每个站点内,文件归各自站点用户所有。 通过使用以下命令将新复制的文件的所有权更改为 site1 来执行此操作:
sudo chown site1:site1 /usr/share/nginx/sites/site1/readfile.php
要确认您已设置正确的文件权限和所有权,请使用命令 ls -l /usr/share/nginx/sites/site1/
列出 site1 网络根目录的内容。 你应该看到:
-rw-r--r-- 1 site1 site1 80 Jun 21 16:44 readfile.php
然后尝试使用命令 lynx --dump http://site1.example.org/readfile.php
从 site1.example.com 访问相同的文件。 您只会看到返回的空白空间。 此外,如果您使用 grep 命令 sudo grep error /var/log/nginx/error.log
在 nginx 的错误日志中搜索错误,您将看到:
2015/06/30 15:15:13 [error] 894#0: *242 FastCGI sent in stderr: "PHP message: PHP Warning: include(/usr/share/nginx/html/config.php): failed to open stream: Permission denied in /usr/share/nginx/sites/site1/readfile.php on line 2
注意: 如果您在 php-fpm 配置文件 /etc/php5/fpm/php.ini
中将 display_errors
设置为 On
,您也会在 lynx 输出中看到类似的错误。
警告显示来自 site1.example.org 站点的脚本无法从主站点读取敏感文件 config.php
。 因此,在不同用户下运行的站点不能损害彼此的安全性。
如果你回到本文的配置部分,你会看到我们已经禁用了 opcache 提供的默认缓存。 如果您好奇为什么,请尝试通过在文件 /etc/php5/fpm/conf.d/05-opcache.ini
中设置超级用户权限 opcache.enable=1
来再次启用 opcache,然后使用命令 sudo service php5-fpm restart
重新启动 php5-fpm。
令人惊讶的是,如果您以完全相同的顺序再次运行测试步骤,您将能够读取敏感文件,而不管其所有权和权限如何。 opcache中的这个问题已经报告了很长时间,但是到本文的时候还没有修复。
结论
从安全的角度来看,必须为同一 Nginx Web 服务器上的每个站点使用具有不同用户的 php-fpm 池。 即使会带来很小的性能损失,这种隔离的好处也可以防止严重的安全漏洞。
本文中描述的想法不是唯一的,它存在于其他类似的 PHP 隔离技术中,例如 SuPHP。 但是,所有其他替代方案的性能都比 php-fpm 差得多。