BLOGS

Apache + PHP-FPM + Chroot

Šajā rakstā pastāstīšu kā izveidot drošu mājaslapu hostingu, izmantojot chroot. Izveidot drošu hostingu ir īpaši svarīgi, ja uz servera ir paredzēts izvietot vairāk kā vienu mājaslapu. Galvenais šīs aizsardzības metodes mērķis ir nodalīt uz servera esošās lapas vienu no otras un neļaut uzbrucējam iegūt pilnu kontroli pār serveri. Lielāko drošības risku rada PHP, tāpēc mēģināsim to ierobežot ar chroot. Rakstā tiks izmantots CentOS 6 (64 bitu versija).

Kas ir chroot?

Chroot ir veids kā uz Linux operētājsistēmas izolēt programmu no pārējās sistēmas. Tas tiek panākts, nomainot saknes direktoriju (/) uz kādu citu direktoriju, piemēram, /chroot/manalapa, ko izolētā programma, turpmāk uzskatīs par šī servera saknes direktoriju. Norādot jaunu saknes direktoriju, programmai vairs nav pieejas citiem failiem, kas atrodas augstāk par mūsu norādīto direktoriju. Lai izolētā programma spētu pilnvērtīgi darboties, ir nepieciešams izveidot jaunu vidi, kurā būtu nepieciešamās bibliotēkas, konfigurācijas faili un citas svarīgas lietas. Šo jauno vidi sauc par chroot jail.

Apache

Kā webserveri izvēlējos Apache, jo internetā ir pieejama salīdzinoši maz informācija kā izveidot šādu chroot konfigurāciju un arī tāpēc, ka daudzi lietotāji vēl nav gatavi atteikties no Apache dēļ .htaccess faila un mod_rewrite. Ja ir iespējams, tad noteikti vajadzētu izmantot nginx webserveri, bet šoreiz koncentrēšos tieši uz Apache.

Lai uzstādītu Apache webserveri, nepieciešams izpildīt šādu komandu:

# yum install httpd
# chkconfig httpd on

Lai izveidotu mūsu chroot konfigurāciju, ir nepieciešams viens papildus Apache modulis (mod_fastcgi), kas nav pieejams no pakām, tāpēc sakompilēsim to paši.

Ja tas jau nav izdarīts, tad uzstādām Apache header failus un kompileri:

# yum install gcc make wget httpd-devel

Lejupielādējam, sakompilējam un ieinstalējam mod_fastcgi moduli:

# wget http://www.fastcgi.com/dist/mod_fastcgi-current.tar.gz
# tar -zxvf mod_fastcgi-current.tar.gz
# cd mod_fastcgi-2.4.6
# cp Makefile.AP2 Makefile
# make top_dir=/usr/lib64/httpd
# make install top_dir=/usr/lib64/httpd
# echo "LoadModule fastcgi_module modules/mod_fastcgi.so" > /etc/httpd/conf.d/mod_fastcgi.conf 
# echo "DirectoryIndex index.php" >> /etc/httpd/conf.d/mod_fastcgi.conf 
# service httpd restart

PHP

Ir vairāki veidi, kā darbināt PHP ar Apache:

  • mod_php darbojas kā Apache modulis un vienmēr ir ielādēts atmiņā, tas palīdz PHP skriptiem izpildīties bez aiztures, bet šāda pieeja patērē daudz operatīvo atmiņu un galvenā problēma ir, ka PHP visām lapām darbojas ar tām pašām privilēģijām, kas ir Apache. Ir mēģinājumi to ierobežot, bet tie visi vairāk vai mazāk ir bijuši nesekmīgi. Ir arī iespējamas problēmas, ja Apache grib darbināt izmantojot worker vai event, nevis prefork MPM.
  • CGI režīmā PHP ir atdalīts no Apache procesa un katrai lapai ir iespējams izpildīt PHP ar dažādām privilēģijām. Problēma ar šo metodi ir tāda, ka tā ir lēna, jo katram pieprasījumam tiek palaists jauns PHP process un PHP nav iespējas izmantot opcode cache kā APC vai eAccelerator.
  • FastCGI režīms ir vispiemērotākais, jo tas piedāvā visas labās CGI īpašības, kā arī PHP procesam ir iespējams palikt atmiņā, izpildīt skriptus bez aiztures, apstrādāt vairākus pieprasījumus pēc kārtas un izmantot opcode cache.

Cerams, ka visiem ir skaidrs, ka mūsu izvēle būs FastCGI.

