简介
众所周知,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 上进行初始服务器设置》。
此外,您还需要一个指向 Droplet 的完全合格的域名(FQDN)以进行测试,除了默认的 localhost
。如果您手头没有,可以使用 site1.example.org
。使用您喜欢的编辑器编辑 /etc/hosts
文件,添加以下行(如果您使用它,请将 site1.example.org
替换为您的 FQDN):
... 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 访问它。
现在我们将创建一个具有自己的 php-fpm 池和 Linux 用户的第二个站点(site1.example.org)。
让我们从创建必要的用户开始。为了获得最佳隔离性,新用户应该有自己的组。因此,首先创建用户组 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:端口组合,但这将需要更多的 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
该文件应包含:
[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 脚本打开远程文件,可能会被攻击者利用。
pm
选项不在当前安全主题之内,但您应该知道它们允许您配置池的性能。
chdir
选项应该是 /
,即文件系统的根目录。除非您使用另一个重要选项 chroot
,否则不应更改此选项。
故意未在上述配置中包含选项 chroot
。它将允许您在受限环境中运行池,即锁定在一个目录中。这对于安全性很好,因为您可以将池锁定在站点的 Web 根目录中。但是,这种终极安全性将为依赖于系统二进制文件和应用程序(如 Imagemagick)的任何体面的 PHP 应用程序带来严重问题。如果您对此话题感兴趣,请阅读文章《如何使用 Firejail 在受限环境中设置 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
,并添加以下行:
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
该文件应包含以下内容:
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
。
创建 Web 根目录:
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
步骤 3 — 测试
为了运行测试,我们将使用众所周知的 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
文件。它应该可以在本地主机上访问,命令如下:
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
的新文件。该文件应包含以下内容:
<?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
的文件来读取敏感信息并将其打印出来。该文件应包含以下代码:
<?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 Web 根目录的内容。您应该看到:
-rw-r--r-- 1 site1 site1 80 Jun 21 16:44 readfile.php
然后尝试从 site1.example.com 访问相同的文件,命令为 lynx --dump http://site1.example.org/readfile.php
。您将只看到返回的空格。此外,如果您使用 grep 命令在 nginx 的错误日志中搜索错误,命令为 sudo grep error /var/log/nginx/error.log
,您将看到:
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
警告显示,来自 site1.example.org 站点的脚本无法读取主站点的敏感文件 config.php
。因此,运行在不同用户下的站点不会危及彼此的安全性。
如果您回到本文的配置部分末尾,您将看到我们已禁用了 opcache 提供的默认缓存。如果您好奇为什么,请尝试通过使用超级用户权限在文件 /etc/php5/fpm/conf.d/05-opcache.ini
中设置 opcache.enable=1
,然后使用命令 sudo service php5-fpm restart
重新启动 php5-fpm 来重新启用 opcache。
令人惊讶的是,如果您按照完全相同的顺序再次运行测试步骤,您将能够读取敏感文件,而不管其所有权和权限如何。这个 opcache 中的问题已经报告了很长时间,但截至本文撰写时,它尚未被修复。
结论
从安全的角度来看,对于在同一台 Nginx web 服务器上的每个站点使用不同用户的 php-fpm 池是至关重要的。即使这会带来一些性能损失,但这种隔离的好处可以防止严重的安全漏洞。
本文描述的思想并不是独一无二的,在其他类似的 PHP 隔离技术中也存在,比如 SuPHP。然而,所有其他替代方案的性能都远远不如 php-fpm。