創建PHP-FPM的Chroot執行環境

原文地址:創建PHP-FPM的Chroot執行環境php

php-fpm中能夠設立chroot,起到很是好的隔離效果,加強系統安全性。可是創建一個合理可用的php-fpm chroot環境則有些難度,比起可以利用debootstrap等工具進行的創建完整的chroot環境還要麻煩一點。網上有一部分教程,但大多比較雜亂或者老舊,對步驟也缺少說明。這裏參考不少資料把php-fpm的chroot創建從新梳理一遍。html

本文以Ubuntu 14.04.2爲例,php-fpm使用的是ppa:ondrej/php5-5.6提供的PHP5.6版本,跟系統自帶以及Debian系統的php-fpm和系統目錄結構應該是一致的。CentOS請自行調整。前端

php-fpm的chroot環境配置和所使用的服務器前端沒有關聯,也不強求Apache/Nginx進行chroot。固然那樣更安全——也更復雜。mysql

1.創建目錄結構

chroot的目錄選擇爲/var/www/chroot,其中頁面文件放置在/var/www/chroot/publiclinux

執行下面的命令創建基本的目錄結構:nginx

bashmkdir -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

下面是此時目錄結構,以後還會添加一些新的東西:sql

/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")的問題。shell

以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固然是不存在的。bootstrap

因此使用一個軟鏈接把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配置文件和庫文件

這是最傳統而經常使用的方法,也相對比較複雜。到底拷貝哪些配置、哪些庫文件因發行版和軟件版本而異,很難有定論也很差調試。並且一旦系統升級,對應的庫文件也須要進行更新,工做量很大。我沒有采用這個方法,可是簡要的介紹一些比較靠譜的方法分享一下。

  1. 域名解析。須要拷貝/etc/resolv.conf,/etc/hosts,/etc/nsswitch.conf到chroot環境下的etc目錄下。還須要拷貝一系列的庫文件,主要是libnss_*.so,libresolv.so,libsoftokn3.so。具體libnss_*.so拷貝哪些,能夠打開nsswitch.conf看列出了哪些。

  2. 時區配置。拷貝/etc/localtime,/usr/share/zoneinfo/zone.tab,和/usr/share/zoneinfo目錄下所使用時區的文件。

  3. 其它經常使用配置。/etc/passwd和/etc/group有時也是須要的,可是內容彷佛能夠僞造,至少能夠選擇性的填寫不用徹底拷貝主系統裏的。

若是使用的時候仍然出現問題,可使用strace來查看php進行了哪些調用使用了哪些庫文件。先執行:

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

查看pool的進程pid(能夠在pool設置裏先把子進程數目限制到1個方便調試)。而後執行:

bashstrace -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觀察須要拷貝哪些庫文件。我這裏的回顯是:

bashldd /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的安裝很簡單:

bashwget 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爲例介紹一下。

bashwget 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文件配置以下:

bashroot=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命令)作個測試:

bashchroot /var/www/chroot /bin/sh             #此時/bin/sh必定要是真正的shell
echo "Subject: test"|sendmail -v username@server.com  #替換郵件地址爲本身的

此時php的mail()函數應該就可用了。

4.其它問題

  1. 配置完chroot環境後記得將php的pool設置裏display_error關閉。
  2. MySQL的鏈接可能會遇到問題,由於若是填寫localhost的話php會試圖尋找MySQL的unix socket來訪問mysqld。填寫127.0.0.1經過TCP鏈接就沒有問題了
  3. 完成後的目錄結構,以我爲例給你們參考一下:
/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 -> ../..

參考資料

相關文章
相關標籤/搜索