PHP-FPM之Chroot执行环境详解


Posted in PHP onAugust 03, 2015

在PHP-FPM中设立chroot,有很好的隔离作用,提高系统安全性,但是要想建立一个合理的PHP-FPM Chroot环境难度有点大,比用debootstrap等工具建立还要麻烦,下面通过参考相关资料,把PHP-FPM之Chroot执行环境整理出来,分享给大家。

本文以Ubuntu 14.04.2为例,php-fpm使用的是 ppa:ondrej/php5-5.6 提供的PHP5.6版本,跟系统自带以及Debian系统的php-fpm和系统目录结构应该是一致的。CentOS请自行调整。

php-fpm的chroot环境配置和所使用的服务器前端没有关联,也不强求Apache/Nginx进行chroot。当然那样更安全——也更复杂。

1.建立目录结构

chroot的目录选择为 /var/www/chroot ,其中页面文件放置在 /var/www/chroot/public 。

执行下面的命令建立基本的目录结构:

bash
mkdir -p /var/www/chroot/
cd /var/www/chroot
mkdir -p public bin dev tmp usr/sbin/ usr/share/zoneinfo/ var/run/nscd/ var/lib/php5/sessions var/www
cp -a /dev/zero /dev/urandom /dev/null dev/ #注3
chmod --reference=/tmp tmp/
chmod --reference=/var/lib/php5/sessions var/lib/php5/sessions #注4
chown -R root:root .     #注2
chown -R www-data:www-data public/ #注2
cd var/www
ln -s ../.. chroot     #注1

下面是此时目录结构,之后还会添加一些新的东西:

/var/www/chroot/
├── bin
├── dev
│ ├── null
│ ├── urandom
│ └── zero
├── public
├── tmp
├── usr
│ ├── sbin
│ └── share
│ └── zoneinfo
└── var
 ├── lib
 │ └── php5
 │ └── sessions
 ├── run
 │ └── nscd
 └── www
 └── chroot -> ../.. #注1

注1:这个软连接用于解决Apache/nginx传给php-fpm的 SCRIPT_FILENAME 在进入chroot后找不到文件(访问php页面返回"File not found")的问题。

以nginx为例,通常设置 SCRIPT_FILENAME 为 $document_root$fastcgi_script_name ,传给php-fpm的脚本路径就是 /var/www/chroot/public/index.php 。而由于php-fpm处在chroot环境下,所以它实际试图去访问的路径就变成了 /var/www/chroot + /var/www/chroot/public/index.php 当然是不存在的。

所以使用一个软连接把chroot环境下的 /var/www/chroot 链接到根目录,就能够正常访问脚本了。

当然也可以将 SCRIPT_FILENAME 设置成 /public$fastcgi_script_name 。但是这样硬编码不利于配置的迁移,仅能用于chroot的环境,切换回非chroot环境的话还需要修改配置。所以不建议这么做。(顺便说一句,有很多老教程里也不使用 $document_root ,直接硬编码根目录,当然也是不可取的)

注2:chroot环境并不是100%安全的。由于php-fpm在chroot环境中的执行权限是www-data,仍然建议把非必要的目录的拥有者设置为root来减少不必要的访问权限。chroot不等于安全,参考 chroot最佳实践 中列出的一些原则。从更安全的角度上讲之后最好也将bin、lib、sbin等目录的读写权限去掉,只留可执行权限,不过也没大差别了……

注3:cp -a除了拷贝文件内容外也会复制文件的权限、模式等信息,可以很方便的直接拿来拷贝zero、urandom和null这三个关键的设备文件。mknod似乎是更为稳妥的方式,不过cp -a我使用起来似乎也没问题。

注4:chmod --reference=XXX会参考XXX的权限设置后面的权限。tmp就不提了,关键是后面的 var/lib/php5/sessions 是php存放session文件的目录,需要让www-data有读写的权限。建议设置完之后再看一眼。当然后面会有测试。

2.PHP-FPM的配置

建立一个新的php-fpm的执行pool来搭建chroot环境。并不建议直接修改php-fpm.conf,因为这样是全局生效的,如果有多个php站点的话会共用一套chroot环境。

其实很多php-fpm的教程都忽略了php-fpm的pool的配置,导致很多人一台服务器上所有站点都共用一套配置,尤其是共用一套php.ini的配置,实际上是不合理的。应当根据站点的需求单独建立pool并在其中调整参数。

