Deblan blog

Tag #Apache

[tips] PHP FPM : récupérer la vraie adresse IP du visiteur

Mon serveur web fonctionne par couches. La première couche est gérée par Nginx et traite les requetes HTTP des internautes. Nginx gère les problématiques de cache sur les assets, c'est à dire les images, les fichiers javascripts et enfin les fichiers CSS. Ensuite, il transmet les requêtes au serveur web Apache qui va délivrer le site web concerné et faire appel au process manager de PHP (FPM) pour exécuter PHP.

En l'état, il n'est pas possible de connaître l'adresse IP de l'internaute via la variable globale $_SERVER en utilisant l'index REMOTE_ADDR. En effet, si j'affiche le contenu de $_SERVER['REMOTE_ADDR'], j'aurai comme résultat 127.0.0.1 qui est l'IP locale de Nginx.

Cependant, j'ai configuré Nginx de tel sorte qu'il ajoute les entêtes HTTP X-Forwarded-For et X-Real-IP. Apache les transmet ensuite via les index HTTP_X_FORWARDED_FOR et HTTP_X_REAL_IP.

proxy_set_header X-Real-IP  $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Ces index contiennent l'IP de l'internaute mais ne sont pas utilisés par les applications hébergées. Typiquement, Nextcloud détectait l'adresse IP 127.0.0.1. Lors d'un brute force, tous les comptes utilisateurs étaient impactés par la sécurité enclanchée par Nextcloud (soit 30s d'attente avant la validation des identifiants de connexion).

J'ai fouiné sur la toile et plusieurs solutions sont évoquées. J'ai un peu tout essayé et au delà des modules Apache Rpaf et RemoteIP, pas grand chose de probant. Rpaf est déjà installé et permet à Apache de logger la bonne IP dans les logs d'accès. RemoteIP n'a pas fonctionné dans mon environnement.

Pour résoudre le problème, j'ai appliqué ce qui a été rédigé dans ce post : inclure un script PHP de façon automatique pour l'ensemble des sites web du serveur.

j'ai créé un fichier PHP comme suit :

<?php

$trustedProxies = [
    '127.0.0.1',
];

$remote = $_SERVER['REMOTE_ADDR'];

$headers = [
    'HTTP_X_FORWARDED_FOR' => 'REMOTE_ADDR',
    'HTTP_X_REAL_IP' => 'REMOTE_HOST',
];

if (in_array($remote, $trustedProxies)) {
    foreach ($headers as $header => $value) {
        $_SERVER[$value] = $_SERVER[$header];
    }
}

Puis j'ai alimenté les php.ini de cette façon :

auto_prepend_file = /etc/php/fpm/real-ip.php

Après avoir relancé les services, j'ai pu constater que l'adresse IP de l'internaute est bien présente dans $_SERVER['REMOTE_ADDR'].


Installer plusieurs versions de PHP sur un serveur web

Il arrive qu'il soit nécessaire d'installer des versions de PHP spécifiques pour des projets qu'on héberge. Le must du must est de passer par son gestionnaire de paquet mais ce n'est pas toujours possible. Il convient donc de récupérer le ou les versions nécessaires et de les compiler.

Une des problématiques réside dans la sécurisation du serveur web. Il est important de considérer que se passer de son gestionnaire de paquet est dangereux car on ne bénéficie pas des mises à jour de sécurité et qu'il est impératif de gérer les droits pour que les projets impactés s'exécutent avec des utilisateurs qui leur sont dédiés.

La procédure d'installation n'est pas compliquée mais ça demande d'être minutieux.

Pour gérer la récupération du code source et de sa compilation, j'aime beaucoup travailler avec PhpFarm. On va donc récupérer le projet et simplement balancer un peu de config.

Pour cet exemple, je veux d'installer PHP 5.4.28. L'ensemble des commandes est fait en root.

aptitude update
aptitude install git make
cd /usr/local
git clone git://git.code.sf.net/p/phpfarm/code phpfarm
cd phpfarm/src

Je vous propose de modifier les options de compilation afin d'avoir quelques outils souvent très utilisés. Il suffit d'éditer le fichier src/options.sh et d'éditer la liste des paramètres :

configoptions="\
--enable-bcmath \
--enable-calendar \
--enable-exif \
--enable-ftp \
--enable-mbstring \
--enable-pcntl \
--enable-soap \
--enable-sockets \
--enable-sqlite-utf8 \
--enable-wddx \
--enable-zip \
--with-openssl \
--with-zlib \
--with-gettext \
--enable-libxml \
--with-zlib \
--with-xsl \
--with-jpeg-dir \
--with-png-dir \
--with-gd \
--with-ttf \
--with-freetype-dir \
--with-pdo-mysql \
--with-mysql \
$gcov"

À présent, il suffit de demander à PhpFarm de télécharger et compiler notre version de PHP.

./compile.sh 5.4.28

Il manquera sans doute des lib de dev. Par exemple, s'il remonte une erreur concerant jpeg, faite une recherche du type aptitude search jpeg | grep dev et installez ce qui convient (le paquet libjpeg8-dev dans ce cas).

Une fois la compilation achevée, il faut configurer le serveur web (apache2 dans mon cas). Je rappel que je veux que mes projets aient un utilisateur dédié. Je vais faire une configuration similaire à celle-ci.

a2enmod actions suexec
mkdir -p /var/www/service-web/bin/cgi
echo /usr/local/phpfarm/inst/bin /var/www/service-web/bin/cgi none bind >> /etc/fstab
mount /usr/local/phpfarm/inst/bin

Si j'ai un projet "blog", mon utilisateur dédié sera "webblog". J'ai également un groupe "webgroup" pour tout mes projets.

addgroup webgroup
useradd -G webgroup -s /bin/false -M webblog
chown -R webblog:webgroup /chemin/vers/projet/blog
mkdir /var/www/service-web/bin/cgi/webblog
cd /var/www/service-web/bin/cgi/webblog
cat > php-cgi <<EOS
#!/bin/sh
exec /usr/local/phpfarm/inst/bin/php-cgi-5.4.28
EOS
chown -R webblog:webgroup .
chmod u+x php-cgi

Il manque plus que la modification du VirtualHost :

<VirtualHost *:80>
	[...]
	SuexecUserGroup webblog webgroup
	ScriptAlias /phpfarm-bin/ /var/www/service-web/bin/cgi/webblog/
	AddHandler application/x-httpd-php5 php
	Action application/x-httpd-php5 /phpfarm-bin/php-cgi
	[...]
</VirtualHost>

Faites une petite prière et relancez apache :

service apache2 restart

Comme je n'utilise que du FastCGI (ou des CGI), je ne sais pas comment la configuration réagit si PHP (en module apache) est activé.


Statistiques de son site web

En règle générale, les personnes apprecient avoir un retour de leur travail. Quand on a un site web, il paraît évident de ressortir quelques chiffres sur les visteurs : quelles pages lisent-ils ? D'où viennent t-ils ? Combien de temps restent-ils ? etc.

Il existe une grande quantité d'outils qui permettent de générer des rapports. Le plus connu du grand public est sans doute Google Analytics. Mon problème c'est que j'ai du mal à confier des choses à des sociétés externes. Quels sont mes outils d'analyse sur mes sites ? En voici quelques uns.

Open Web Analytics (OWA) est un outil open source. Il est très similaire à Google Analytics et s'installe sur son hébergement. Il dispose de pas mal de fonctionnalités : statistiques sur la consultation des pages, les domaines de référence, l'analyse des requêtes sur les moteurs de recherche, création de campagnes, etc. Il est complet et permet de gérer plusieurs sites. Il fonctionne via l'ajoût de scripts javascript et/ou PHP au sein des pages.

Open Web Analytics

Webalizer est un générateur de rapport html qui se repose sur les logs Apache2. Il va sortir des chiffres sur la consultation des pages, les url de référence et de sortie, quelques données sur la bande passante utilisée et quelques infos sur les clients.

Il se lance en ligne de commande. Voila une commande type qui va générer un rapport nommé "Mon blog", dans le répertoire "stats" via le fichier de log "mon_site.log" :

webalizer -n "Mon blog" -o stats mon_blog.log

Webalizer est packagé sur Debian.

Webalizer

Le dernier outil est goaccess. C'est un programme qui s'exécute en ligne de commande et le rendu se fait dans console (interface ncurses).

goaccess

goaccess -f mon_blog.log

Goaccess est packagé sur Debian.

Pour lire un fichier de log apache2, vous devez disposez d'un accès root (comportement de base).

C'est primordiale de confronter les données générées par ces outils.


Domaines de développement - Apache2

Aujourd'hui j'ai eu besoin d'une configuration particulière d'Apache : j'ai décidé que les sous-domaines de deblan.org de type "dev-XXX" devaient être gérés de tel sorte que "XXX" corresponde à un sous-répertoire de dev.deblan.org. Ainsi, dev-01.deblan.org est l'équivalent de dev.deblan.org/01/.

Jusque là, rien de bien méchant sauf que j'ai une contrainte : je souhaite que ce soit automatique (ou le plus possible). Donc je ne souhaite pas ajouter/modifier des Virtualhost, je veux qu'un sous-domaine puisse être supprimé dans me prendre la tête.

La solution est au final simple (mais un peu tordue) :

Etape 1. Créer un fichier de mapping

L'idée est la suivante : comme tout ne sera pas automatique, je m'autorise l'édition d'un seul fichier pour traiter la correspondance d'un nom et de son répertoire de destination (en quelque sorte son DocumentRoot).

Ce fichier sera placé à la racine du compte associé à dev.deblan.org. Sur mon serveur, c'est le chemin suivant :

/services/web/www/dev-xx.deblan.org/vhost.map

Ce fichier contient ces informations :

dev.deblan.org /var/www/service-web/www/dev-xx.deblan.org/public_html/
dev-01.deblan.org /var/www/service-web/www/dev-xx.deblan.org/public_html/01
dev-foo.deblan.org /var/www/service-web/www/dev-xx.deblan.org/public_html/foo

On a donc à gauche le nom de domaine et à droite le répertoire web associé.

A travers une connexion au compte dev.deblan.org (via SSH ou en FTP par exemple), je peux modifier ce fichier.

Etape 2. Création du VirtualHost d'Apache
<VirtualHost *:80>
	# On définit le domaine de base
	ServerName dev.deblan.org
	# On indique les alias de domaine à prendre en compte 
	ServerAlias dev-*.deblan.org

	# La partie la plus importante : les règles qui permettront de traiter ma demande
	RewriteEngine on

	# C'est ici que tout se passe
	RewriteMap lowercase int:tolower
	# On charge le fichier pour le mapping
	RewriteMap vhost txt:/services/web/www/dev-xx.deblan.org/vhost.map
	# On prend le soin de travailler de tout mettre en minuscules (donc attention à la casse !)
	RewriteCond ${lowercase:%{SERVER_NAME}} ^(.+)$
	RewriteCond ${vhost:%1} ^(/.*)$
	# On "simule" le DocumentRoot du domaine
	RewriteRule ^/(.*)$ %1/$1

	[...]
</VirtualHost>
Etape 3. Création d'un script de mise à jour d'Apache

Le fichier de mapping n'est chargé qu'une seule fois et c'est quand on relance ou reload Apache. Il faut donc, à des temps réguliers, vérifier si le fichier de mapping a été modifié et si on doit relancer Apache.

Il existe plusieurs méthodes, moi je vous propose un truc très simple :

(Fichier /root/apache2/reload)

#!/bin/sh

# On indique le fichier de mapping
MAPPING=/services/web/www/dev-xx.deblan.org/vhost.map

# On indique le fichier qui va contenir le retour de "ls -l" sur le fichier de mapping
LS_FILE=/root/apache2/ls

[ -f "$LS_FILE" ] || touch "$LS_FILE"

LS=$(ls -l "$MAPPING")

# Si il y une différence entre le "nouveau" ls et l'ancien ls, alors le fichier à été modifié
# Donc on relance apache et on met à jour $LS_FILE
if [ "$LS" != "$(cat "$LS_FILE")" ]; then
	/usr/sbin/service apache2 reload
	echo "$LS" > "$LS_FILE"
fi

Il ne vous reste plus qu'à placer ça dans un crontab avec par exemple cette ligne de conf :

*/1 * * * * /root/apache2/reload 2>&1 >/dev/null

Ici le script est exécuté toutes les minutes.

Normalement tout devrait fonctionner :)

