PHP-FPM, le serveur d'application made by PHP

Jean Baptiste FAVRE

Septembre 2010

English version is available here: PHP-FPM, application server made by PHP.

Introduction

Avant, faire tourner plusieurs application PHP sur un serveur Web vous laissait avec 2 options:

Aujourd'hui, PHP-FPM est en train de changer la donne. PHP-FPM, pour FastCGI Process Manager, permet d'avoir en permanence des processus PHP, sur le modèle d'Apache pre-fork. Ces processus ont la particularité d'être regroupés en pool. Ce regroupement permet d'exploiter un certain nombre de fonctionnalités:

Du coup, il est possible d'atteindre un niveau d'isolation des applications PHP intéressant. Il n'est pas question ici de faire l'analyse détaillée du modèle de sécurité de PHP-FPM, mais bien de vous montrer concrètement comment vous pourriez faire.

Préparation

Je ne vais pas expliquer dans le détail l'installation des composants. D'autres l'ont fait bien avant moi, et probablement mieux que je ne pourrais le faire moi-même.

Nous allons simplement voir concrètement comment utiliser au mieux PHP-FPM pour déployer des applications PHP. Avant de commencer, jetons un oeil au cahier des charges:

La question du chroot est délicate: lors de mes tests, une application chrootée ne pouvais plus résoudre de mon de domaines. Ceci est sans doute dû à une librairie absente de l'arborescence de chroot, donc pensez-y si vous vous lancez.

Dans tous les cas, nous avons besoin d'une arborescence basique et ce quelque soit l'application concernée. Imaginons que toute votre arborescence web se trouve dans /var/www/. Alors, les domaines hébergés setrouveront dans /var/www/domains/, les applications mutualisées dans /var/www/apps/.

Pour plus de détails en ce qui concerne les domaines, je vous renvoie aux autres publications s'appliquant à Apache ou Nginx:

En résumé, voici à quoi doit ressembler l'arborescence de notre domain domain.tld:

tree /var/www/domains/
domain.tld/
|-- config
|   |-- auth
|   `-- ssl
|-- logs
|   |-- access.log
|   |-- error.log
`-- docroot
    |-- subdir1
    |-- subdir2
    |-- subdir3
    `-- subdir4

Quant à nos applications mutualisées, pas de grande différence:

tree /var/www/apps/
appname/
|-- config
|-- docroot
|-- logs
|-- private
`-- tmp

Quelques précisions tout de même:

Installation de PHP-FPM

Pas de surprise, je m'appuye pour cela sur l'excellent travail de Guillaume Plessis qui met gracieusement à disposition des utilisateurs Debian des paquets PHP 5.3.3 intégrant PHP-FPM via le site DotDeb.

sous Debian
cat /etc/apt/sources.list
deb //php53.dotdeb.org stable all
deb //packages.dotdeb.org stable all
gpg --keyserver keys.gnupg.net --recv-key 89DF5277
gpg -a --export 89DF5277 | sudo apt-key add -
aptitude update
aptitude install php5-fpm

Une fois installé, il est temps de commencer la configuration. PHP-FPM fonctionne donc un peu à la manière d'un serveur d'application. Pour cela, un process père va démarrer et tourner en tant que root, ce qui lui permettra de forker d'autres processus avec des caractéristiques que nous préciserons par la suite.

Configuration de PHP-FPM

La configuration de PHP-FPM sous Debian se trouve dans le fichier /etc/php5/fpm/php5-fpm.conf

cat /etc/php5/fpm/php5-fpm.conf
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;
; Global Options ;
;;;;;;;;;;;;;;;;;;

[global]
pid = /var/run/php5-fpm.pid

error_log = /var/log/php5-fpm.log
log_level = notice

emergency_restart_threshold = 5
emergency_restart_interval = 2
process_control_timeout = 2

daemonize = yes

;;;;;;;;;;;;;;;;;;;;
; Pool Definitions ;
;;;;;;;;;;;;;;;;;;;;
include=/home/hosting/www/apps/*/config/fpm-pool.conf
include=/home/hosting/www/domains/*/config/fpm-pool.conf

Comme vous pouvez le constater, la conf globale est des plus réduite. Vous aurez bien entendu noté les 2 directives include en fin de fichier. Ces 2 directives vont nous permettre:

Exemple de pool de domaine

Premier pool à être configuré, le pool de domaine. Celui-ci devra tourner avec des UID et GID particulier. Par défaut, il sera chrooté pour ne voir que l'arborescence du domaine. Enfin, un certain nombre de paramètres PHP seront personnalisés.

Les pools de domaine utiliseront un UID/GID compris entre 9001 et 9999. Ce sera également le port d'écoute de PHP-FPM.

