Postanowiłem na własne potrzeby stworzyć pewien szablon konfiguracji PHP opartej o Apache. Podstawowym celem było bezpieczeństwo rozwiązania, a jak udało mi się to zrealizować – życie pokaże. W prezentowanych przykładach pominąłem większość parametrów konfiguracyjnych, a więc podczas tworzenia własnego środowiska na bazie niniejszego artykuły zalecam dostosowanie ich do własnych potrzeb.

Na potrzeby niniejszej konfiguracji użyłem:

  • Linux Slackware w wersji 12. Starałem się nie używać ustawień i pakietów pochodzących z dystrybucji, żeby rozwiązanie było jak najbardziej uniwersalne
  • Apache w wersji 2.2.6
  • FastCGI w wersji 2.4.6
  • PHP w wersji 4.4.7 i 5.2.5

Przyjąłem również kilka założeń:

  • Skrypty PHP powinny być uruchamiane w zamkniętym środowisku (np: chroot() albo open_basedir())
  • Skrypty PHP powinny być uruchamiane z uprawnieniami właściciela pliku/virtualhosta
  • Każdy użytkownik powinien mieć indywidualny folder tmp i folder przechowywania plików sesji
  • Każdy użytkownik sam może wybrać wersji PHP
  • Dla każdego virtualhosta powinien istnieć oddzielny plik php.ini
  • Tworzone rozwiązanie powinno być jak najbardziej uniwersalne. Jednocześnie powinno oferować administratorowi wygodę zarządzania

Instalacja Apache:

Apache wraz z modułem suexec potrafi uruchamiać skrypty CGI z uprawnieniami użytkownika. Model bezpieczeństwa suexec wymaga deklarowania zmiennych środowiskowych, które mają być przekazywane do CGI. Większość zmiennych środowiskowych np.: REQUEST_URL, DOCUMENT_ROOT itp. są wpisane na stałe do pliku support/suexec.c, jednak brakuje tam zmiennych dla PHP, które pomagają mu współpracować z modułem FastCGI. Musimy je ręcznie dopisać, a więc otwieramy plik support/suexec.c w ulubionym edytorze tekstowym i znajdujemy kod:

static const char *const safe_env_lst[] =
{
/* variable name starts with */
"HTTP_",
"SSL_",

/* variable name is */
"AUTH_TYPE=",
"CONTENT_LENGTH=",
"CONTENT_TYPE=",
"DATE_GMT=",
"DATE_LOCAL=",
"DOCUMENT_NAME=",
"DOCUMENT_PATH_INFO=",
"DOCUMENT_ROOT=",

Dopisujemy przed linijką zawierającą AUTH_TYPE:

"PHP_FCGI_MAX_REQUEST=",
"PHP_FCGI_CHILDREN=",

Plik wynikowy powinien wyglądać następująco:

static const char *const safe_env_lst[] =
{
/* variable name starts with */
"HTTP_",
"SSL_",

/* variable name is */
"PHP_FCGI_MAX_REQUEST=",
"PHP_FCGI_CHILDREN=",
"AUTH_TYPE=",
"CONTENT_LENGTH=",
"CONTENT_TYPE=",
"DATE_GMT=",
"DATE_LOCAL=",
"DOCUMENT_NAME=",
"DOCUMENT_PATH_INFO=",
"DOCUMENT_ROOT=",

Dzięki wykonanej modyfikacji suexec będzie przekazywał zmienne środowiskowe, które ustawi FastCGI, do wrappera PHP (w dalszej części artykuły opisze obie zmienne i dlaczego trzeba było wprowadzić tą poprawkę).

Teraz możemy przystąpić do rzeczywistej konfiguracji Apache. Uruchamiamy komendę:

./configure --enable-so  --with-suexec-caller=www --with-suexec-docroot=/var/www --with-suexec-logfile=/var/log/suexec --enable-suexec=shared --enable-cgi=shared --with-suexec-bin=/usr/local/apache2/sbin/suexec

Opis parametrów:

  • –enable-so: włączenie mechanizmu DSO
  • –with-suexec-caller: użytkownik z którego będzie wywoływany suexec. Należy tutaj wpisać nazwę użytkownika na którym działa Apache
  • –with-suexec-docroot: ścieżka w której znajdują się wszystkie skrypty PHP uruchamiane przez suexec
  • –with-suexec-logfile: plik do którego suexec będzie logował. Należy pamiętać o utworzeniu takiego pliku i nadaniu mu uprawnień do zapisu dla użytkownika na którym działa Apache. Należy mieć też na uwadze konieczność rotowania tego pliku – Apache się tym nie zajmuje
  • –enable-suexec: włączenie mechanizmu suexec
  • –enable-cgi:włącza obsługę CGI – niezbędne do konfiguracji PHP jako CGI
  • –with-suexec-bin: ścieżka do pliku suexec w naszym systemie. Plik ten może znajdować się w dowolnym miejscu

Jeżeli nie dodałeś żadnego layoutu ustawień (–enable-layout=LAYOUT) podczas konfigurowania Apache, pliki zostaną umieszczone w katalogu /usr/local/apache2. Jeżeli zmieniłeś layout, pamiętaj o zmianie ścieżki do pliku suexec (–with-suexec-bin).

make
make install

Instalacja FastCGI:

Konfiguracja FastCGI sprowadza się do wykonywania instrukcji instalacyjnych z pliku INSTALL.AP2. Należy pamiętać o zmianie ścieżki top_dir w przypadku innej konfiguracji Apache. We wskazanym katalogu powinien się znaleźć plik build/special.mk:

cp Makefile.AP2 Makefile
make top_dir=/usr/local/apache2
make install

Instalacja PHP:

Instalacja obu wersji PHP przebiega podobnie dlatego opiszę tylko jedną wersję w tym miejscu. W przypadku PHP 4 zalecał bym tylko zmianę prefixu na /usr/local/php4. Dalsza część artykułu zawiera konfiguracje dla systemu z obiema wersjami PHP.

./configure --prefix=/usr/local/php5 --enable-force-cgi-redirect --enable-fastcgi

Celowo nie dodawałem tutaj parametru –sysconfdir, który wskazuje na miejsce szukania domyślnego pliku php.ini. Będę używał parametru -c do wskazania pliku php.ini. Dowiedz się więcej o kolejności ładowania plików php.ini z mojego artykułu.

Opis parametrów:

  • –prefix: prefix instalacji ustawiłem na /usr/local/php5 gdyż równolegle będziemy instalować php4 i dobrze by było gdyby dwie wersji nie wchodziły sobie w drogę
  • –enable-force-cgi-redirect: parametr ten zabezpiecza przed wywołaniem skryptu PHP poprzez link bezpośredni do skryptu FastCGI w folderze fcgi. Skrypt PHP zostanie wykonany tylko i wyłącznie jeżeli odwołanie do parsera zostało wykonane przez Apache
  • –enable-fastcgi: włączenie obsługi FastCGI w parserze PHP

Zanim wykonasz poniższe komendy utwórz katalog /usr/local/php5.

make
make install

W tym momencie zakończyliśmy instalacje i podstawową konfigurację niezbędnego programowania. Przystąpmy zatem do dalszej konfiguracji.

Konfiguracja Apache:

Plik konfiguracyjny Apache (httpd.conf) powinien zawierać między innymi takie ustawienia:

LoadModule suexec_module modules/mod_suexec.so
LoadModule cgi_module modules/mod_cgi.so
LoadModule fastcgi_module modules/mod_fastcgi.so

<IfModule mod_fastcgi.c>
FastCgiWrapper /usr/sbin/suexec
FastCgiConfig \
-initial-env PHP_FCGI_CHILDREN=1 \
-initial-env PHP_FCGI_MAX_REQUESTS=5000 \
-singleThreshold 90 \
-killInterval 300 \
-autoUpdate \
-idle-timeout 240 \
-pass-header HTTP_AUTHORIZATION
</IfModule>

Opis parametrów:

  • FastCgiWrapper: w tym miejscu deklarujemy wrapper dla FastCGI. Zamiast suexec możemy użyć np. cgiwrap albo sbox. Jeżeli chcemy użyć tego samego wrapper co Apache możemy ustawić tą opcję na „On”
  • FastCgiConfig: deklarujemy w jaki sposób ma się zachowywać FastCGI
  • -initial-env PHP_FCGI_CHILDREN=1: Ustawia zmienną środowiskową PHP_FCGI_CHILDREN która poprzez wrapper (np.: suexec) przekazujemy do PHP. Jeżeli nie zmodyfikowali byśmy źródeł suexec , zmienna ta została by wycięta. PHP_FCGI_CHILDREN określa ile procesów potomnych ma obsługiwać żądania PHP dla jednego virtualhosta
  • -initial-env PHP_FCGI_MAX_REQUEST=5000: Zmienna ta określa maksymalną ilość żądań po których dany proces zostanie zakończony i uruchomiony w razie potrzeby od nowa
  • -singleThreshold 90: Parametr przyjmuje wartości liczbowe od 0 do 100. FastCGI na tej podstawie określa czy dany proces ma być utrzymywany (wartość bliższa 1) czy zabity (bliżej 100) jeżeli nie jest już wykorzystywany. Jeżeli zależy Ci się na oszczędności pamięci ustaw ten parametr bliżej 100. Ustawienie na 0 zapobiegnie wyłączaniu ostatniego procesu potomnego
  • -killInterval 300: Wartość parametru w sekundach, określa po jakim czasie bezczynności proces ma zostać zabity
  • -autoUpdate: Powoduje sprawdzanie daty skryptów FastCGI. Jeżeli któryś z nich zostanie zaktualizowany, FastCGI zabije proces i uruchomi go od nowa
  • -idle-timeout 240: Czas w sekundach który ma skrypt PHP na uruchomienie i rozpoczęcie zwracania wyników użytkownikowi
  • -pass-header HTTP_AUTHORIZATION: Przekazania nagłówków uwierzytelniania HTTP do CGI. Standardowo nagłówki te nie są przekazywane

Przyjąłem, że każdy virtualhost posiada swój katalog w /var/www. Struktura prezentuje się następująco:
/var/www/host1.example.com
/var/www/host1.example.com/htdocs – główny folder dostępny przez http
/var/www/host1.example.com/htdocs/fcgi – folder ze skryptami FastCGI
/var/www/host1.example.com/tmp – folder przeznaczony na pliki sesji i pliki tymczasowe

Konfiguracja virtualhosta:

<VirtualHost *:80>
SuexecUserGroup v1 v1

<Location /fcgi/php4>
SetHandler fastcgi-script
Options +ExecCGI
</Location>

<Location /fcgi/php5>
SetHandler fastcgi-script
Options +ExecCGI
</Location>

<Directory /var/www/host1.example.com/htdocs/fcgi >
AllowOverride None
Options None
Order allow,deny
Allow from all
</Directory>

AddHandler php5-fastcgi .php
Action php5-fastcgi /fcgi/php5
Action php4-fastcgi /fcgi/php4

AddType application/x-httpd-php .php

DocumentRoot /var/www/host1.example.com /htdocs/
ServerName host1.example.com
ErrorLog /var/log/httpd/host1.example.com-error_log
CustomLog /var/log/httpd/host1.example.com-access_log common
</VirtualHost>

Opis parametrów:

  • SuexecUserGroup v1 v1: określa systemowego użytkownika i grupę na którą suexec ma przenieść uprawnienia
  • <Location /fcgi/php5>…</Location>: Zarejestrowanie pliku /fcgi/php5 jako skryptu FastCGI
  • <Location /fcgi/php4>…</Location>: Jw. tylko, że dla pliku /fcgi/php4
  • <Directory>…</Directory>: Ustawienia katalogu fcgi. Należy tutaj szczególną uwagę zwrócić na „Options None”. Zabezpiecza to przed możliwością uruchamiania programów niż zadeklarowane w <Location …>. Dodatkowo opcja „AllowOverride None” zabezpiecza przed możliwością zmiany tych ustawień z poziomu pliku .htaccess
  • AddHandler php5-fastcgi .php: Ustawia domyślny interpretator dla plików .php na PHP 5. Gdyby użytkownik chciał zmienić wersje PHP na inna wystarczy, że sam we własnym zakresie stworzy plik .htaccess i wpisze do niego AddHandler php4-fastcgi .php
  • Action php5-fastcgi /fcgi/php5: Deklaracja skryptu FastCGI dla PHP 5
  • Action php4-fastcgi /fcgi/php4: Deklaracja skryptu FastCGI na PHP 5

Trzy opisane na końcu parametry można umieścić poza sekcją <VirtualHost>…</VirtualHost> czyniąc z nich ustawienia globalne dla serwera.

Teraz utworzymy skrypty FastCGI zadeklarowane w konfiguracji Apache. Skrypty te musimy utworzyć dla każdego virtualhosta w katalogu htdocs/fcgi.
Dla PHP 5 tworzymy plik htdocs/fcgi/php5:

#!/bin/bash
exec /usr/local/php5/bin/php-cgi -c php5.ini

Dla PHP tworzymy plik htdocs/fcgi/php4:

#!/bin/bash
exec /usr/local/php4/bin/php -c php4.ini

Niektórzy zadadzą sobie teraz pytanie „Po co dodawać każdemu katalog fcgi razem ze skryptami skoro mogę zrobić jeden katalog i podlinkować go każdemu?”.

Odpowiedź leży w źródłach suexec, które edytowaliśmy na początku. Model bezpieczeństwa suexec jest bardzo restrykcyjny i dokładnie porównuje uprawnienia uruchamianych programów razem z folderami virtualhosta. Jeżeli chodź jeden z testów nie będzie zgodny, skrypt nie zostanie uruchomiony. Rozwiązaniem jest modyfikacja źródeł suexec albo rozwiązanie problemu poprzez kopiowanie skryptów FastCGI do każdego virtualhosta. Pamiętaj o zmianie właściciela plików php4 i php5.

W tym miejscu powinienem powrócić jeszcze do modyfikacji suexec, którą wykonaliśmy na początku. Nic nie stało na przeszkodzie żebyśmy dopisali do utworzonych przed chwilą skryptów export zmiennych globalnych np.:

#!/bin/bash
export PHP_FCGI_CHILDREN 1
export PHP_FCGI_MAX_REQUESTS 5000
exec /usr/local/php5/bin/php-cgi -c php5.ini

Modyfikację tą wprowadzaliśmy jednak, żeby istniała możliwość ustawienia tych parametrów globalnie. Jeżeli jeden z virtualhostow wymaga innej konfiguracji to możemy zmodyfikować te wartości właśnie poprzez export jak na ww. przykładzie.

Pliki konfiguracyjne php.ini:

Żeby cała ta konfiguracja zdała egzamin musimy w przygotować indywidualne pliki php.ini dla każdego virtualhosta. W przypadku PHP 5 sprawa jest odrobinę ułatwiona. Temat ten opisałem w artykule „Uproszczenie w konfiguracji PHP 5 per virtualhost„.W pliku konfiguracyjnym obu wersji PHP powinny się znaleźć:

open_basedir = "/var/www/host1.example.com:/usr/local/php5"
disable_functions = proc_open, popen, disk_free_space, diskfreespace, leak, exec, system, shell_exec, passthru, dl
expose_php = Off
log_errors = On
error_log = "/var/www/host1.example.com /php_error"
register_globals = Off
enable_dl = Off
upload_tmp_dir = "/var/www/host1.example.com /tmp"
sendmail_path = "/usr/sibn/sendmail -t -i -f info@host1.example.com"
session.save_path = "/var/www/host1.example.com /tmp"

Finalizacja:

Na koniec pozostaje nam kilka problemów do rozwiązania, a mianowicie:

  • użytkownik może zmodyfikować zawartość skryptów znajdujących się w htdocs/fcgi, a co za tym idzie, dajemy mu dostęp do powłoki systemu, czego z pewnością większość z Was wolała by uniknąć
  • użytkownik może zmienić/skasować plik php.ini. Ma wtedy dostęp do modyfikacji spersonalizowanych wartości open_basedir, error_log, session.save_path, disable_functions itp.

Żeby rozwiązać te problemy, należało by zabrać użytkownikowi możliwość edycji plików w katalogu htdocs/fcgi. Gdy zabierzemy mu uprawnienia poprzez zmianę właściciela, suexec bardzo szybko przypomni nam o tym fakcie. Rozwiązanie jakie znalazłem niestety nie jest eleganckie, ale za to jest skuteczne.

Użytkownicy systemu plików ext2/3 mogą nadać atrybut +i dla katalogu htdocs/fcgi poprzez chattr +i fcgi. Uniemożliwi to wprowadzanie zmian na folderze fcgi (razem z jego zawartością) dla wszystkich użytkowników systemu. Tak założone atrybut +i zdejmuje tylko i wyłącznie root.

Change log artykułu:

Będę się starał uaktualniać ten artykuł, a więc w tym miejscu opisze przyszłe zmiany.
25/02/2008 19:38 – Usunięcie niepoprawnych spacji z konfiguracji VirtualHost.
04/02/2008 17:41 – Zmieniłem nazwę „apache2.conf” na „httpd.conf” celem poprawienia zgodności z domyślną konfiguracją Apache.
30/01/2008 20:28 – Zmiana <IfModule> na </IfModule> w konfiguracji Apache.
25/01/2008 12:26 – Poprawka związana z błędną nazwa foldera w przykładowej konfiguracji Apache. Zamienione „fcgi-bin” na „fcgi”.
30/12/2007 16:45 – Publikacja pierwszej wersji

16 odpowiedzi na “Konfiguracja Apache + FastCGI + SuExec + PHP 4/5 + kilka innych rozwiązań”
  1. jackpot pisze:

    Bardzo ciekawy artykul, podobna konfiguracje udalo mi sie wykonac na lighttpd. Przeczytalem Twoj pomysl na apache i chyba mniej/wiecej wszystko rozumiem ale…. :-)

    Action php5-fastcgi /fcgi-bin/php5
    Action php4-fastcgi /fcgi-bin/php4

    a potem piszesz, na temat wrappera php:
    „tworzymy plik htdocs/fcgi/php5 (…)
    tworzymy plik htdocs/fcgi/php4 (…)”

    _Chyba_ wkradla sie mala niescislosc co do sciezek, albo brakuje wpisu „Alias” w apache.
    Wszedzie odwolujesz sie do katalogu „fcgi” a w „Action” ustawiasz katalog „fcgi-bin”

    z gory dzieki za pare slow komentarza :)

    pozdrawiam
    Jacek

  2. Mariusz Dalewski pisze:

    @jackpot: Oczywiście masz racje. Dzięki. Nawet opis konfiguracji odnosił się do /fcgi/.

    Poprawione. Zmiana opisana w changelogu.

  3. kinek pisze:

    Dobry artykuł, ale mam pytanko,

    „Plik konfiguracyjny Apache (apache2.conf) powinien zawierać [..]”

    czy chodzi o plik httpd.conf ? Jeśli nie to gdzie znajduje sie plik apache2.conf ?

  4. Mariusz Dalewski pisze:

    @kinek: Tak, oczywiście chodzi o httpd.conf. W zależności od atrybutów konfiguracyjny, plik ten może nosić różne nazwy.
    Zmieniłem nazwy apache2.conf na httpd.conf celem poprawienia zgodności z domyślną konfiguracją.

  5. kustosz pisze:

    Świetny artykuł podsumowujący bezpieczeństwo apache i php! :) Gratulacje!

    Znalazłem błąd, w “Konfiguracja virtualhosta:” występują znaki spacji, których być nie powinno:

    […]
    Directory /var/www/ host1.example.com/htdocs/fcgi
    ——————–^^^^
    ErrorLog /var/log/httpd/ host1.example.com-error_log
    ————————^^^^
    CustomLog /var/log/httpd/ host1.example.com -access_log common
    —————————^^^^—————–^^^^
    […]

    pzdr…
    /kustosz

  6. Mariusz Dalewski pisze:

    @kustosz: Dziękuje za wyłapanie błędów. Oczywiście poprawione.

  7. Gutek pisze:

    czy ten artykuł należy „wdrażać” na serwerze do którego ma dostęp jedna osoba?? tzn mamy serwer firmowy ale jest tylko jedna osoba która dokonuje jakich kolwiek zmian w systemie. Mam wrażenie jakby to dotyczyło mass hostingu tak?

  8. Mariusz Dalewski pisze:

    @Gutek: Zależy jaką funkcje spełnia dany serwer. Rozwiązanie to zapewnia separacje posiadanej strony/stron www między sobą. Dodatkowo daje większe możliwości kontroli i podnosi bezpieczeństwo.

    Jeżeli masz kilka domen/subdomen (nawet przy jednym użytkowniku), to ja bym właśnie tak skonfigurował serwer.

  9. kustosz pisze:

    Mam jeszcze pytania:

    1. czy w przypadku takiej konfiguracji logi php i samego apache zapisują się z UID/GID danego użytkownika (SuexecUserGroup v1 v1), czy też z UID/GID użytkownika zdefinowanego w httpd.conf (User i Group) ???

    2. w samym systemie (np. za pomocą ps) widać procesy „httpd” jako uruchomione z UID/GID zgodne z httpd.conf (User i Group) ???

    pzdr…
    /kustosz

  10. Mariusz Dalewski pisze:

    @kustosz:
    1. Logi Apache (obsługiwane przez dyrektywy np ErrorLog albo CustomLog) zapisywane są z uprawnieniami użytkownika zdefiniowanego w httpd.conf przy użyciu dyrektyw User i Group. Logi PHP, jako że PHP uruchamiane jest z poziomu użytkownika, zapisywane są z uprawnieniami użytkownika zdefiniowanego przy użyciu dyrektywy SuexecUserGroup.

    2. Zgadza się. Główny proces nasłuchujący będzie działał z uprawnieniami roota, procesy potomne z uprawnieniami nadamy przez User i Group, a skrypty php będzie uruchamiane z uprawnieniami nadanymi przez SuexecUserGroup.

  11. Eozwiazanie pisze:

    htdocs/fcgi poprzez chattr +i fcgi. Uniemożliwi to wprowadzanie zmian na folderze fcgi (razem z jego zawartością) dla wszystkich użytkowników systemu. Tak założone atrybut +i zdejmuje tylko i wyłącznie root.

    Problem w tym, że użytkownik może zmienic nazwę kataogu htdocs i założyc własny htdocs/fcgi i wgrac sobie tam co chce, takie rozwiazania niestety nigdy nie będa 100% bezpieczne, chyba że katalog fcgi wyciagniemy poza katalog użytkowna ale za dużo z tym roboty biorąc pod uwagę, że dla każdej domeny ma byc osobny fcgi katalog najlepiej/

  12. Mariusz Dalewski pisze:

    @Eozwiazanie:
    Masz rację, że jest to luka, której rozwiązania nie ująłem w powyższym opisie, ale wszystko zależy od uprawnień użytkownika do htdocs. Postaram się na dniach znaleźć na to skuteczne rozwiązanie.

  13. Eozwiazanie pisze:

    Rozwiązaniem jest wyciągnąć katalog dla wrapperów fastcgi do katalogu powyżej którego użytkownik nie jest w stanie wyjść przez FTP czyli np do /home/LOGIN (przyjmując że FTP jest chrootniete do /home/login).

    Drugi problem to trzeba jakoś w .htaccess zablokować możliwość takiego wpisu:

    AddHandler php5-fastcgi .html
    Action php5-fastcgi /costam/php55

    Przy takim wpisie użytkownik sam sobie robi wrapper i znów obchodzi nam zabezpieczenia :(

  14. Mariusz Dalewski pisze:

    @Eozwiazanie:
    Drugi problem rozwiązujemy nie dodając dyrektywy „FileInfo” do „AllowOverride” w ustawieniach katalogu /home i mamy komplet.

  15. Eozwiazanie pisze:

    Tak ale wtedy zabieramy użytkownikowi wiele porzebnych opcji, które może sobie ustawic w .htaccess :(

  16. Eozwiazanie pisze:

    Teraz widzę, że własny wrapper działa tylko w katalogu cgi-bin (poza fcgi na który dałem chattr +i) w związku z tym że IncludesNOEXEC nie wiem dlaczego nic nie daje pozostaje tylko z apachea pokasować wszystkie
    ScriptAlias do cgi-bin mam racje?

  17.