Si vous avez d'autres solutions, je suis prenneur !


Serveur web Debian : Apache2 et FastCGI

Dans le cadre de l'installation d'un serveur web personnel avec un unique site, il n'est pas forcément nécessaire d'imaginer une configuration "poussée" d'apache avec une gestion des droits elle aussi "poussée".

Sur une Debian, apache s'exécute par défaut avec l'utilisateur www-data et le groupe www-data. Ainsi, si on héberge un site dont les fichiers ont un propriétaire différent, ils devront avoir un chmod type 755. Cependant, si on veut permettre à apache de modifier un fichier ou un repertoire, le chmod sera 777 et c'est le mal absolu !

Il faut donc installer les outils de telle sorte qu'un site puisse avoir des fichiers en 755 (par exemple) et qu'apache puisse tout de même écrire dedans. On admet donc facilement que apache doit devenir le propriétaire des fichiers.

On a donc deux solutions : utiliser www-data:www-data ou bien demander qu'apache s'exécute avec les droits des fichiers du site.

Dans le cas où on a un seul site, la première solution est envisageable...mais vient le problème d'un serveur multi-sites : peut-on prendre le risque de voir l'ensemble des sites supprimés car un d'eux aurait été piraté ? Ma réponse est non !

L'objectif de ce tutorial est le suivant :

  • Un site web admet un utilisateur unique
  • On aura un groupe commun pour tout les sites
  • Apache devra être capable, pour chaque site, de s’exécuter avec le bon utilisateur et le bon groupe

