systemd : durcir un service avec systemd-analyze sans le casser
Un service Linux qui fonctionne n’est pas forcément un service correctement isolé. S’il est compromis, peut-il lire les dossiers personnels, modifier le système, charger un module noyau ou consommer toute la mémoire du serveur ? systemd permet de réduire cette surface d’attaque sans ajouter de conteneur, mais il faut avancer proprement.
L’objectif n’est pas d’obtenir un joli score en activant toutes les options. L’objectif est de retirer au processus ce dont il n’a pas besoin, tout en gardant un service testable, observable et réversible.
Commencer par observer, pas par modifier
Pour afficher l’unité réellement chargée, y compris ses éventuels fichiers de surcharge :
sudo systemctl cat monservice.service
sudo systemctl show monservice.service -p User -p Group -p ExecStart -p FragmentPath -p DropInPaths
Vérifiez ensuite la version de systemd. Certaines directives récentes ne sont pas disponibles sur les anciennes distributions :
systemd --version
Puis lancez l’analyse de sécurité :
sudo systemd-analyze security monservice.service
La commande examine les protections déclarées dans l’unité et attribue un niveau d’exposition. C’est un outil d’aide, pas un audit complet. Il ne lit pas le code de l’application, ne vérifie pas ses dépendances et ne prouve pas qu’une configuration est exploitable. Un service peut obtenir un mauvais score parce qu’il a réellement besoin d’un accès particulier ; l’inverse est également vrai.
Ne modifiez pas le fichier fourni par le paquet
Éditer directement un fichier dans /usr/lib/systemd/system ou /lib/systemd/system expose vos changements à un écrasement lors d’une mise à jour. Créez plutôt un drop-in :
sudo systemctl edit monservice.service
systemd placera normalement la surcharge dans /etc/systemd/system/monservice.service.d/override.conf. Avant de commencer, conservez le contenu de systemctl cat et repérez les autres drop-ins éventuels. Un retour arrière ne doit supprimer que le fichier que vous venez de créer, pas les réglages locaux appartenant à quelqu’un d’autre.
Palier 1 : exécuter avec une identité dédiée
Si l’application n’a pas besoin des privilèges administrateur, elle ne devrait pas tourner en root. Pour un compte système déjà créé :
[Service]
User=monservice
Group=monservice
UMask=0077
NoNewPrivileges=yes
NoNewPrivileges=yes empêche le processus et ses enfants d’obtenir de nouveaux privilèges par un nouvel appel à execve(), notamment via les bits setuid, setgid ou les capacités de fichiers. Ce réglage est très utile, mais il casse légitimement un programme qui compte sur une élévation après son démarrage.
UMask=0077 fait créer les nouveaux fichiers avec des permissions restrictives par défaut. Ne l’utilisez pas si le service doit produire des fichiers lisibles par un autre compte sans qu’un groupe ou des ACL aient été prévus.
Palier 2 : isoler les espaces temporaires et le noyau
Pour un service applicatif classique qui n’administre pas la machine, ce groupe constitue une bonne base à tester :
[Service]
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
PrivateTmp=yesfournit au service ses propres espaces/tmpet/var/tmp.PrivateDevices=yesréduit l’accès aux périphériques exposés dans/dev.ProtectKernelTunables=yesprotège notamment les réglages du noyau accessibles par/procet/sys.ProtectKernelModules=yesempêche le service de charger ou décharger des modules noyau.ProtectControlGroups=yesrend la hiérarchie des groupes de contrôle inaccessible en écriture.RestrictSUIDSGID=yesinterdit la création de fichiers dotés des bits setuid ou setgid.
Ces protections ne conviennent pas à tous les démons. Un service de supervision matérielle, un moteur de conteneurs, un VPN ou un outil qui pilote le système peut avoir besoin d’une partie de ces accès.
Palier 3 : rendre le système de fichiers presque entièrement en lecture seule
ProtectSystem=strict rend pratiquement toute la hiérarchie de fichiers en lecture seule pour le service, à l’exception des pseudo-systèmes comme /dev, /proc et /sys, qui disposent de protections séparées. Il faut ensuite déclarer précisément les emplacements où l’écriture reste autorisée.
[Service]
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/monservice
ReadWritePaths=/var/log/monservice
ReadWritePaths=/run/monservice
ProtectHome=yes rend /home, /root et /run/user inaccessibles. Si l’application doit seulement lire un fichier précis dans un répertoire personnel, mieux vaut revoir l’emplacement de ce fichier plutôt que d’ouvrir tout le dossier.
Pour une unité que vous maîtrisez, systemd peut aussi créer les répertoires avec le bon propriétaire :
[Service]
StateDirectory=monservice
LogsDirectory=monservice
RuntimeDirectory=monservice
CacheDirectory=monservice
Ces directives correspondent respectivement à des emplacements gérés sous /var/lib, /var/log, /run et /var/cache. Elles évitent les scripts de démarrage qui créent des dossiers à la main, mais il faut vérifier les chemins réellement attendus par l’application.
Palier 4 : limiter capacités, réseau et architectures
Une application Web ordinaire n’a généralement pas besoin des dizaines de capacités offertes à root. Si elle tourne déjà avec un compte non privilégié et n’a besoin d’aucune capacité particulière :
[Service]
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
Une valeur vide pour CapabilityBoundingSet= retire toutes les capacités. Si le service écoute directement sur un port privilégié ou réalise une opération système spécifique, cette ligne peut l’empêcher de démarrer. Dans ce cas, accordez uniquement la capacité nécessaire, par exemple CAP_NET_BIND_SERVICE, ou placez un reverse proxy devant l’application.
RestrictAddressFamilies= doit refléter les sockets réellement utilisés. Une application peut avoir besoin de AF_UNIX pour communiquer localement, de AF_INET pour IPv4 et de AF_INET6 pour IPv6. N’ajoutez pas une famille “par habitude” et ne retirez pas IPv6 si l’application ou le résolveur en dépend.
Les filtres d’appels système avec SystemCallFilter= peuvent aller plus loin, mais ils sont très dépendants du langage, du runtime et des bibliothèques. Une liste générique copiée depuis un blog peut casser le service après une simple mise à jour. Commencez par les protections de fichiers, d’identité et de capacités, puis filtrez les appels système uniquement avec des tests représentatifs.
Palier 5 : contenir un emballement de ressources
Le durcissement concerne aussi la disponibilité. Un processus qui boucle ou fuit en mémoire ne devrait pas emporter tout le serveur avec lui :
[Service]
MemoryHigh=512M
MemoryMax=768M
TasksMax=128
La documentation systemd recommande d’utiliser MemoryHigh= comme mécanisme principal de contrôle et MemoryMax= comme dernière ligne de défense. Si l’utilisation ne peut pas rester sous MemoryMax=, le mécanisme de manque de mémoire intervient dans l’unité. Une limite trop basse ressemble donc à une panne aléatoire alors qu’elle est provoquée par votre configuration.
TasksMax= limite le nombre de tâches dans le groupe de contrôle du service. Comptez les processus et les threads en charge réelle avant de choisir une valeur. Une application Java, un navigateur automatisé ou un outil de traitement parallèle peut dépasser rapidement une limite choisie au hasard.
La méthode de test qui évite le verrouillage
Appliquez un seul palier à la fois et gardez une seconde session SSH ouverte. Après chaque modification :
sudo systemctl daemon-reload
sudo systemctl restart monservice.service
sudo systemctl status monservice.service --no-pager
sudo journalctl -u monservice.service -b -n 100 --no-pager
sudo systemd-analyze security monservice.service
Ne vous contentez pas d’un statut “active”. Testez les fonctions réelles : écriture en base, envoi de courriel, résolution DNS, accès au réseau, rotation des journaux, traitement d’un fichier, redémarrage et mise à jour. Une protection peut laisser le processus démarrer tout en cassant une fonction rarement utilisée.
Si le service échoue, lisez d’abord le journal. Un message “Read-only file system”, “Permission denied” ou “Operation not permitted” indique souvent quelle protection bloque l’action. Retirez ou ajustez la dernière directive ajoutée, rechargez systemd et retestez.
Exemple de profil raisonnable pour une petite API
Voici un point de départ, pas une recette universelle :
[Service]
User=monapi
Group=monapi
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
StateDirectory=monapi
LogsDirectory=monapi
RuntimeDirectory=monapi
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
MemoryHigh=512M
MemoryMax=768M
TasksMax=128
Avant de l’utiliser, adaptez le compte, les répertoires, les besoins réseau et les limites à l’application. Si elle est fournie par un paquet, lisez son unité d’origine et sa documentation : l’éditeur a peut-être déjà prévu des protections ou des chemins spécifiques.
En clair
Le bon durcissement systemd n’est pas celui qui active le plus de lignes. C’est celui qui décrit précisément ce que le service a le droit de faire, et rien de plus. Utilisez systemd-analyze security pour trouver des pistes, créez un drop-in, avancez par paliers et testez le comportement réel après chaque changement.
La sécurité gagne ici par réduction progressive : un compte non privilégié, un système de fichiers en lecture seule, quelques chemins d’écriture explicites, des capacités supprimées et des limites de ressources réalistes. C’est moins spectaculaire qu’un “profil ultra-hardened” copié-collé, mais beaucoup plus fiable en production.