Tā kā mūsu konfigurācijā Apache vairs nebūs nekādas teikšanas pār PHP, tad mums ir nepieciešams neatkarīgs serviss, kas rūpēsies par PHP procesu palaišanu. Par laimi, kopš PHP 5.3.3, ir pieejams PHP FastCGI Process Manager jeb PHP-FPM. Diemžēl konservatīvajos CentOS repozitorijos šis brīnums nav pieejams, tāpēc to vajadzēs iegūt no citiem repozitorijiem.

Pievienojam EPEL un REMI repozitorijus:

# rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-7.noarch.rpm
# rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

Un tagad ieinstalējam PHP-FPM no REMI repozitorija:

# yum --enablerepo=remi install php-fpm
# chkconfig php-fpm on

Chroot jail izveidošana

Šo procesu būs nepieciešams atkārtot katrai mājaslapai, kas atradīsies uz servera. To ir iespējams automatizēt, uzrakstot nelielu skriptu, bet, lai pārāk nesarežģītu visu, mēs to izdarīsim ar dažām vienkāršām komandām.

Izveidojam chroot jailu domēnam test.lv:

# mkdir -p /var/www/vhosts/test.lv
# cd /var/www/vhosts/test.lv
# mkdir -p {bin,dev,etc,htdocs,lib64,tmp/sessions,usr/share,var}
# mkdir -p var/lib/mysql
# mknod dev/null c 1 3
# mknod dev/zero c 1 5
# chmod -R 0777 tmp
# echo "root:*:0:0:Root:/:/bin/false" > etc/passwd
# grep -E '^apache:' /etc/passwd >> etc/passwd
# grep -E '^root:|^apache:' /etc/group > etc/group
# cp /bin/false bin
# cp /etc/{hosts,resolv.conf} etc
# cp -a /lib64/libnss_dns* lib64
# cp -r /usr/share/zoneinfo usr/share/zoneinfo

Apache vhost

Ieslēdzam iespēju turēt vairākus domēnus uz vienas IP adreses:

# echo "NameVirtualHost *:80" >> /etc/httpd/conf/httpd.conf

Izveidojam Apache vhostu domēnam test.lv. Šeit mēs pastāstīsim Apache, ka vēlamies, lai visi .php faili tiktu apstrādāti ar PHP-FPM.

Saglabājam šo failā /etc/httpd/conf.d/vhost_test.lv.conf:

<VirtualHost *:80>
    ServerName test.lv
    ServerAlias www.test.lv
    ErrorLog logs/test.lv-error_log
    CustomLog logs/test.lv-access_log common

    DocumentRoot /var/www/vhosts/test.lv/htdocs

    # Liekam visus .php failus padot uz PHP-FPM
    Alias /php-external.fcgi /test.lv.fcgi
    FastCGIExternalServer /test.lv.fcgi -socket /tmp/php-test.lv.sock
    AddType application/x-httpd-php .php
    Action application/x-httpd-php /php-external.fcgi

    # Aizliedzam pieeju no ārpuses
    <Location /php-external.fcgi>
        Order Deny,Allow
        Deny from All
        Allow from env=REDIRECT_STATUS
    </Location>

</VirtualHost>

Restartējam Apache:

# service httpd restart

PHP-FPM pool

Tagad ir pienācis laiks izveidot jaunu PHP-FPM pool domēnam test.lv. Šī ir ļoti atbildīga vieta, jo šeit mēs norādīsim, ka vēlamies, lai PHP izmantotu chroot un norādīsim, kur atrodas mūsu chroot jail.

Saglabājam šo failā /etc/php-fpm.d/test.lv.conf:

[test.lv]

listen = /tmp/php-test.lv.sock

user = apache
group = apache

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500

chroot = /var/www/vhosts/test.lv

php_admin_value[doc_root] = /htdocs
php_admin_value[open_basedir] = /htdocs/:/tmp/:/usr/php/
php_admin_value[cgi.fix_pathinfo] = 0
php_admin_value[session.save_path] = /tmp/sessions
php_value[date.timezone] = Europe/Riga

Ja interesē ko katrs parametrs nozīmē, tad to var izlasīt šeit.

Restartējam PHP-FPM:

# service php-fpm restart

E-pastu sūtīšana ar mail() funkciju

Šo soli var izlaist, ja neviena mājaslapa neizmantos iebūvēto PHP mail() funkciju, bet izmantos kādu PHP klasi, kas ļauj sūtīt e-pastus uzreiz caur SMTP.

Lai atdzīvinātu mail() funkciju, mēs varētu iekopēt chroot jailā sendmail un visas nepieciešamās bibliotēkas, bet tas rada pārāk daudz darba. Mēs gribam saglabāt chroot jail pēc iespējas mazāku, jo katra papildus bibliotēka palielina risku, ka uzbrucējs varēs izlausties no mūsu chroot jail.

