Greboca  

Suport technique et veille technologique

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.

LinuxFr.org : les journaux  -  Durcir nginx et PHP avec systemd

 -  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

par Samuel

LinuxFr.org : les journaux

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, (...)