L'idée est d'utiliser pour les scripts PHP (ou perl, peu importe le langage finalement) un "Wrapper" qui permettra d'utiliser un binaire type php5-cli pour lancer les scripts php du site hébergé. C'est ce wrapper et l'utilisation de modules qui conditionnera l'exécutation d'apache avec des droits spécifiques.

Je rédige ce tutoriel avec une Debian Squeeze mais il est valable pour beaucoup d'autres système (pour peu qu'on adapte les commandes et les fichiers de conf).

Informations :

  • Sauf précision, toutes les commandes suivantes sont exécutées en root
  • Votre système doit être à jour et la liste des paquets du gestionnaire également

Si vous avez déjà un serveur en place, il y aura des perturbations quant à apache donc prévoyez des coupures relativement longues du service web. Aussi, si vous utilisez le mode php5 pour apache, il faut veiller à le désactiver.

prompt> a2dismod php5

Pour les personnes qui n'ont pas apache installé :

prompt> aptitude install apache2

Mon objectif est d'avoir des sites avec php. On va donc installer les paquets dont on a besoin :

prompt> aptitude install php5-cgi

Bien sûr, si vous souhaitez des modules comme par exemple GD, vous pouvez les installer sans problème.

Suite à cette installation, on a dans "/usr/bin/", un binaire php5-cgi qui quand on l'exécute retourne une entête HTTP et le résultat de l'exécution du code qu'on lui donne :

