Chroot Apache2 vs dyrektywa open_basedir w PHP

Spis treści

Kilka dni temu wpadł mi w oko artykuł na temat wykonania chroot serwera Apache2. Problem z tamtym tekstem jest taki, że nie uwzględnia on serwera bazy danych MySQL. W efekcie, taki chroot'owany Apache2 będzie miał problemy z połączeniem się do bazy, a nasz serwis bez niej raczej nie będzie działał prawidłowo. Przydałoby się zatem dopracować nieco ten artykuł i wypracować takie rozwiązanie, które nie popsuje przy okazji naszego serwisu www. Dlatego też w tym wpisie wykonamy sobie chroot zarówno serwera Apache2 z obsługą PHP i bazy danych MySQL za sprawą modułu unixd .

Po co nam właściwie chroot

W konfiguracji Apache2 jest standardowo kilka dyrektyw Directory , które określają politykę dostępu do zasobów serwera. Domyślnie w pliku /etc/apache2/apache2.conf mamy min. ten poniższy wpis:

<Directory />
    Options FollowSymLinks
    AllowOverride None
    Require all denied
</Directory>

Dzięki niemu, odwiedzający nasz serwer użytkownicy, nie mają dostępu do katalogu / i do wszystkich jego podfolderów. Na niewiele by nam się taka konfiguracja zdała, dlatego też niżej w tym pliku jest kilka dodatkowych wpisów podobnych do tego powyższego. Jeden z nich zezwala na dostęp do katalogu /var/www/ i to z niego możemy serwować strony internetowe. Niemniej jednak, skrypty PHP mogą wyjść poza ten katalog i zwrócić zawartość dowolnego pliku, który mamy w systemie. Poniżej jest prosty przykład takiego skryptu:

<?php
    $var = file_get_contents('/etc/hosts');
    echo 'Plik /etc/hosts: <br />' . $var;
?>

Po uruchomieniu skryptu przez odwiedzenie tego pliku w przeglądarce, naszym oczom powinna się pokazać zawartość pliku /etc/hosts . Oczywiście w dalszym ciągu obowiązują restrykcje nałożone przez prawa dostępu do plików, przez co nie da rady, np. podejrzeć pliku /etc/shadow , przynajmniej nie w przypadku serwera działającego z uprawnieniami użytkownika/grupy www-data . Oczywiście zakładamy, że prawa tego pliku również nie zezwalają temu użytkownikowi i grupie na dostęp.

Chroot może nam pomóc w takiej sytuacji. W efekcie, wykonanie się tego powyższego skryptu nic nam nie zwróci, bo w środowisku chroot zwyczajnie nie będziemy mieli dostępu do plików systemowych.

Chroot za sprawą modułu unixd

Serwer Apache2 ma statycznie wkompilowany moduł unixd. Umożliwia on bardzo szybkie i proste zamknięcie serwera w środowisku chroot. Jedyne co musimy zrobić to określić katalog chroot'a. Robimy to w pliku /etc/apache2/apache2.conf przez dodanie poniższego wpisu:

ChrootDir /jail

Nazwę katalogu /jail/ możemy sobie dowolnie dostosować. Proponowałbym także zostawienie starej struktury folderów wewnątrz katalogu /jail/ . Przykładowo, jeśli w konfiguracji wirtualnego hosta wykorzystujemy katalog /var/www/strona/ , to wewnątrz folderu /jail/ utwórzmy dokładnie taką samą strukturę katalogów, a do podfolderu strona/ przenieśmy zawartość starego katalogu. W ten sposób nie będziemy musieli nic ruszać w konfiguracji samego Apache2 czy też jego wirtualnych hostów.

Dostosowanie środowiska chroot

Po przekopiowaniu zawartości naszej strony do środowiska chroot, czeka nas jeszcze drobne dostosowanie struktury katalogów wewnątrz folderu /jail/ . Chodzi o to, że gdybyśmy przykładowo odwiedzili katalog serwera, który umożliwia listing plików, to pierwsze co nam się rzuci w oczy to brak ikonek. Zamiast nich zostaną nam jedynie pokazane [ICO] , [DIR] czy [TXT] :

chroot-apache2-mysql-php-bledy

Te ikonki znajdują się w katalogu /usr/share/apache2/ i by je wyświetlić w środowisku chroot, musimy stworzyć taki katalog wewnątrz folderu /jail/ i przekopiować do niego odpowiednie pliki. Oczywiście jeśli katalog zezwala na podlinkowanie zawartości, to możemy również i z tej opcji skorzystać:

# mkdir -p /jail/usr/share/
# cp -a /usr/share/apache2/ /jail/usr/share/