在 /etc/php5/fpm/pool.d/ 下新建 chroot.conf (注意必须以.conf结尾,才能被php-fpm.conf调用):

[chroot]
user = www-data
group = www-data
listen = /var/run/php-chroot.sock
listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3

chroot = /var/www/chroot
chdir = /public
;security.limit_extensions = .php
php_flag[display_errors] = on
php_value[date.timezone] = Asia/Hong_Kong
;php_admin_value[session.gc_probability] = 1
;php_admin_value[open_basedir] = "/tmp/:/public/:/var/www/chroot/public/"

前面的参数都比较熟悉了。只需要简单的设置chroot为配置好的环境根目录就可以开启chroot了。通过执行 php5-fpm -t 测试一下之后,用 service php5-fpm reload 即可启用新的pool。当然Apache/nginx对应的配置中要设置好后端。

提一下最后几行。倒数第四行打开了 display_errors ,以便之后对chroot下的php的功能进行测试,测试完了记得注释掉。

设置 session.gc_probability 允许php进程自行对session进行删除回收。正常情况下session是由php添加的cron任务清理的,但是似乎php不会自动清理chroot环境下的session。当然也可以自己在cron.d下添加自动执行的脚本来清理,就不用开启这个选项了。

3.修复Chroot环境下PHP的各项功能

在/var/www/chroot/public下新建一个test.php,写入以下内容:

php

<?php

session_start();

header( "Content-Type: text/plain" );

echo( gethostbyname( "localhost" )."\n" );

print_r( getdate() );

mail( "your@address", "subject", "message" );

这里主要测试的功能是:session、DNS解析、时间和日期、邮件mail()函数。

访问上面的测试页面,提示No such directory or file或者Permission denied说明session配置不正确。 gethostbyname 返回的不是127.0.0.1或者::1,则说明DNS解析没有生效。提示 timezone database is corrupt 之类的,说明时间和日期有错误。mail()也会有各种错误提示。

session就不提了,设置好目录权限就没有问题。主要处理一下后面三个问题,也是php的chroot环境主要需要处理的内容。

3.1 域名解析/时区等问题

mail()的解决方法大同小异,放后面谈。前面的域名解析等问题这里我介绍两种解决方法。方法1是参考Kienzl的简便方法,方法2是大部分教程采用的方法。

方法1:使用nscd

nscd是(e)glibc的“Name Service Caching Daemon”。除了处理gethostbyname()这样的函数外,也处理getpwnam()等需要访问/etc/passwd的函数。(e)glibc访问nscd的unix socket, /var/run/nscd/socket 来通过nscd获取这些内容,如果不能连接到nscd则转而自行进行解析。

也就是说,只要装好nscd,并且让chroot环境里的程序能够访问到socket连接上nscd,就可以把chroot环境内的解析请求转由chroot外顺利进行了。由于/var/run一般是tmpfs,硬链接无法跨文件系统使用,所以可以使用 mount -bind 来把/var/run/nscd目录mount到chroot环境中同样的位置去即可。

同样的道理,用 mount -bind 把/usr/share/zoneinfo目录mount到chroot环境里,配合在php-fpm的pool里设置date.timezone就可以非常直接而暴力的解决时区问题。

先执行 apt-get install nscd 安装nscd,然后为了能够让 mount -bind 自动执行,把下面的脚本存为 /etc/init.d/php-chroot

bash
#!/bin/sh
### BEGIN INIT INFO
# Provides:  php5-fpm-chroot-setup
# Required-Start: nscd
# Required-Stop:
# Default-Start:  2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Bind-mounts needed sockets and data into a php-fpm-chroot
### END INIT INFO
CHROOT=/var/www/chroot
DIRS="/var/run/nscd /usr/share/zoneinfo"
case "$1" in
 start)
 $0 stop 2>/dev/null
 for d in $DIRS; do
  mkdir -p "${CHROOT}${d}"
  mount --bind -o ro "${d}" "${CHROOT}${d}"
 done
 ;; 
 stop) 
 for d in $DIRS; do
  umount "${CHROOT}${d}"
 done
 ;; 
 *)
 echo "Usage: $N {start|stop}" >&2
 exit 1
 ;;
esac
exit 0

执行 update-rc.d php-chroot defaults 来让脚本在启动时执行。如果有多个chroot环境以及多个目录需要bind-mount,可以自行添加一个循环改写。