prompt> /usr/bin/php5-cgi 
<?php echo "Hello World\n"; ^D
X-Powered-By: PHP/5.3.3-7+squeeze1
Content-type: text/html

Hello World

C'est via ce cgi que nos scripts seront exécutés.

Nous allons installer deux modules apache2 : libapache2-mod-fcgid (pour utiliser des cgi) et apache2-suexec (pour changer les droits).

prompt> aptitude install libapache2-mod-php5 libapache2-mod-fcgid apache2-suexec

Sauf compilation par nos soins, il faudra absolument placer les sites web dans /var/www. Si comme moi, vous avez une partitions dédiée aux site web, non montée dans /var/www, vous devrez modifier la configuration de votre système. J'ai pour ma part, sur les serveurs que j'administre, un point de montage /services qui contient les données de mes services (web, git, svn, etc). Via /etc/fstab, j'ai simplement monté mon répertoire /services/web dans /var/www/service-web en faisant un bind :

prompt> mkdir /var/www/service-web
prompt> echo "/services/web /var/www/service-web none bind 0 0" >> /etc/fstab
prompt> mount -a

Nous allons créer le groupe commun de chaque site : webgroup.

prompt> addgroup webgroup

Dans mon tutoriel, j'ai décidé que mes sites seraient : site1.com, site2.com. Ainsi, j'ai choisi de nommé les utilisateurs website1 et website2. Nous allons créer les utilisateurs (unix) :

