介绍
众所周知,LEMP堆栈(Linux,nginx,MySQL,PHP)为运行PHP网站提供了无与伦比的速度和可靠性。
在本文中,我们将向您展示在LEMP上运行不同Linux用户的网站的安全性和隔离优势,这将通过为每个nginx服务器块(网站或虚拟主机)创建不同的php-fpm池来完成。
前提条件
此指南已在 Ubuntu 14.04 上测试,描述的安装和配置在其他 OS 或 OS 版本上类似,但配置文件的命令和位置可能有所不同。
它还假定你已经安装了 nginx 和 php-fpm. 如果没有,请遵循文章中的第一步和第三步 How To Install Linux, nginx, MySQL, PHP (LEMP) stack on Ubuntu 14.04)。
本教程中的所有命令都应该作为非 root 用户运行. 如果命令需要 root 访问,则将先由 sudo
。
你还需要一个完全合格的域名(fqdn),它指向Droplet进行测试,除了默认的localhost
。如果你没有一个,你可以使用site1.example.org
。用你最喜欢的编辑器编辑/etc/hosts
文件,如sudo vim /etc/hosts
,并添加这个行(如果你正在使用它,则用你的fqdn代替site1.example.org
):
1[label /etc/hosts]
2...
3127.0.0.1 site1.example.org
4...
额外保护LEMP的理由
在一个常见的LEMP设置中,只有一个php-fpm池,在同一用户下运行所有PHP脚本。
- 如果一个 nginx 服务器块上的 Web 应用程序,即子域或单独的网站被破坏,那么这个 Droplet 上的所有网站也将受到影响。攻击者可以读取其他网站的配置文件,包括数据库细节,甚至改变他们的文件。
- 如果你想让用户访问你的 Droplet 上的一些网站,你实际上会让他访问所有网站。例如,你的开发者需要在舞台环境中工作。然而,即使有非常严格的文件权限,你仍然会让他访问所有网站,包括你的主要网站,在同一个 Droplet.
上述问题在php-fpm中通过创建一个不同的池子来解决,该池子在每个网站的不同用户下运行。
步骤 1 – 配置 php-fpm
如果你已经满足了前提条件,那么你应该已经在Droplet上有一个功能性的网站,除非你为它指定了自定义fqdn,否则你应该能够在fqdn下本地访问它,或者通过Droplet的IP远程访问它。
现在我们将创建一个第二个网站(site1.example.org),拥有自己的php-fpm池和Linux用户。
让我们从创建必要的用户开始。为了获得最佳的隔离,新用户应该有自己的组。
1sudo groupadd site1
然后请创建一个用户网站1属于这个组:
1sudo useradd -g site1 site1
到目前为止,新用户网站1没有密码,无法登录Droplet. 如果您需要为某人提供直接访问本网站的文件,那么您应该为该用户创建一个密码,用命令 sudo passwd site1
. 使用新用户/密码组合,用户可以通过ssh或sftp远程登录。
接下来,为网站创建一个新的php-fpm池1. 一个php-fpm池在其本质上只是一个普通的Linux流程,它在某些用户/组下运行,并在Linux插槽上收听。
默认情况下,在 Ubuntu 14.04 中,每个 php-fpm 池都应该被配置为在目录内的一份文件 /etc/php5/fpm/pool.d
. 该目录中的每个扩展 .conf
文件都会自动加载到 php-fpm 全球配置中。
因此,对于我们的新网站,让我们创建一个新的文件 /etc/php5/fpm/pool.d/site1.conf
. 您可以用您最喜欢的编辑器这样做:
1sudo vim /etc/php5/fpm/pool.d/site1.conf
该文件应包含:
1[label /etc/php5/fpm/pool.d/site1.conf]
2[site1]
3user = site1
4group = site1
5listen = /var/run/php5-fpm-site1.sock
6listen.owner = www-data
7listen.group = www-data
8php_admin_value[disable_functions] = exec,passthru,shell_exec,system
9php_admin_flag[allow_url_fopen] = off
10pm = dynamic
11pm.max_children = 5
12pm.start_servers = 2
13pm.min_spare_servers = 1
14pm.max_spare_servers = 3
15chdir = /
在上面的配置中,请注意这些具体选项:
对于每个池,你必须指定一个独特的名称。
*用户
和组
代表Linux用户和新池将运行的组。
*倾听
应该指向每个池的独特位置。
*listen.owner
和listen.group
定义了倾听者的所有权,即新 php-fpm池的插槽。 Nginx必须能够读取这个插槽。这就是为什么插槽与用户和组一起创建,其中nginx运行 - www-data.
*php_admin_value允许你设置自定义的php配置值。我们使用它来取消功能,它可以运行Linux命令 - `exec,passe``,_exhole_ex
<$>[note]
** 注意:** 上面的 php_admin_value
和 php_admin_flag
值也可能在全球范围内应用,但是,网站可能需要它们,这就是为什么默认情况下它们没有配置的原因。php-fpm 池的美好之处在于它允许您精确调整每个网站的安全设置。此外,这些选项可以用于任何其他 PHP 设置,不包括安全范围,以进一步定制网站的环境。
pm
选项不在当前的安全主题之外,但您应该知道它们允许您配置池的性能。
chdir
选项应该是/
,这是文件系统的根源,除非你使用另一个重要的选项chroot
。
chroot
选项不包含在上述配置中,它允许您在监狱环境中运行池,即锁定在目录中。这对安全而言非常好,因为您可以锁定池在网站的 Web 根中。然而,这种最终的安全性会给任何依赖系统二进制和 Imagemagick 等应用程序的适当 PHP 应用带来严重问题。如果您对这个主题更感兴趣,请阅读文章 如何在监狱环境中使用 Firejail 来设置 WordPress 安装。
一旦您完成了上述配置,请重新启动php-fpm,以便新的设置与命令生效:
1sudo service php5-fpm restart
通过搜索其过程来验证新池是否正常运行:
1ps aux |grep site1
如果您遵循到目前为止的准确指示,您应该看到类似的输出:
1site1 14042 0.0 0.8 133620 4208 ? S 14:45 0:00 php-fpm: pool site1
2site1 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
并添加行:
1[label /etc/php5/fpm/conf.d/05-opcache.ini]
2opcache.enable=0
然后重新启动 php-fpm(‘sudo service php5-fpm restart’)以使设置生效。
步骤 2 – 配置 nginx
一旦我们为我们的网站配置了php-fpm池,我们就会在nginx中配置服务器封锁. 为此,请使用您最喜欢的编辑器创建一个新的文件 /etc/nginx/sites-available/site1
:
1sudo vim /etc/nginx/sites-available/site1
该文件应包含:
1[label /etc/nginx/sites-available/site1]
2server {
3 listen 80;
4
5 root /usr/share/nginx/sites/site1;
6 index index.php index.html index.htm;
7
8 server_name site1.example.org;
9
10 location / {
11 try_files $uri $uri/ =404;
12 }
13
14 location ~ \.php$ {
15 try_files $uri =404;
16 fastcgi_split_path_info ^(.+\.php)(/.+)$;
17 fastcgi_pass unix:/var/run/php5-fpm-site1.sock;
18 fastcgi_index index.php;
19 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
20 include fastcgi_params;
21 }
22}
上面的代码显示了 nginx 中的服务器块的常见配置,请注意有趣的突出部分:
- Web 根是
/usr/share/nginx/sites/site1
. - 服务器名称使用 fqdn
site1.example.org
,这是本文的前提中提到的一个。
创建 Web 根目录:
1sudo mkdir /usr/share/nginx/sites
2sudo mkdir /usr/share/nginx/sites/site1
要启用上述网站,您必须在目录 /etc/nginx/sites-enabled/
中创建一个对其的simlink。
1sudo ln -s /etc/nginx/sites-available/site1 /etc/nginx/sites-enabled/site1
最后,重新启动 nginx 以便更改生效如下:
1sudo service nginx restart
步骤三:测试
为了运行测试,我们将使用已知的 phpinfo 函数,该函数提供有关 php 环境的详细信息. 创建一个名为 info.php
的新文件,该文件只包含行 <?php phpinfo();?>
. 您需要这个文件首先在默认的 nginx 网站和它的 Web 根 /usr/share/nginx/html/
. 为此,您可以使用这样的编辑器:
1sudo vim /usr/share/nginx/html/info.php
然后将文件复制到另一个网站(site1.example.org)的 Web 根,如下:
1sudo cp /usr/share/nginx/html/info.php /usr/share/nginx/sites/site1/
现在你已经准备好运行最基本的测试来验证服务器用户. 你可以使用浏览器或从Droplet终端和Lynx,命令行浏览器进行测试. 如果你还没有Lynx在Droplet上,请用命令sudo apt-get install lynx
安装它。
首先,从您的默认网站检查info.php
文件,它应该在 localhost 下可访问:
1lynx --dump http://localhost/info.php |grep 'SERVER\["USER"\]'
在上面的命令中,我们仅将输出过滤为SERVER
变量,这代表服务器用户,对于默认网站,输出应该显示默认的www-data
用户:
1_SERVER["USER"] www-data
同样,接下来检查服务器用户的 site1.example.org:
1lynx --dump http://site1.example.org/info.php |grep 'SERVER\["USER"\]'
你应该在输出中看到这个时候的site1
用户:
1_SERVER["USER"] site1
如果您已根据每个 php-fpm 池进行任何自定义的 php 设置,那么您也可以通过过滤您感兴趣的输出来检查其相应的值。
到目前为止,我们知道我们的两个网站在不同的用户下运行,但现在让我们看看如何确保连接。 为了展示我们在本文中解决的安全问题,我们将创建一个包含敏感信息的文件。 通常,此类文件包含连接链接到数据库,并包括数据库用户的用户和密码细节。
使用您最喜欢的编辑器创建一个新的文件在您的主要网站 /usr/share/nginx/html/config.php
. 该文件应该包含:
1[label /usr/share/nginx/html/config.php]
2<?php
3$pass = 'secret';
4?>
在上面的文件中,我们定义了一个名为pass
的变量,该变量具有秘密
的值。当然,我们希望限制访问该文件,所以我们将其权限设置为400,这只会让文件的所有者访问读取。
若要將權限變更為 400,請執行以下命令:
1sudo chmod 400 /usr/share/nginx/html/config.php
此外,我们的主要网站运行在用户www-data
下,该用户应该能够读取此文件。
1sudo chown www-data:www-data /usr/share/nginx/html/config.php
在我们的示例中,我们将使用另一个名为 /usr/share/nginx/html/readfile.php
的文件来读取秘密信息并打印它。
1[label /usr/share/nginx/html/readfile.php]
2<?php
3include('/usr/share/nginx/html/config.php');
4print($pass);
5?>
更改此文件的所有权为www-data
:
1sudo chown www-data:www-data /usr/share/nginx/html/readfile.php
要确认所有权限和所有权是正确的,在 Web 根中运行命令 ls -l /usr/share/nginx/html/
。
1-r-------- 1 www-data www-data 27 Jun 19 05:35 config.php
2-rw-r--r-- 1 www-data www-data 68 Jun 21 16:31 readfile.php
现在,您可以访问默认网站上的后者文件,使用命令lynx --dump http://localhost/readfile.php
您应该能够看到在输出中打印的秘密
,这表明含有敏感信息的文件在同一个网站内可访问,这是预期正确的行为。
现在将文件 /usr/share/nginx/html/readfile.php
复制到您的第二个网站, site1.example.org 如下:
1sudo cp /usr/share/nginx/html/readfile.php /usr/share/nginx/sites/site1/
要保持网站/用户关系的顺序,请确保每个网站中的文件由相应的网站用户拥有。
1sudo chown site1:site1 /usr/share/nginx/sites/site1/readfile.php
要确认您已设置了文件的正确权限和所有权,请用命令 ls -l /usr/share/nginx/sites/site1/
列出 site1 web root 的内容。
1-rw-r--r-- 1 site1 site1 80 Jun 21 16:44 readfile.php
然后尝试从 site1.example.com 访问相同的文件,使用命令 lynx --dump http://site1.example.org/readfile.php
. 您只会看到空空间返回。 此外,如果您在 nginx 错误日志中使用 grep 命令 sudo grep error /var/log/nginx/error.log
搜索错误,您将看到:
12015/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
,您也会在 lynx 输出中看到类似的错误。
警告显示,来自 site1.example.org 网站的脚本无法从主网站读取敏感文件 `config.php。
如果你回到本文的最后的配置部分,你会看到我们已经禁用了由opcache提供的默认缓存. 如果你想知道为什么,请尝试通过在文件中设置超级用户权限 opcache.enable=1
以重新启用opcache,并使用命令 sudo service php5-fpm restart
重新启动php5-fpm。
令人惊讶的是,如果您在完全相同的顺序下再次运行测试步骤,您将能够读取敏感文件,无论其所有权和许可。
结论
从安全的角度来看,对于同一个 Nginx 网页服务器上的每个网站,使用 php-fpm 池具有不同的用户是必不可少的。
本文所描述的想法不是独一无二的,它存在于其他类似的PHP隔离技术中,如SuPHP。