Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.
C’est alors la barrière de la prise en main qui fait peur, et pourtant...
Les logiciels libres
L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.
Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.
Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.
Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.
- Février 2022 -
Sommaire
Dans une installation Linux-nginx-PHP classique, on a:
- systemd qui doit orchestrer les services et s'exécute en root (inévitable)
- nginx qui reçoit les les requêtes web et les répartit, notamment vers php-fpm. Il fonctionne avec un processus maître qui fonctionne en root pour se mettre en écoute sur le port 443 et des workers, non privilégiés, qui traitent les requêtes
- php-fpm qui tourne sous root, reçoit les requêtes vers des scripts PHP de la part de nginx et les répartit vers des workers moins privilégiés (qui, sous Debian, s'exécutent sous l'utilisateur
www-data
).
Ça fait beaucoup trop de choses qui tournent avec les droits root. Systemd, c'est inévitable. Mais:
- php-fpm il n'y a aucune raison
- nginx a simplement besoin de pouvoir écouter sur le port 443 (port privilégié). On peut confier cette tâche à systemd et ainsi réduire les droits de nginx.
On peut donc configurer tout ça de façon :
- à exécuter php-fpm et nginx sans les droits root grâce à l'activation par socket
- à isoler les différentes applications PHP qui s'exécutent grâce aux template units de systemd
- à bloquer certaines escalades de privilège vers
root
(Baron Samedit, PwnKit, PHP-FPM local root vulnerability) et empêcher l'exploitation de failles de type SSRF
Les fichiers de configuration
Je reprends ici la configuration correspondant à Debian bullseye (stable en février 2022). Il faudra sans doute adapter les chemins/exécutables ou versions à votre système.
PHP
-
/etc/systemd/system/php-fpm@.service
:
[Unit]
Description=The PHP 7.4 FastCGI Process Manager for %i
Documentation=man:php-fpm7.4(8)
After=network.target php-fpm@%i.socket
# On a besoin de php-fpm@%i.socket pour ouvrir les ports et les envoyer à PHP-FPM
Requires=php-fpm@%i.socket
[Service]
Type=notify
Environment="FPM_SOCKETS=/run/php/%i.socket=3"
ExecStart=/usr/sbin/php-fpm7.4 --nodaemonize --fpm-config /etc/php/7.4/fpm/%i.conf
ExecReload=/bin/kill -USR2 $MAINPID
StateDirectory=%i
RuntimeDirectory=%i
LogsDirectory=%i
SupplementaryGroups=%i
# Options de durcissement
DynamicUser=true
PrivateUsers=true
ProtectSystem=strict
PrivateTmp=true
PrivateNetwork=true
NoNewPrivileges=true
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
SystemCallFilter=@system-service
SystemCallFilter=~@resources @privileged
CapabilityBoundingSet=
MemoryDenyWriteExecute=true
UMask=0077
ProtectHome=true
PrivateDevices=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectKernelLogs=true
SystemCallArchitectures=native
RestrictNamespaces=true
LockPersonality=true
RestrictRealtime=true
RemoveIPC=true
ProtectHostname=true
ProtectClock=true
ProtectProc=invisible
ProcSubset=pid
RestrictSUIDSGID=true
-
/etc/systemd/system/php-fpm@.socket
:
[Unit]
PartOf=php-fpm@.service
Documentation=https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/#php-fpm
[Socket]
ListenStream=/run/php/%i.socket
SocketMode=0660
SocketUser=php
SocketGroup=www-data
[Install]
WantedBy=sockets.target
-
/etc/php/7.4/fpm/mon-application.conf
(pour chaque application PHP que vous souhaitez isoler) :
[global]
pid = /run/mon-application/pid
error_log = /var/log/mon-application/error.log
[www]
listen = /run/php/mon-application.socket
access.log = /var/log/mon-application/access.log
php_admin_value[session.save_path] = /var/lib/mon-application
php_admin_value[pcre.jit] = 0
; Ci-dessous sont les options proposées par Debian par défaut pour un worker php
; Je vous renvoie à la documentation pour plus d'info : https://www.php.net/manual/fr/install.fpm.configuration.php
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
Nginx
-
/etc/systemd/system/nginx.service
:
[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
After=network.target nss-lookup.target nginx.socket
# On a besoin de nginx.socket pour ouvrir les ports et les envoyer à nginx
Requires=nginx.socket
[Service]
PIDFile=/run/nginx/pid
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx/pid
TimeoutStopSec=5
Environment=NGINX=3:4:5:6:
NonBlocking=true
# dans la ligne suivante : pensez à ajouter toute application pour laquelle vous auriez créé un groupe
SupplementaryGroups=ssl-cert mon-application
# Options de durcissement
User=www-data
PrivateUsers=true
LogsDirectory=nginx
ProtectSystem=strict
RuntimeDirectory=nginx
ReadOnlyPaths=/etc/certificats/actif/
PrivateTmp=true
NoNewPrivileges=true
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
PrivateNetwork=true
SystemCallFilter=@system-service
SystemCallFilter=~@resources @privileged
CapabilityBoundingSet=
MemoryDenyWriteExecute=true
UMask=0077
ProtectHome=true
PrivateDevices=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectKernelLogs=true
SystemCallArchitectures=native
RestrictNamespaces=true
LockPersonality=true
RestrictRealtime=true
RemoveIPC=true
ProtectHostname=true
ProtectClock=true
ProtectProc=invisible
ProcSubset=pid
RestrictSUIDSGID=true
[Install]
WantedBy=multi-user.target
-
/etc/systemd/system/nginx.socket
:
[Unit]
PartOf=nginx.service
Documentation=https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/
[Socket]
ListenStream=80
ListenStream=0.0.0.0:80
ListenStream=443
ListenStream=0.0.0.0:443
BindIPv6Only=ipv6-only
-
Dans /etc/nginx/nginx.conf
, il faut :
- retirer la directive
user
- modifier la directive
pid
pour indiquer un fichier sous /run/nginx
- adapter les directives
fastcgi_pass
pour pointer sur les adresses du type unix:/run/php/mon-application.socket
Exemple d'un fichier de configuration minimal :
pid /run/nginx/pid;
events {
}
http {
include mime.types;
# Serveur HTTP (sans chiffrement)
server {
listen [::]:80 default_server;
listen 80 default_server;
server_name _;
return 404;
}
# Afficher les statistiques nginx pour les requêtes depuis localhost et ciblant 127.0.1.1 :
server {
listen 127.0.1.1:80;
server_name _;
stub_status;
}
# Serveur HTTP redirigeant vers HTTPS
server {
listen [::]:80;
listen 80;
server_name mon-application.example.com;
# On redirige vers la version https
location / {
return 302 https://$host$request_uri;
}
}
# Serveur HTTPS (avec chiffrement TLS)
server {
listen [::]:443 ssl default_server;
listen 443 ssl default_server;
server_name mon-application.example.com;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
ssl_protocols TLSv1.3;
index index.php index.html;
root /var/www/mon-application/public/;
location ~ \.php$ {
include fastcgi.conf;
fastcgi_pass unix:/run/php/mon-application.socket;
}
}
}
Installation
Les commandes suivantes sont à lancer avec root
ou via sudo
systemctl disable --now php7.4-fpm.service # Désactivation des anciens processus si nécessaire
systemctl daemon-reload # Prise en compte des nouveaux fichiers systemd
# pour chaque application PHP installée :
groupadd --system mon-application
chown -R root:mon-application /var/www/mon-application
find /var/www/mon-application -type f -exec chmod 640 {} + -o -type d -exec echo chmod 750 {} +
systemctl enable --now php-fpm@mon-application.socket
# Pour chaque clé/certificat TLS utilisé par nginx :
chgrp ssl-cert /etc/xxx/yyyy/mes-certificats.key
chmod 640 /etc/xxx/yyyy/mes-certificats.key
systemctl restart nginx
Activation par socket
Systemd peut se placer en écoute sur des ports réseau, des sockets ou des files FIFO puis transférer les flux entrants aux programmes correspondants, pour peu que ceux-ci soient configurés pour : ils reçoivent alors ces sockets sous forme de file descriptors
numérotés à partir de 3 (après STDIN, STDOUT et STDERR). Il est possible de ne démarrer le service voulu que lorsqu'un premier message arrive sur la socket en question, c'est même la motivation initiale de cet outil.
Nginx peut être configuré pour récupérer ces sockets grâce à la variable d'environnement (non documentée) NGINX
. La seule façon dont j'ai réussi à faire fonctionner nginx de cette façon a été d'indiquer la valeur 3:4:5:6:
(info glanée sur la page DaemonSocketActivation for nginx and php-fpm).
PHP-FPM peut aussi être configuré pour récupérer ces sockets grâce à une variable d'environnement non documentée : FPM_SOCKETS
. Elle est de la forme : /run/php/app.socket=3
ou /run/php/pool1.socket=3,/run/php/pool2.socket=4,/run/php/pool3.socket=5
si on a plusieurs pool
de workers PHP orchestrés par FPM (mais s'il s'agit de séparer plusieurs applications, il vaut mieux utiliser des services systemd différents, cf. plus bas).
Confiner les services avec systemd
Directives de confinement
Systemd met à disposition de nombreuses options pour confiner les services :
-
User, Group : le service est lancé avec les privilèges de l'utilisateur et/ou du groupe indiqué. Cela évite de lancer avec les droits de l'utilisateur
root
tout-puissant (ce qui est fait par défaut pour nginx et php-fpm). Il faut alors s'assurer que l'utilisateur indiqué a bien accès aux ressources nécessaires (fichiers PHP pour php-fpm, fichiers statiques servis par nginx, etc.)
-
DynamicUser : le service est lancé sous un utilisateur créé pour ça, et détruit lorsque le service s'éteint. Il est possible de donner des droit sur des répertoires particuliers via l'option SupplementaryGroups
-
PrivateNetwork, PrivateUsers, PrivateIPC, ProtectHostname, PrivateMounts : place le service dans des namespaces isolés du reste du système, reprenant ainsi les outils d'isolation utilisés par les techologies de conteneurisation (Docker, LXC). Si on souhaite ajouter une cage "chroot" pour se rapprocher davantage d'un conteneur type docker, on peut utiliser les directives RootDirectory ou RootImage
-
PrivateTmp, ProtectHome, ProtectSystem : isole
/tmp
des autres applications, rend /home
et /root
inaccessibles et place l'ensemble du système en lecture seule. On peut relâcher ces contraintes en indiquant des répertoires qui doivent rester accessibles en lecture ou en écriture (ReadOnlyPaths,ReadWritePaths) ou plus simplement en indiquant quels répertoires sous /run
, /var/lib
, /var/cache
ou /var/log
sont utilisés (RuntimeDirectory, StateDirectory, CacheDirectory et LogsDirectory)
-
SystemCallFilter, CapabilityBoundingSet : limitent les appels système et les capabilities atteignables par le processus ou ses enfants.
-
IPAddressAllow, IPAddressDeny : filtrer les flux réseaux autorisés en entrée et en sortie. Les flux établis via les sockets associés au service ne sont pas concernés par ces restrictions.
… et tout un tas d'autres qu'il est fastidieux de lister ici. Je vous renvoie à Mastering systemd: Securing and sandboxing applications and services qui présente les options introduites dans RHEL 7 et 8. De nouvelles directives sont ajoutées au fil des versions de systemd.
Évaluer l'efficacité de ces mesures
La commande systemd-analyze security nginx.service
permet de lister les directives appliquées ou non et de calculer un score entre 0 (service fortement durci, dont la compromission aura un impact minimal sur le système en terme de sécurité) et 10 (service non durci, dont la compromission peut entraîner la compromission de tout le système). Sur une Debian stable, passer du fichier nginx.service
proposé par défaut à celui poposé ci-dessus fait descendre le score de 9,6 à 0,2.
Pour évaluer plus concrètement le confinement mis en place, on peut déployer un web shell PHP. Quelques résultats :
-
ls /home
: Error in Code Execution --> ls: cannot open directory '/home': Permission denied
-
ls -alh /run
: on constate que tous les fichiers sont détenus soit par root
, soit par nobody
(on ne "voit pas" les autres utilisateurs). Cependant, getent passwd
permet de retrouver la liste des comptes installés.
-
ls /tmp
: vide
-
ls /proc -alh
: on ne voit que les processus exécutés par php-mon-application.
-
nslookup linuxfr.org
: Error in Code Execution --> ;; connection timed out; no servers could be reached
-
sudo /bin/true
: Error in Code Execution --> sudo: effective uid is not 0, is sudo installed setuid root?
Relâcher les mesures de confinement
Certaines de ces mesures peuvent être trop drastiques pour les applications hébergées. Le blocage du réseau, par exemple, empêche nginx de récupérer ses réponses OCSP pour agrafage, ou peut l'empêcher de faire reverse proxy vers des applications sur d'autres machines ou sur la même machine mais accessibles uniquement via TCP/IP (directives proxy_pass http://127.0.0.1:xxxx/;
). Si votre base de données est hébergée sur le même serveur, l'application PHP peut sans doute s'y connecter par socket Unix, mais si ça n'est pas le cas elle aura besoin d'accéder au réseau. Elle peut aussi avoir besoin d'écrire dans tel ou tel répertoire. Enfin, il est possible que telle ou telle bibliothèque ait besoin d'appels systèmes indûment bloqués par le confinement trop strict imposé (c'est ainsi qu'on a dû désactiver le JIT pcre dans la configuration car incompatible avec la directive MemoryDenyWriteExecute).
Pour cela, vous pouvez modifier directement les fichiers .service
ci-dessus ou créer le dossier /etc/systemd/system/php-fpm@mon-application.service.d/
(par exemple) et y ajouter un fichier relachement.conf
:
[Service]
# Pour rendre le dossier 'upload' accessible en écriture, il faut ̀ chmod 660` le dossier et ajouter la ligne :
ReadWritePaths=/var/www/mon-application/upload
# Pour autoriser les flux réseau :
PrivateNetwork=false
RestrictAddressFamilies=
# Si on veut ouvrir à des requêtes sur localhost uniquement :
RestrictAddressFamilies=AF_UNIX AF_INET
IPAddressAllow=localhost
# Si on veut ouvrir à des requêtes sur tout internet :
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressAllow=any
Instances de services systemd
Les fichiers PHP de systemd ci-dessus contiennent un @
et on a indiqué %i
à plusieurs reprises : cela permet de créer plusieurs instances de services similaires.
Ainsi, si vous avez 3 applications différentes, vous pouvez créer autant de fichiers de configuration sous /etc/php/7.4/fpm/
et activer ces services indépendamment avec systemctl start|stop|reload|status php-fpm@applicationX.service
. Chaque service proposera sa propre socket sous /run/php
, il faudra configurer nginx en conséquence.
En restreignant les droits d'accès à /var/www/applicationX/
comme proposé ci-dessus (section installation) vous empêchez un attaquant qui compromettrait une application d'accéder aux informations liées à une autre des applications.
Considérations de sécurité
Sauf faille de sécurité supplémentaire, une fois ces mesures mises en place la compromission de nginx ou php-fpm ne pourra plus aboutir aux conséquences suivantes :
- installation d'applications arbitraires / rootkit
- accès aux clés SSH du serveur
- escalade de privilège vers
root
- exfiltration de données ou exécution de code via Server-Side Request Forgery
- accès direct aux données stockées sur le serveur
Néanmoins :
- l'attaquant qui compromet une application PHP déployée sur le serveur pourra toujours :
- voler les accès et les données d'un autre utilisateur
- accéder à la base de données sans filtre ou à toute donnée à laquelle l'application donne accès (il ne pourra pas accéder au code ou aux données d'une autre application hébergée sur la même machine)
- l'attaquant qui compromet nginx pourra inévitablement accéder aux clés TLS et potentiellement déchiffrer le traffic des autres usagers de l'application
Pour sécuriser davantage, il faut configurer un Linux Security Module comme apparmor ou SELinux.
Références
Commentaires :
voir le flux Atom
ouvrir dans le navigateur
LinuxFr.org : Journaux
La version 2.0 de WhosWho est sortie
- 15 mai -
Bonjour Nal,Je viens de publier la version 2.0 de WhosWho, mon logiciel pour faire des trombinoscopes, dont j'avais parlé il y a longtemps dans (...)
décrire une une image avec une iA locale
- 8 mai -
Aujourd'hui c'est fourien™, petit tuto sans prétention!Pour décrire des images en utilisant une iA localement j'utilise LLaVA qui fait partie de (...)
antistress adventure in Flatpak land
- 30 avril -
Hello nal, ça faisait un bail !Certain (il se reconnaîtra) m'a demandé de le tenir au courant lorsque j'aurai basculé sur un usage de Firefox (...)
Téléphone sous Linux ?
- 25 avril -
Aujourd'hui, avoir un téléphone avec un Android libéré, c'est possible, on pense en particulier à Murena.Avoir un téléphone sous GNU/Linux, c'est (...)
Quand votre voiture vous espionne… et vous le fait payer
- 23 avril -
Ceci se passe aux États-Unis, pour l’instant, aucune preuve qu’une telle fuite existe en Europe. Mais… si votre assurance augmente brutalement, (...)