prompt> useradd -G webgroup -s /bin/false -M website1
prompt> useradd -G webgroup -s /bin/false -M website2

J'ai évoque tout à l'heure la notion de "wrapper". C'est un script shell qui va faire le lien entre apache et php5-cgi.

Je décide de placer mes wrappers, uniques pour chaque site, dans : /var/www/service-web/bin/webXXX :

prompt> mkdir -p /var/www/service-web/bin/website1 /var/www/service-web/bin/website2

Je vais maitenant placer mon script (wrapper) dans ces répertoires :

prompt> cd /var/www/service-web/bin
prompt> cat << EOF > /tmp/php5-fcgi
#!/bin/sh
exec /usr/bin/php5-cgi
EOF
prompt> cp /tmp/php5-fcgi website1
prompt> cp /tmp/php5-fcgi website2

On va maintenant donner les droits adéquates :

prompt> chmod -R 755 web*
prompt> chown -R website1:webgroup website1
prompt> chown -R website2:webgroup website2

Pour être certain que nos modules apache précédemment installés sont activés, ben nous allons les....activer :

prompt> a2enmod suexec fcgid

On ajoute une configuration chargée systématiquement par apache avec des options très bien documentées ici.

prompt> cat << EOF >  /etc/apache2/conf.d/php-fcgi
<IfModule mod_fcgid.c>
  AddHandler fcgid-script .fcgi .php
  FcgidIPCDir /var/lib/apache2/fcgid/sock
  FcgidConnectTimeout 10
  FcgidIOTimeout 120
  FcgidOutputBufferSize 0
  FcgidMaxRequestsPerProcess 500
  FcgidMaxRequestLen 131072
  FcgidMinProcessesPerClass 0
  FcgidIdleScanInterval 1
  FcgidProcessLifeTime 20	
</IfModule>
EOF

L'étape suivante est la création des répertoires pour les sites web. J'ai décidé de les placer dans un répertoire www de /var/www/service-web/ :

prompt> mkdir -p /var/www/service-web/www/site1.com /var/www/service-web/www/site2.com 
prompt> cd /var/www/service-web/www/
prompt> cat << EOF > /tmp/droits.php
<?php echo shell_exec("id"); ?>
EOF
prompt> cp /tmp/droits.php site1.com
prompt> cp /tmp/droits.php site2.com
prompt> chmod -R 755 site*
prompt> chown -R website1:webgroup site1.com
prompt> chown -R website2:webgroup site2.com

La configuration des VirtualHost se fait très simplement.

prompt> cat << EOF > /tmp/vhost
<VirtualHost *:80>
 ServerName DOMAIN
 DocumentRoot /var/www/service-web/www/DOMAIN
 SuexecUserGroup USER webgroup
 <Directory /var/www/service-web/www/DOMAIN>
  FCGIWrapper /var/www/service-web/bin/USER/php5-fcgi .php
  Options Indexes FollowSymLinks MultiViews
  Options +ExecCGI
 </Directory>
</VirtualHost>
EOF
prompt> sed 's/USER/website1/;s/DOMAIN/site1.com/' /tmp/vhost > /etc/apache2/sites-available/site1.com
prompt> sed 's/USER/website2/;s/DOMAIN/site2.com/' /tmp/vhost > /etc/apache2/sites-available/site2.com
prompt> a2ensite site1.com site2.com

L'ajout de VirtualHost cumulée à l'activation de modules, il faut redémarrer apache :

prompt> service apache2 restart

Pour terminer, on doit s'assurer que tout fonctionne correctement. Si vous allez sur http://site1.com/droits.php et http://site2.com/droits.php, vous devriez constater que les uid sont différents.