这个方法的好处是简单易行,不需要拷贝大量etc下的配置文件和库文件到chroot环境中。使用nscd在解决域名访问的问题过程中也顺道解决了/etc/passwd和/etc/group。但是bind-mount和nscd的安全性尚没有确切的说法,只能说so far so good。另外 mount -bind 会消耗一定的系统资源,有评论称大约一个mount 大概会消耗500k内存,所以对于大量的chroot环境(几百个)不见得适合。

方法2:拷贝/etc配置文件和库文件

这是最传统而常用的方法,也相对比较复杂。到底拷贝哪些配置、哪些库文件因发行版和软件版本而异,很难有定论也不好调试。而且一旦系统升级,对应的库文件也需要进行更新,工作量很大。我没有采用这个方法,但是简要的介绍一些比较靠谱的方法分享一下。

域名解析。需要拷贝/etc/resolv.conf,/etc/hosts,/etc/nsswitch.conf到chroot环境下的etc目录下。还需要拷贝一系列的库文件,主要是libnss_*.so,libresolv.so,libsoftokn3.so。具体libnss_*.so拷贝哪些,可以打开nsswitch.conf看列出了哪些。

时区配置。拷贝/etc/localtime,/usr/share/zoneinfo/zone.tab,和/usr/share/zoneinfo目录下所使用时区的文件。

其它常用配置。/etc/passwd和/etc/group有时也是需要的,但是内容 似乎可以伪造 ,至少可以选择性的填写不用完全拷贝主系统里的。

如果使用的时候仍然出现问题,可以使用strace来查看php进行了哪些调用使用了哪些库文件。先执行:

bash

ps aux | grep php | grep 'chroot' #chroot是php的pool名

查看pool的进程pid(可以在pool设置里先把子进程数目限制到1个方便调试)。然后执行:

bash
strace -p 进程pid -o chroot1.txt&  #有多个子进程就修改pid执行多次,输出改为chroot2/3.txt存到不同文件里
此时在页面里执行各种函数,然后查看输出文件里记录了哪些库文件,对应拷贝到chroot环境里即可。

这个方法很麻烦,尤其是第一次安装设置和后续系统更新时。当然身为运维人员写写shell脚本简化工作肯定是基本功了。这种方法没有额外的内存消耗,可以部署大量chroot环境,当然硬盘消耗会高一点,而且安全性也经历了长久的考验

3.2 修复mail()

如果是使用WordPress,也可以利用MailChimp等插件不使用系统自身的邮件服务。事实上因为垃圾邮件的标准日益严格,和VPS主机商的限制,我现在更倾向于干脆不在系统里部署邮件服务了,所以php的mail()函数算是被废掉了……当然如果需要的话也可以很简单的设置好的。

php的mail()函数是使用system()调用sendmail进行邮件发送操作,所以需要chroot环境里有能够调用的sendmail程序即可。常见的替代品是mini_sendmail,这里多介绍ssmtp,msmtp也类似。

前提:处理/bin/sh

system()调用产生的命令行是 /bin/sh -c command 。在chroot环境中调用外部程序必须存在/bin/sh,一个基本的shell。通常选择拷贝dash:

bash

#cp /bin/dash /var/www/chroot/bin/sh

注意运行 ldd /bin/dash 观察需要拷贝哪些库文件。我这里的回显是:

bash

ldd /bin/dash

    linux-vdso.so.1 =>  (0x00007fff779fe000)

    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f165620f000)

    /lib64/ld-linux-x86-64.so.2 (0x00007f16567fc000)

第一条那个只列了个文件名,=>后面也没有文件的基本上都是不用管的。剩下的库文件基本的原则是如果列出的是/lib64,就拷贝到chroot环境下的/lib64,如果列出的是/lib,虽然有很多发行版,大部分库文件包括libc.so是在/lib/x86_64-linux-gnu/目录下的,也直接拷贝到chroot环境的/lib目录下即可,是可以正常找到的。

但是!

前面那句 “必须存在/bin/sh,一个基本的shell” 其实并不是真的,对于mail()只要有一个能接受-c参数调用后面的命令的程序就可以了。所以Kienzl写了这样一个程序:

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAXARG 64
int main( int argc, char* const argv[] ) {
 char* args[ MAXARG ] = {};
 if( argc < 3 || strcmp( argv[1], "-c" ) != 0 ) {
 fprintf( stderr, "Usage: %s -c <cmd>\n", argv[0] ); 
 return 1;
 }
 {
 char* token;
 int i = 0; 
 char* argStr = strdup( argv[2] );
 while( ( token = strsep( &argStr, " " ) ) != NULL ) {
  if( token && strlen( token ) )
  args[ i++ ] = token;
  if( i >= MAXARG )
  return 2;
 }
 } 
 return execvp( args[0], args );
}

