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 相关文章推荐
一个目录遍历函数
Oct 09 PHP
php函数array_merge用法一例(合并同类数组)
Feb 03 PHP
php while循环得到循环次数
Oct 26 PHP
ThinkPHP模板中判断volist循环的最后一条记录的验证方法
Jul 01 PHP
去掉destoon资讯内容页keywords关键字自带的文章标题的方法
Aug 21 PHP
php中实现记住密码下次自动登录的例子
Nov 06 PHP
php使用pdo连接mssql server数据库实例
Dec 25 PHP
PHP获取一年有几周以及每周开始日期和结束日期
Aug 06 PHP
PHP的数组中提高元素查找与元素去重的效率的技巧解析
Mar 03 PHP
PHP简单日历实现方法
Jul 20 PHP
PHP使用函数用法详解
Sep 30 PHP
使用PHPWord生成word文档的方法详解
Jun 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
php学习之简单计算器实现代码
2011/06/09 PHP
PHP 之 写时复制介绍(Copy On Write)
2014/05/13 PHP
PHP常用编译参数中文说明
2014/09/27 PHP
Symfony2学习笔记之控制器用法详解
2016/03/17 PHP
jquery+thinkphp实现跨域抓取数据的方法
2016/10/15 PHP
php使用curl实现ftp文件下载功能
2017/05/16 PHP
PHP ADODB生成下拉列表框功能示例
2018/05/29 PHP
prototype class详解
2006/09/07 Javascript
详解Angular开发中的登陆与身份验证
2016/07/27 Javascript
轻松实现jquery选项卡切换效果
2016/10/10 Javascript
浅谈JS中String()与 .toString()的区别
2016/10/20 Javascript
js仿微博动态栏功能
2017/02/22 Javascript
jQuery简介_动力节点Java学院整理
2017/07/04 jQuery
vue 组件中slot插口的具体用法
2018/04/03 Javascript
通过nodejs 服务器读取HTML文件渲染到页面的方法
2018/05/17 NodeJs
CKEditor 4.4.1 添加代码高亮显示插件功能教程【使用官方推荐Code Snippet插件】
2019/06/14 Javascript
[05:02]2014DOTA2 TI中国区预选赛精彩TOPPLAY第三弹
2014/06/25 DOTA
python自动zip压缩目录的方法
2015/06/28 Python
python实现的按要求生成手机号功能示例
2019/10/08 Python
selenium中get_cookies()和add_cookie()的用法详解
2020/01/06 Python
Python requests获取网页常用方法解析
2020/02/20 Python
python Selenium 库的使用技巧
2020/10/16 Python
Ubuntu配置Pytorch on Graph (PoG)环境过程图解
2020/11/19 Python
python音频处理的示例详解
2020/12/23 Python
微软开源最强Python自动化神器Playwright(不用写一行代码)
2021/01/05 Python
Python 调用C++封装的进一步探索交流
2021/03/04 Python
小学生新年寄语
2014/04/03 职场文书
《新型玻璃》教学反思
2014/04/13 职场文书
职工擅自离岗检讨书
2014/09/23 职场文书
慰问信格式
2015/02/14 职场文书
幼儿教师师德师风自我评价
2015/03/05 职场文书
《作风建设永远在路上》心得体会
2016/01/21 职场文书
关于Vue Router的10条高级技巧总结
2021/05/06 Vue.js
react 路由Link配置详解
2021/11/11 Javascript
Redis中缓存穿透/击穿/雪崩问题和解决方法
2021/12/04 Redis
Vue Element plus使用方法梳理
2022/12/24 Vue.js