Mēs izmantosim mazu programmu, kas darbosies kā starpnieks starp PHP un mūsu lokālo SMTP serveri. Šo programmu sauc mini_sendmail. Sakompilējam to statiski un iekopējam domēna test.lv chroot jailā.

# yum install glibc-static
# cd ~
# wget http://www.acme.com/software/mini_sendmail/mini_sendmail-1.3.6.tar.gz
# tar -zxvf mini_sendmail-1.3.6.tar.gz
# cd mini_sendmail-1.3.6
# sed -i 's/getlogin()/"apache"/g' mini_sendmail.c
# make
# mkdir -p /var/www/vhosts/test.lv/usr/sbin
# cp mini_sendmail /var/www/vhosts/test.lv/usr/sbin/sendmail

Papildinām chroot jail:

# cd /var/www/vhosts/test.lv
# cp /bin/dash bin/sh
# cp -a /lib64/ld-* lib64
# cp -a /lib64/libc.* lib64
# cp -a /lib64/libc-* lib64

Lai viss strādātu, uz servera jābūt uzinstalētam un palaistam SMTP serverim. To var izdarīt šādi:

# yum --enablerepo=remi install postfix
# service postfix start

MySQL

Tā kā PHP darbojas chroot jailā un tam vairs nav pieejas pie MySQL socket, tad PHP var rasties zināmas problēmas pieslēgties pie datubāzes. Tam ir divi risinājumi: slēgties pie datubāzes caur TCP/IP vai arī izveidot hardlinku uz MySQL socketu.

Pirmais risinājums ir ļoti vienkāršs, jo tikai jānomaina serveris pie kura slēgties no localhost uz 127.0.0.1 un sockets vairs nebūs nepieciešams.

Otrs risinājums ir izveidot hardlinku no chroota uz īsto socketu. Domēnam test.lv to var izdarīt šādi:

# ln /var/lib/mysql/mysql.sock /var/www/vhosts/test.lv/var/lib/mysql/mysql.sock

Problēma ar šo risinājumu ir tāda, ka hardlinks ir jāatjauno katru reizi, kad tiek restartēts serveris vai MySQL, pretējā gadījumā PHP nevarēs pieslēgties datubāzei. Lai šo problēmu atrisinātu, nepieciešams papildināt MySQL init.d skriptu ar komandu, kas atjaunos hardlinkus visiem chroot jailiem. Lai nesarežģītu visu vēl vairāk, šoreiz neiedziļināsimies init.d skriptu labošanā.

DOCUMENT_ROOT un SCRIPT_FILENAME problēma

Šobrīd izmantojot PHP-FPM chroot nākas saskarties ar nelielu nepilnību, ka dažādi $_SERVER masīva elementi satur pilnos ceļus pirms PHP tika nomainīta saknes direktorija. Piemēram, $_SERVER['DOCUMENT_ROOT'] ir nevis "/htdocs", bet "/var/www/vhosts/test.lv/htdocs".

Kamēr PHP izstrādātāji to nav salabojuši, var izmantot auto_prepend_file parametru PHP-FPM pool konfigurācijas failā. Izmantojot šo metodi, mēs varam padot savu .php skripu, kas izpildīsies pirms katra pieprasījuma. Šajā skriptā mēs varam salabot mums nepieciešamos ceļus. Skriptam ir jāatrodas konkrētā domēna chroot jailā.

Pievienot konfigurācijas parametru var šādi:

php_admin_value[auto_prepend_file] = /usr/php/fix_env.php

Skripta paraugs:

<?php

preg_match('/\/var\/www\/vhosts\/.*\/(.*)/', $_SERVER['DOCUMENT_ROOT'], $dr);

$_SERVER['DOCUMENT_ROOT'] = '/'.$dr[1];
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].$_SERVER['REDIRECT_URL'];
$_SERVER['PATH_TRANSLATED'] = $_SERVER['SCRIPT_FILENAME'];
$_SERVER['SCRIPT_NAME'] = $_SERVER['REDIRECT_URL'];

?>

Viss gatavs

Lai sāktu lietot jauno chroot hostingu, atliek vien iekopēt savus .php failus attiecīgajā direktorijā. Mūsu piemērā ar domēnu test.lv tas būtu šeit - "/var/www/vhosts/test.lv/htdocs".

Ja tomēr kaut kas nav skaidrs ar šo konfigurāciju, tad vari mēģināt mani uzmeklēt twitter - @ricardse un @noderack.