保存成sh.c执行: gcc sh.c -o sh -static 然后把sh拷贝到chroot环境的/bin目录下即可。

这样一个不完全的shell从一定程度上也算是增强了chroot环境的安全性了。

方法1:使用mini_sendmail

mini_sendmail似乎专为chroot环境而生。调用mini_sendmail后,它会转而访问本机的25端口,通过本机的邮件服务来发送邮件。所以如果主环境有安装postfix/exim4等邮件服务的话可以使用mini_sendmail来在chroot环境中发送邮件,这是最简单的方法。

mini_sendmail的安装很简单:

bash

wget http://www.acme.com/software/mini_sendmail/mini_sendmail-1.3.8.tar.gz

tar zxf mini_sendmail-1.3.8.tar.gz

cd mini_sendmail-1.3.8

make

cp mini_sendmail /var/www/chroot/usr/sbin/sendmail

最后一行自行修改chroot环境的目录。切记要拷贝到chroot环境的/usr/sbin目录下并且命名为sendmail。否则的话要在pool里自行设置ini参数的sendmail_path来指导php找到sendmail程序。

由于mini_sendmail默认就是静态链接,所以也无需拷贝其它的库文件了。

方法2:使用ssmtp/msmtp

对于本机没有安装邮件服务的情况,就不能使用mini_sendmail了。ssmtp和msmtp都支持把接收到的邮件发送请求转而通过其它SMTP服务器来发送。需要注意的是由于ssl支持需要更多更复杂的库文件和配置,所以不建议为两者编译ssl支持……下面以ssmtp为例介绍一下。

bash
wget ftp://ftp.debian.org/debian/pool/main/s/ssmtp/ssmtp_2.64.orig.tar.bz2
tar jxf ssmtp_2.64.orig.tar.bz2
cd ssmtp_2.64
./configure --prefix=/ #别忘了prefix
make     #千万别手抖make install
cp ssmtp /var/www/chroot/usr/sbin
mkdir -p /var/www/chroot/etc/ssmtp
cp ssmtp.conf revaliases /var/www/chroot/etc/ssmtp #配置文件
cd /var/www/chroot/usr/sbin
ln -s ssmtp sendmail

同样记得ldd然后把对应的库文件拷贝过去。另外ssmtp需要/etc/passwd和/etc/group,如果上面没有使用nscd需要拷贝/伪造这两个文件。

ssmtp需要配置。ssmtp.conf文件配置如下:

bash
root=admin@example.com  #其实这行好像可以乱写

mailhub=smtp.example.com  #smtp服务器地址
hostname=myexample.com  #此处的hostname似乎会用于产生默认的“root@myexample.com”形式的发件人地址
AuthUser=admin@example.com #此处使用真实的登录用户名
AuthPass=password   #密码
FromLineOverride=YES   #允许改写发件人

revaliases里配置每个用户在使用ssmtp时使用的“发件人”地址和smtp服务器地址。可以不配置,但是文件要有。具体格式是:

bash

# 本地用户名:发件人地址:smtp服务器[:端口(默认25)]

root:admin@example.com:smtp.example.com

www-data:noreply@example.com:smtp.example.com

可以使用chroot(指真正的chroot命令)做个测试:

bash

chroot /var/www/chroot /bin/sh             #此时/bin/sh一定要是真正的shell

echo "Subject: test"|sendmail -v username@server.com  #替换邮件地址为自己的

此时php的mail()函数应该就可用了。

4.其它问题

配置完chroot环境后记得将php的pool设置里display_error关闭。
MySQL的连接可能会遇到问题 ,因为如果填写localhost的话php会试图寻找MySQL的unix socket来访问mysqld。填写127.0.0.1通过TCP连接就没有问题了
完成后的目录结构,以我为例给大家参考一下:

/var/www/chroot/
├── bin
│ └── sh
├── dev
│ ├── null
│ ├── urandom
│ └── zero
├── etc
│ └── ssmtp
│ ├── revaliases
│ └── ssmtp.conf
├── lib
│ └── libc.so.6
├── lib64
│ └── ld-linux-x86-64.so.2
├── public
├── tmp
├── usr
│ ├── sbin
│ │ ├── sendmail -> ssmtp
| │ └── ssmtp
│ └── share
│ └── zoneinfo
│  ├── 大量时区的目录结构
│  └── zone.tab
└── var
 ├── lib
 │ └── php5
 │ └── sessions
 ├── run
 │ └── nscd
 │ ├── nscd.pid
 │ └── socket
 └── www
 └── chroot -> ../..

以上就是本文的全部内容,希望大家喜欢。

PHP 相关文章推荐
php 使用post,get的一种简洁方式
Apr 25 PHP
理解php Hash函数,增强密码安全
Feb 25 PHP
解析php如何将日志写进syslog
Jun 28 PHP
解析isset与is_null的区别
Aug 09 PHP
destoon网站转移服务器后搜索汉字出现乱码的解决方法
Jun 21 PHP
Laravel 5 框架入门(二)构建 Pages 的管理功能
Apr 09 PHP
smarty高级特性之过滤器的使用方法
Dec 25 PHP
PHP快速生成各种信息提示框的方法
Feb 03 PHP
php代码检查代理ip的有效性
Aug 19 PHP
php获取文章内容第一张图片的方法示例
Jul 03 PHP
YII分模块加载路由的实现方法
Oct 01 PHP
详解php命令注入攻击
Apr 06 PHP
织梦sitemap地图实时推送给百度的教程
Aug 03 #PHP
php生成图片验证码的实例讲解
Aug 03 #PHP
android上传图片到PHP的过程详解
Aug 03 #PHP
php将远程图片保存到本地服务器的实现代码
Aug 03 #PHP
php基于session实现数据库交互的类实例
Aug 03 #PHP
php通过排列组合实现1到9数字相加都等于20的方法
Aug 03 #PHP
PHP实现递归复制整个文件夹的类实例
Aug 03 #PHP
You might like
星际流派综述
2020/03/04 星际争霸
PHPUnit PHP测试框架安装方法
2011/03/23 PHP
PHP中让curl支持sock5的代码实例
2015/01/21 PHP
提交表单后 PHP获取提交内容的实现方法
2016/05/25 PHP
PHP实现批量删除(封装)
2017/04/28 PHP
laravel 解决Validator使用中出现的问题
2019/10/25 PHP
封装的原生javascript弹出层代码
2010/09/24 Javascript
JavaScript ECMA-262-3 深入解析.第三章.this
2011/09/28 Javascript
解释&amp;&amp;和||在javascript中的另类用法
2014/07/28 Javascript
详解ECMAScript typeof用法
2018/07/25 Javascript
Vue源码解析之数据响应系统的使用
2019/04/24 Javascript
[39:07]LGD vs VP 2018国际邀请赛淘汰赛BO3 第二场 8.21
2018/08/22 DOTA
python正则表达式修复网站文章字体不统一的解决方法
2013/02/21 Python
编写自定义的Django模板加载器的简单示例
2015/07/21 Python
Python设计模式之观察者模式简单示例
2018/01/10 Python
python简易远程控制单线程版
2018/06/20 Python
Python单元和文档测试实例详解
2019/04/11 Python
Jacobi迭代算法的Python实现详解
2019/06/29 Python
Python matplotlib以日期为x轴作图代码实例
2019/11/22 Python
Python 实现递归法解决迷宫问题的示例代码
2020/01/12 Python
Python随机数函数代码实例解析
2020/02/09 Python
从零开始的TensorFlow+VScode开发环境搭建的步骤(图文)
2020/08/31 Python
pytorch 中forward 的用法与解释说明
2021/02/26 Python
canvas三角函数模拟水波效果的示例代码
2018/07/03 HTML / CSS
戴森美国官网:Dyson美国
2016/09/11 全球购物
工作时间上网检讨书
2014/02/03 职场文书
数控专业毕业生自荐信范文
2014/03/04 职场文书
2014全国两会学习心得体会2000字
2014/03/10 职场文书
餐饮业员工工作决心书
2014/03/11 职场文书
爱护花草树木的标语
2014/06/11 职场文书
个人思想政治总结
2015/03/05 职场文书
2015年师德表现自我评价
2015/03/05 职场文书
2015年老干部工作总结
2015/04/23 职场文书
springboot 启动如何排除某些bean的注入
2021/08/02 Java/Android
4种方法python批量修改替换列表中元素
2022/04/07 Python
zabbix配置nginx监控的实现
2022/05/25 Servers