cat /var/www/domains/domain.tld/config/fpm-pool.conf
[domain.tld]
    listen                 = 127.0.0.1:9001
    listen.backlog         = -1
    listen.allowed_clients = 127.0.0.1
    listen.owner           = domain.tld
    listen.group           = domain.tld
    listen.mode            = 0666

    user  = domain.tld
    group = domain.tld

    pm                   = dynamic
    pm.max_requests      = 0
    pm.max_children      = 2
    pm.start_servers     = 1
    pm.min_spare_servers = 1
    pm.max_spare_servers = 1

    pm.status_path       = /php_pool_domain.tld_status
    ping.path            = /domain.tld_ping
    ping.response        = domain.tld_pong
 
    request_terminate_timeout = 2
    request_slowlog_timeout   = 1
    slowlog                   = /var/www/domains/domain.tld/logs/php-slow.log
 
    ;rlimit_files = 1024
    ;rlimit_core = 0
 
    chroot = /var/www/domains/domain.tld/
    ; Chdir to this directory at the start. This value must be an absolute path.
    ; Default Value: current directory or / when chroot
    chdir = /docroot/

    catch_workers_output = yes
 
    env[HOSTNAME] = $HOSTNAME
    env[TMP]      = /tmp
    env[TMPDIR]   = /tmp
    env[TEMP]     = /tmp

    ;   php_value/php_flag             - you can set classic ini defines which can
    ;                                    be overwritten from PHP call 'ini_set'. 
    ;   php_admin_value/php_admin_flag - these directives won't be overwritten by
    ;                                     PHP call 'ini_set'
    php_flag[display_errors]            = on
    php_admin_value[error_log]          = /logs/php_err.log
    php_admin_flag[log_errors]          = on
    php_admin_value[memory_limit]       = 1M
    php_value[max_execution_time]       = 2

Points particuliers:

Vous aurez remarquer qu'une directive ne tient pas compte du chroot éventuel: slowlog. En fait, ceci est tout à fait normal: elle est prise en compte par le processus père et, de ce fait, insensible au chroot auquel on soumet le processus fils. L'avantage ici est que la mesure est plus précise car s'effectuant en dehors du contexte d'exécution PHP. Néanmoins, il est pratique de journaliser les requètes lentes par domaine, notamment dans le cas où le propriétaire du domaine possède un accès FTP ou SSH qui lui donne de ce fait accès à ces logs.

C'est bien joli tout ça, mais notre utilisateur domain.tld n'existe pas. il faut y remédier:

Création du compte utilisateur pour une application spécifique
mkdir -p /var/www/domains/domain.tld/{config,docroot,logs,stats,tmp}
addgroup --force-badname --system --gid 9001 domain.tld
adduser --home /var/www/domains/domain.tld --shel /bin/false --no-create-home \
     --uid 9001 --gid 9001 --force-badname --disabled-password --disabled-login domain.tld

L'option --force-badname est nécessaire car le nom d'utilisateur comporte un point. Je n'ai pas vu d'effets de bord indésirable juqu'à maintenant, mais libre à vous de faire autrement.

Exemple de pool applicatif

Second type de pool à être configuré, le pool applicatif. Celui-ci devra également tourner avec des UID et GID particulier. Par défaut, il sera lui aussi chrooté pour ne voir que l'arborescence de l'application. Enfin, un certain nombre de paramètres PHP seront personnalisés.

Les applications partagées entre plusieurs domaines utiliseront un gid/uid compris entre 10001 et 10999. Ce sera également le port d'écoute de PHP-FPM

cat /var/www/apps/appname/config/fpm-pool.conf
[appname]
    listen                 = 127.0.0.1:10001
    listen.backlog         = -1
    listen.allowed_clients = 127.0.0.1
    listen.owner           = appname
    listen.group           = appname
    listen.mode            = 0666

    user  = appname
    group = appname

    pm                   = dynamic
    pm.max_requests      = 0
    pm.max_children      = 10
    pm.start_servers     = 5
    pm.min_spare_servers = 5
    pm.max_spare_servers = 5
    pm.status_path       = /php_pool_appname_status

    ping.path     = /appname_ping
    ping.response =  appname_pong
 
    request_terminate_timeout = 5
    request_slowlog_timeout   = 2
    slowlog                   = /var/www/apps/appname/logs/php-slow.log
 
    ;rlimit_files = 1024
    ;rlimit_core = 0
 
    chroot = /var/www/apps/appname/
    chdir = /docroot/

    catch_workers_output = yes
 
    env[HOSTNAME] = $HOSTNAME
    env[TMP]      = /tmp
    env[TMPDIR]   = /tmp
    env[TEMP]     = /tmp

    ;   php_value/php_flag             - you can set classic ini defines which can
    ;                                    be overwritten from PHP call 'ini_set'. 
    ;   php_admin_value/php_admin_flag - these directives won't be overwritten by
    ;                                     PHP call 'ini_set'
    php_flag[display_errors]       = on
    php_admin_value[error_log]     = /logs/php_err.log
    php_admin_flag[log_errors]     = on
    php_admin_value[memory_limit]  = 2M

