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.