W pliku /etc/apache2/apache2.conf dopisujemy jeszcze ten poniższy blok kodu:

<Directory /usr/share/apache2>
    AllowOverride None
    Require all granted
</Directory>

Ten powyższy przykład z ikonkami listingu strony zwracanej przez serwer Apache2 miał jedynie zobrazować problem z dostępem do zasobów wewnątrz środowiska chroot. Praktycznie każdy plik, którego wymaga nasz serwis czy też konfiguracja serwera www, będziemy musieli w ten sposób przenieść do katalogu /jail/ .

Blog WordPress wymaga, np. bazy stref czasowych, tj. katalogu /usr/share/zoneinfo/ oraz również katalogu /tmp/ z odpowiednimi uprawnieniami:

# cp -a /usr/share/zoneinfo/ /jail/usr/share/
# mkdir /jail/tmp/
# chmod 1777 /jail/tmp/

Skrypty PHP nie wymagają dodatkowego dopieszczenia i będą nam działać w takim środowisku chroot. Problem jednak pozostaje z bazą danych MySQL, która już taka miła dla nas nie będzie i trzeba poświęcić jej nieco więcej czasu.

Problem z bazą danych MySQL po zaimplementowaniu chroot

Zakładając, że nasz serwer www został poprawnie skonfigurowany pod kątem pracy w środowisku chroot, możemy teraz spróbować odwiedzić nasz blog. Z reguły taki serwis wymaga do pracy serwera bazy danych. Po przejściu pod adres www, przywita nas ten poniższy komunikat:

chroot-apache2-mysql-php-bledy

Serwerowi bazy danych nic nie dolega ale skrypty PHP w środowisku chroot nie potrafią go odnaleźć. W efekcie zwracają błąd połączenia: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock . By poprawić ten problem, musimy edytować dwa pliki.

Plik /etc/mysql/my.cnf :

...
[client]
...
socket          = /jail/var/run/mysqld/mysqld.sock
...
[mysqld_safe]
...
socket          = /jail/var/run/mysqld/mysqld.sock
...
[mysqld]
...
pid-file       = /jail/var/run/mysqld/mysqld.pid
socket         = /jail/var/run/mysqld/mysqld.sock
...

Plik /etc/mysql/debian.cnf :

...
[client]
...
socket   = /jail/var/run/mysqld/mysqld.sock
[mysql_upgrade]
...
socket   = /jail/var/run/mysqld/mysqld.sock
...

Tworzymy także katalog /jail/var/run/mysqld :

# mkdir -p /jail/var/run/mysqld/
# chown mysql:mysql /jail/var/run/mysqld/

I resetujemy serwer bazy danych MySQL. W tej chwili nasz serwis www powinien już uzyskać dostęp do bazy danych. Na wypadek błędów dobrze jest zajrzeć do katalogu /var/log/apache2/ lub też /var/log/mysql/ i przejrzeć znajdujące się w tych folderach logi systemowe.

Dyrektywa open_basedir w PHP

Jak widać, upchnięcie serwera www w środowisku chroot może być nieco problematyczne. Poza tym, niektórzy użytkownicy niezbyt przychylnie są nastawieni do tego typu rozwiązań argumentując swoje zdanie, tym, że "chroot i tak przed niczym nas nie ochroni". Pamiętajmy, że w tym przypadku, serwer Apache2 nie działa z uprawnieniami root, zatem może mieć on lekki problem z ucieczką poza katalog /jail/ . Z jedną rzeczą mogę się zgodzić, mianowicie taki chroot nie jest wygodnym rozwiązaniem.

Jako, że w artykule, którego link został przytoczony we wstępie, nacisk został położony na uniemożliwienie skryptom PHP odczytywania plików systemowych, to możemy skorzystać z nieco innego rozwiązania. Chodzi o parametr open_basedir, który jest do skonfigurowania w pliku /etc/php5/apache2/php.ini :

open_basedir = "/var/www/"

Gdy skrypt PHP spróbuje uzyskać dostęp do pliku/katalogu w innym miejscu systemu plików niż określono w dyrektywie open_basedir , np. za pomocą include lub fopen() , to taka próba zostanie udaremniona. Mamy mniej więcej ten sam efekt, co w przypadku środowiska chroot, z tym, że o wiele mniej kombinowania.

W przypadku, gdy na serwerze jest hostowanych kilka serwisów, to opcję open_basedir możemy określić osobno dla każdego wirtualnego hosta w konfiguracji serwera Apache2 wykorzystując dyrektywę php_admin_value. Poniżej przykład:

<VirtualHost *:80>
    ...
    php_admin_value open_basedir "/var/www/strona/"
    ...
</VirtualHost>
Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.