Rien de particulier qui n'ai pas déjà été expliqué à l'étape précédente. Là encore, la directive slowlog ne tient pas compte du chroot éventuel. Et, bien entendu, il faut créer l'utilisateur adéquat:

Création du compte utilisateur pour un pool applicatif
mkdir -p /var/www/apps/appname/{config,docroot,logs,private,tmp}
chmod 7777 /var/www/apps/appname/tmp
chown appname: /var/www/apps/appname/private
chmod -R u=rwX,g=rX,o= /var/www/apps/appname/private
addgroup --force-badname --system --gid 9001 domain.tld
adduser --home /var/www/apps/appname --shel /bin/false --no-create-home \
     --uid 9001 --gid 9001 --disabled-password --disabled-login appname

Bien. Nous avons installé PHP-FPM, paramétré le processus père, paramétré 2 processus, un de domaine, l'autre d'application. Il est à présent temps de les rendre accessibles. Pour cela, rien de tel qu'un petit NGinx.

Configuration de Nginx

Je passe sur l'installation de NGinx. Elle ne présente absolument aucune difficulté sous Debian. Tout au plus pourrez-vous vous faire des nœuds au cerveau pour installer la version backport au lieu de stable. Bref, rien de vraiment bien méchant, et de toute façon, largement documentée sur le Net. As usual, Google is your best friend ever !

Pour faire simple, Nginx va être configuré de la manière suivante:

Un vhost par défaut, qui accueillera les URL de monitoring de vos pool PHP-FPM
Ben oui, parce que c'est pas parce que c'est du FastCGI, que c'est chrooté et que ça tourne sous un ID particulier que faut pas surveiller un tantinet.
Les URL d'entrée de vos applications partagées
Ces définitions d'URL, directive Location en jargon Nginx, seront incluses dans les vhosts concernés.
Configuration Nginx
server {
	listen 80 default;
	allow 127.0.0.1;
	deny all;
	location = /nginx_status {
		stub_status on;
		access_log off;
		allow 127.0.0.1;
		deny all;
	}
	include /var/www/domains/*/config/nginx_status.conf;
	include /var/www/apps/*/config/nginx_status.conf;
}
server {
        listen 80;
        server_name domain.tld *.domain.tld;
        root /var/www/domains/domain.tld;

        access_log /var/www/domains/domain.tld/logs/access.log vhosts;
        error_log  /var/www/domains/domain.tld/logs/error.log info;

        location / {
            root   /var/www/domains/domain.tld/docroot/;
            try_files $uri $uri/ $uri.php;
        }

        ##### Enabled domain.tld PHP support #####
        include /var/www/domains/domain.tld/config/nginx.conf;

        ##### Enabled global PHP apps support #####
        include /var/www/apps/appname/config/nginx.conf;
        # 1 line per PHP app to enable...
        #include /var/www/apps/appname1/config/nginx.conf;
        #include /var/www/apps/appname2/config/nginx.conf;
}

Comme vous pouvez le constater, le vhost par défaut charge toutes les URL de monitoring (fichiers nginx_status.conf) des pool PHP-FPM. Le vhost du domaine "domain.tld" quant à lui va charger un par un les fichiers de config des pools applicatifs qui doivent être accessibles sur ce domaine.

En ce qui concerne les applications, rien de bien difficile, pour qui comprend un peu la syntaxe Nginx:

Configuration d'un pool applicatif
location = /php_pool_appname_status {
	fastcgi_pass 127.0.0.1:10001;

	##### Include common FastCGI params #####
	include /etc/nginx/conf.d/fastcgi.conf;
}
location ~ /appname(.*)\.(css|js|gif|png)$ {
    root /var/www/apps/appname/docroot;
    access_log off;
    expires 30d;
}
location /appname {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    # A handy function that became available in 0.7.31 that breaks down 
    # The path information based on the provided regex expression
    # This is handy for requests such as file.php/some/paths/here/ 

    fastcgi_pass   127.0.0.1:10001;

    fastcgi_connect_timeout          5;
    fastcgi_send_timeout             5; 
    fastcgi_read_timeout             5; 
    fastcgi_buffer_size              128k;
    fastcgi_buffers                4 256k;
    fastcgi_busy_buffers_size        256k;
    fastcgi_temp_file_write_size     256k;
    fastcgi_intercept_errors         on;

	##### Include common FastCGI params #####
    include /etc/nginx/conf.d/fastcgi.conf;
}
Configuration d'un pool de domaine
location = /php_pool_domain.tld_status {
	fastcgi_pass 127.0.0.1:9001;

	##### Include common FastCGI params #####
	include /etc/nginx/conf.d/fastcgi.conf;
}
location ~ \.php$ {
	fastcgi_split_path_info ^(.+\.php)(/.+)$;
	# A handy function that became available in 0.7.31 that breaks down
	# The path information based on the provided regex expression
	# This is handy for requests such as file.php/some/paths/here/

	fastcgi_pass   127.0.0.1:9001;
	# Forward request to "domain.tld" pool

	fastcgi_connect_timeout          1;
	fastcgi_send_timeout             1;
	fastcgi_read_timeout             2;
	fastcgi_buffer_size              128k;
	fastcgi_buffers                4 256k;
	fastcgi_busy_buffers_size        256k;
	fastcgi_temp_file_write_size     256k;
	fastcgi_intercept_errors         on;

	include /etc/nginx/conf.d/fastcgi.conf;
}

Comme vous pouvez le constater, rien de délicat. Mais vous aurez besoin du fichier de configuration FastCGI. Celui-ci regroupe tous les éléments communs de la configuration FastCGI. Les autres, qui demeure propres à chaque domaine, vous permettrons de jouer sur, par exemple, les timeout:

Configuration FastCGI
fastcgi_index  index.php;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
#SCRIPT_NAME needs to remain without the document_root on front
#Otherwise PHP_SELF breaks, the interpreter will still find the file
#because of the added SCRIPT_FILENAME parameter below

fastcgi_param  SCRIPT_FILENAME    $fastcgi_script_name;
#This one was added right into the file, since with root moved, and path_info split
#It should no longer need to be set manually for each virtual host

fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;

#the two lines below are possible now because of the fastcgi_split_path_info function
fastcgi_param  PATH_INFO          $fastcgi_path_info;
fastcgi_param  PATH_TRANSLATED    $document_root$fastcgi_path_info;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

#PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

Tests

Une fois tout ceci mis en place, il est temps de tester. Pour cela, créez un fichier /var/www/domains/domain.tld/docroot/index.php et un autre /var/www/apps/appname/docroot/index.php avec le contenu suivant:

Fichier index.php de test
<?php
phpinfo();

Puis, pour vérifier que tout fonctionne bien, ouvrez les URL suiventes dans votre navigateur:

Pool de domaine
//domain.tld/
Pool applicatif
//domain.tld/appname/

Vous devez voir apparaître la page classique de PHPInfo. Sinon, ben... respirez un grand coup, faites une pause, et reprenez depuis le début ;-)

Conclusion

Cette méthode, bien qu'un peu contraignante, vous apportera de la souplesse dans la gestion du déploiement de vos applications PHP. En outre, vous bénéficierez d'une sécurité accrue, ce qui est loin d'être négligeable quand on sait que la majeure partie des vulnérabilités actuelles se situent justement dans les applications Web.

Bien sûr, tout n'est pas idéal: le chroot ne vous protégera pas contre un défacement de site si une faille est trouvée dans l'application. En revanche, une application bien cloisonnée limitera l'attaquant à l'arborescence de l'application, et l'empêchera d'avoir acès au reste du disque dur.

Enfin, comme toujours, tout ceci ne doit pas vous dispenser de sécuriser le reste du serveur. N'oubliez pas que "Le niveau de sécurité d'un ensemble correspond au niveau de sécurité du maillon le plus faible".

Format

Ce document est disponible aux formats suivants:

À propos de Jean Baptiste FAVRE

Ingénieur système Linux, je travaille, entre autres, sur la virtualisation et l'amélioration des performances web. De temps en temps, j'arrive à décrocher de mon clavier pour lire un bon bouquin en écoutant de la musique.

License

Creative Commons License Cette publication est publiée sous contrat Creative Common by-nc-sa

Valid XHTML 1.0 Strict |  Valid CSS |  contrat Creative Common by-nc-sa

Table des matières

  1. Introduction
  2. Préparation
  3. Installation de PHP-FPM
  4. Configuration de PHP-FPM
  5. Exemple de pool de domaine
  6. Exemple de pool applicatif
  7. Configuration de Nginx
  8. Tests
  9. Conclusion
  10. Format
  11. À propos ...
  12. License