The point of no return

Chrooted PHP-FPM with Nginx on CentOS 6

11th July 2013 by Ali Erdinç Köroğlu

What is chroot (change root)? It’s about creating a virtualized environment in Linux operating system to separate it from the main operating system and directory structure. When you change root to another directory you can not access files and commands outside that directory. As you see, chroot enhances the security for the system and creates a virtual environment inside of which the application operates. If a vulnerability exist in the application or code such that an attacker can gain file system access, who would only be able to access files inside virtualized environment and the rest of the operating system and directory structure would remain inaccessable.

It’s good but chroot != security

Installation

Before installation, we should add Nginx repository to /etc/yum/repos.d. EPEL repository has Nginx too but the version is very old.

 nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

Installing..

yum install nginx php-common php-cli php-pdo php-mysql php-fpm

Configurations

/etc/php-fpm.d/example.conf
[example]
listen = /tmp/example.sock 
; Set listen(2) backlog. A value of '-1' means unlimited.
; Default Value: -1
;listen.backlog = -1
 
; Unix user/group of processes
; RPM: apache Choosed to be able to access some dir as httpd
user = nginx
; RPM: Keep a group allowed to write in log dir.
group = nginx
 
; Choose how the process manager will control the number of child processes.
pm = dynamic
pm.max_children = 50
 
; The number of child processes created on startup.
pm.start_servers = 5
 
; The desired minimum number of idle server processes.
pm.min_spare_servers = 5
 
; The desired maximum number of idle server processes.
pm.max_spare_servers = 35
 
; The log file for slow requests
slowlog = /var/log/php-fpm/example-slow.log
 
; Set open file descriptor rlimit.
; Default Value: system defined value
;rlimit_files = 1024
 
; Set max core size rlimit.
; Possible Values: 'unlimited' or an integer greater or equal to 0
;rlimit_core = 0
 
chroot = /chroot/jailchdir = / 
php_admin_value[disable_functions] = dir,chdir,opendir,readdirphp_admin_value[error_log] = /var/log/php-fpm/example-error.log
php_admin_flag[log_errors] = on
php_admin_value[error_reporting] = E_ALL & ~E_NOTICE
 
;php_admin_value[memory_limit] = 128M
 
; Set session path to a directory owned by process user
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session
/etc/nginx/conf.d/example.conf
server {
        server_name ae.koroglu.org;
        root /srv-www/example;        add_header "X-UA-Compatible" "IE=Edge,chrome=1";
        access_log /var/log/nginx/example.access.log main;
        error_log /var/log/nginx/example.error error;
        index index.php ;
 
        location / {
                try_files $uri $uri/ $uri.php;
        }
 
        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }
 
        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }
 
        location ~* \.(ico|css|js|gif|jpg|jpeg|png)(\?[0-9]+)?$ {
                expires max;
                log_not_found off;
                access_log off;
        }
 
        location ~ \.php$ {
                try_files $uri = 404;
                include /etc/nginx/fastcgi_params;
                fastcgi_pass unix:/tmp/example.sock;                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;        }
}

Chroot Debugging

Beginning was simple, a classic PHP configuration error..

Warning: date() [function.date]: It is not safe to rely on the system's timezone settings.
You are required to use the date.timezone setting or the date_default_timezone_set() function.
In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier.
/etc/php.ini
[Date]
date.timezone = Europe/Istanbul

PHP requires date.timezone information so I added “Europe/Istanbul” into php.ini and copied related files into chroot environment. Rest requires tracing.. 1st step is to find example pool php-fpm processes..

[root@8bitplus ~]# ps aux | grep php | grep example
nginx    11947  0.0  0.2 344656  4496 ?        S    13:07   0:00 php-fpm: pool example       
nginx    11949  0.0  0.2 344656  4496 ?        S    13:07   0:00 php-fpm: pool example       
nginx    11950  0.0  0.2 344656  4496 ?        S    13:07   0:00 php-fpm: pool example       
nginx    11951  0.0  0.2 344656  4496 ?        S    13:07   0:00 php-fpm: pool example       
nginx    11952  0.0  0.2 344656  4496 ?        S    13:07   0:00 php-fpm: pool example

The minimum number of child processes can not be less than 5 for any PHP-FPM pool by default so we should trace related pool process ids.

[root@8bitplus ~]# strace -p 11947 -o chroot1.txt&
[1] 12650
[root@8bitplus ~]# Process 11947 attached - interrupt to quit
 
[root@8bitplus ~]# strace -p 11949 -o chroot2.txt&
[2] 12651
[root@8bitplus ~]# Process 11949 attached - interrupt to quit
 
[root@8bitplus ~]# strace -p 11950 -o chroot3.txt&
[3] 12652
[root@8bitplus ~]# Process 11950 attached - interrupt to quit
 
[root@8bitplus ~]# strace -p 11951 -o chroot4.txt&
[4] 12653
[root@8bitplus ~]# Process 11951 attached - interrupt to quit
 
[root@8bitplus ~]# strace -p 11952 -o chroot5.txt&
[5] 12654
[root@8bitplus ~]# Process 11952 attached - interrupt to quit

I checked strace outputs it seems that /etc/resolv.conf, /etc/hosts, /etc/host.conf, /lib64/libnss_dns-2.12.so and /usr/lib64/libsoftokn3.so files needed for PHP-FPM chroot environment. Also you should know more about those files like where they come from, dependencies etc.. This will be very important while updating the system. If any upgrade operation updates those packages, your chroot may not be working due to broken shared library dependencies.

[root@8bitplus ~]# rpm -qf /lib64/libnss_dns-2.12.so /usr/lib64/libsoftokn3.so /usr/share/zoneinfo/Europe/Istanbul 
glibc-2.12-1.107.el6.x86_64nss-softokn-3.12.9-11.el6.x86_64tzdata-2013c-2.el6.noarch

Since PHP-FPM is not in the same chroot environment with Nginx, you should either make sure that both PHP-FPM and Nginx using the same chroot (Nginx should be chrooted too) or you should create soft link from Nginx document root folder to chrooted document root folder and that’s what I did..

[root@8bitplus srv-www]# pwd
/srv-www[root@8bitplus srv-www]# ls -l
total 0
lrwxrwxrwx 1 root root 34 Jul  2 18:30 example -> /chroot/jail/srv-www/example/

We could create multiple mysqld processes that listen for connections on different Unix socket files and TCP/IP ports but practical and easy way is to use TCP socket instead of dealing with mysqld_multi configuration.

/etc/my.cnf
#skip-networking
bind-address = 127.0.0.1

Also create urandom device for chroot

[root@8bitplus srv-www]# mknod -m 644 /chroot/jail/dev/urandom c 1 9

Chroot Structure

[root@8bitplus /]# tree chroot
chroot/
└── jail
    ├── dev
    │   └── urandom
    ├── etc
    │   ├── host.conf
    │   ├── hosts
    │   ├── localtime
    │   └── resolv.conf
    ├── lib64
    │   ├── libnss_dns-2.12.so
    │   └── libnss_dns.so.2 -> libnss_dns-2.12.so
    ├── srv-www
    │   └── example
    ├── usr
    │   ├── lib64
    │   │   └── libsoftokn3.so
    │   └── share
    │       └── zoneinfo
    │           ├── Europe
    │           │   └── Istanbul
    │           └── zone.tab
    └── var
        ├── lib
        │   └── php
        │       └── session
        └── log
            └── php-fpm

In this example you can use single chroot environment with multiple PHP-FPM pool. But if you’re using WordPress or any application which requires opendir function, I would strongly recommended that to create different chroot environment for each. You could test your chroot environment with PhpSpy Shell