URL: https://linuxfr.org/users/samuel/journaux/durcir-nginx-et-php-avec-systemd Title: Durcir nginx et PHP avec systemd Authors: Samuel Date: 2022年02月03日T12:00:49+01:00 License: CC By-SA Tags: sécurité, systemd, nginx, php, apparmor, selinux et serveur_web Score: 56 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](https://blog.qualys.com/vulnerabilities-threat-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit), [PwnKit](https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034), [PHP-FPM local root vulnerability](https://www.ambionics.io/blog/php-fpm-local-root)) et empêcher l'exploitation de failles de type [SSRF](https://portswigger.net/web-security/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](http://0pointer.de/blog/projects/socket-activation.html). 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](https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/)). 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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#User=) : 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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SupplementaryGroups=) - [PrivateNetwork](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork=), [PrivateUsers](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateUsers=), [PrivateIPC](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateIPC=), [ProtectHostname](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHostname=), [PrivateMounts](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateMounts=) : place le service dans des [namespaces](https://en.wikipedia.org/wiki/Linux_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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RootDirectory=) ou [RootImage](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RootImage=) - [PrivateTmp](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp=), [ProtectHome](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=), [ProtectSystem](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#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](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RuntimeDirectory=)) - [SystemCallFilter](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=), [CapabilityBoundingSet](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#CapabilityBoundingSet=) : limitent les appels système et les capabilities atteignables par le processus ou ses enfants. - [IPAddressAllow, IPAddressDeny](https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#IPAddressAllow=ADDRESS%5B/PREFIXLENGTH%5D%E2%80%A6) : 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](https://www.redhat.com/sysadmin/mastering-systemd) 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](https://github.com/rainbow-unicorns-ctf/php-web-shell/blob/fix_input_id/php-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](https://fr.wikipedia.org/wiki/Agrafage_OCSP), 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](https://www.php.net/manual/fr/pcre.configuration.php#ini.pcre.jit) dans la configuration car incompatible avec la directive [MemoryDenyWriteExecute](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#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](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Description). 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](https://portswigger.net/web-security/ssrf) - 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](https://fr.wikipedia.org/wiki/AppArmor) ou [SELinux](https://fr.wikipedia.org/wiki/SELinux). # Références - [Socket activation part 1](http://0pointer.de/blog/projects/socket-activation.html) et [Socket activation part 2](http://0pointer.de/blog/projects/socket-activation2.html) - [DaemonSocketActivation for nginx and php-fpm](https://freedesktop.org/wiki/Software/systemd/DaemonSocketActivation/) - [Mastering systemd: Securing and sandboxing applications and services](https://www.redhat.com/sysadmin/mastering-systemd) - [man systemd.socket](https://www.freedesktop.org/software/systemd/man/systemd.socket.html) - [man systemd.exec](https://www.freedesktop.org/software/systemd/man/systemd.exec.html) - [man systemd.resource-control](https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html) - [man systemd.unit](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) - [documentation: configuration FasctCGI Process Manager](https://www.php.net/manual/fr/install.fpm.configuration.php) - [nginx ticket#237: Add optional systemd socket activation support](https://trac.nginx.org/nginx/ticket/237#comment:15) - [traitement de l'option FPM_SOCKETS dans le code source de PHP](https://github.com/php/php-src/blob/d528d29ed4e5833f9b3e696bcb030c6d81c1c523/sapi/fpm/fpm/fpm_sockets.c#L395) - [What is SSRF ?](https://portswigger.net/web-security/ssrf) - [PHP-FPM local root vulnerability](https://www.ambionics.io/blog/php-fpm-local-root) - [PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec (CVE-2021-4034)](https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034) - [CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit)](https://blog.qualys.com/vulnerabilities-threat-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit)

AltStyle によって変換されたページ (->オリジナル) /