systemd-analyze security est la commande que je lance avant de modifier quoi que ce soit dans une unité systemd. Elle attribue à chaque service une note d’exposition, liste les options de sandboxing que l’unité n’utilise pas encore et indique par où commencer. Cet article est la partie 1 d’une série de huit sur le durcissement de systemd, directive par directive. Je couvre ici la méthode : comment fonctionnent les drop-ins, comment lire la sortie de systemd-analyze security, comment tester une modification sans risque et quels services traiter en premier.
Les parties suivantes passent en revue les directives, une famille à la fois. Pour chaque directive, j’explique ce qu’elle fait, quels services de ma stack elle touche et ce qu’elle peut casser. Cette première partie reste sur la méthode, parce que toutes les suivantes s’appuient dessus. Aucune expérience de systemd n’est nécessaire : chaque étape vient avec la commande complète, un commentaire sur chaque ligne et un petit lab sur un service de démonstration inoffensif.
Banc de test. Toutes les sorties de cet article viennent d’exécutions réelles sur Rocky Linux 10.2 avec systemd 257, dans une machine virtuelle WSL2 (noyau 6.18.54.1-microsoft-standard-WSL2, session root). SELinux y est désactivé et certaines fonctions du noyau peuvent différer d’un serveur classique. Je le signale à chaque endroit où cela compte. Quand je coupe des lignes d’une sortie, je les remplace par [...], et je marque non testées les commandes que je n’ai pas lancées.
Avant de commencer
Lisez d’abord cette courte liste. Elle vous évite l’erreur classique : durcir un service de production du premier coup.
- Une machine de test. Utilisez une machine virtuelle ou un serveur de rechange sous systemd. J’ai tout exécuté sur Rocky Linux 10.2 uniquement ; ces commandes appartiennent à systemd lui-même et devraient se comporter de la même façon sur d’autres distributions, mais je ne l’ai pas testé.
- Un shell avec les droits
sudo. Chaque commande qui modifie le système commence parsudo. - Un service inoffensif pour vos premiers essais. Le lab ci-dessous crée un service de démonstration nommé
demo-hardening-sandbox.service: vous ne touchez donc à aucun vrai service. - Un moyen de tout annuler. Chaque changement vit dans un seul fichier drop-in, et supprimer ce fichier annule le changement. Le lab se termine par une étape de nettoyage qui supprime tout ce qu’il a créé.
- Un systemd récent. Plusieurs options de systemd-analyze security sont apparues dans la version 250 : vérifiez donc la vôtre avec
systemctl --version.
Un court glossaire
| Terme | Sens dans cet article |
|---|---|
| Unité | Un fichier qui décrit ce que systemd gère. Une unité de type service se termine par .service. |
| Service | Une unité qui lance un programme : un démon qui reste actif, ou une commande qui s’exécute une fois puis se termine. |
| Drop-in | Un petit fichier .conf placé dans un répertoire .d, qui modifie une unité sans toucher au fichier d’origine. |
| Directive | Un réglage d’un fichier d’unité, par exemple ProtectSystem=strict. |
| Sandboxing | Les directives qui limitent ce qu’un service peut voir et faire. |
| Namespace | Une fonction du noyau qui donne à un processus sa propre vue du système, par exemple ses propres points de montage. Plusieurs directives s’appuient dessus. |
| Capability | Une part des pouvoirs de root, par exemple ouvrir un port bas, que le noyau peut accorder séparément. |
| Appel système | Une requête d’un programme au noyau, par exemple ouvrir un fichier. |
| Niveau d’exposition | La note que systemd-analyze security calcule pour une unité. |
| Unité transitoire | Une unité temporaire que systemd-run crée à la volée ; elle disparaît quand elle s’arrête. |
En un coup d’œil
| Étape | Commande | Ce qu’elle vous apprend |
|---|---|---|
| Noter tous les services actifs | systemd-analyze security | Une ligne par service chargé, avec un niveau d’exposition |
| Détailler un service | systemd-analyze security UNITÉ | Chaque réglage, appliqué (✓) ou non (✗), avec son exposition |
| Contrôler un fichier avant de le déployer | systemd-analyze verify FICHIER puis security --offline=true FICHIER | Les erreurs de syntaxe, puis la revue hors ligne |
| Changer une directive | systemctl edit UNITÉ | Un fichier drop-in, rechargé à l’enregistrement |
| Expérimenter sans modifier de fichier | systemd-run -p DIRECTIVE=VALEUR ... | Une unité temporaire avec les propriétés de votre choix |
| Lire un échec | systemctl status UNITÉ et journalctl -u UNITÉ | Le code de sortie, puis les messages du service lui-même |
| Revenir en arrière | systemctl revert UNITÉ | Supprime les drop-ins de l’unité |
Pourquoi durcir les services avec systemd
Un service qui n’a aucune directive de sandboxing tourne sans aucune de ces protections. Le manuel systemd.exec qualifie les options de sandboxing de moyen efficace de limiter l’exposition du système, et recommande d’en activer autant que possible sans empêcher le processus de fonctionner. Mon service de démonstration montre le point de départ : sans aucun sandboxing, systemd-analyze security lui attribue 9,6 et l’étiquette UNSAFE, avec 75 réglages non appliqués et 4 appliqués (vous le mesurerez vous-même dans le lab).
Le sandboxing ne remplace pas les autres couches de défense. En revanche, il en ajoute une qui ne coûte presque rien : quelques lignes dans un drop-in. Deux limites du manuel valent d’être retenues. D’abord, beaucoup d’options sont désactivées sans bruit sur les systèmes où le mécanisme sous-jacent n’existe pas. Ensuite, certaines fonctions de sandboxing ne sont en général pas disponibles dans les services utilisateur.
Comment une unité et ses drop-ins s’assemblent
Un drop-in est la façon la plus sûre de modifier un service. Au lieu d’éditer le fichier installé par un paquet, vous ajoutez un petit fichier à côté, et systemd fusionne les deux. Le schéma montre quel fichier l’emporte.

Où vivent les drop-ins et lequel gagne
Pour une unité nommée foo.service, systemd cherche un répertoire foo.service.d/. Il ne lit que les fichiers qui se terminent par .conf et les fusionne par ordre alphanumérique après avoir lu le fichier principal de l’unité. Chaque drop-in doit contenir son propre en-tête de section, par exemple [Service].
| Emplacement | Rôle | Priorité |
|---|---|---|
/usr/lib/systemd/system/ | Unités et drop-ins fournis par les paquets | La plus basse |
/run/systemd/system/ | Changements d’exécution, perdus au redémarrage | Intermédiaire |
/etc/systemd/system/ | Vos propres unités et drop-ins | La plus haute |
Le manuel ajoute trois règles. Les drop-ins de /etc/ l’emportent sur ceux de /run/, qui l’emportent sur ceux de /usr/lib/. Les drop-ins l’emportent sur les fichiers d’unité, où qu’ils se trouvent. À niveau égal, les fichiers de noms différents s’appliquent dans l’ordre lexicographique. Cette dernière règle explique pourquoi je donne à mes fichiers un nom qui indique leur rôle, comme hardening.conf.
Créer, vérifier et annuler un drop-in
Le lab ci-dessous lance les vraies commandes dans l’ordre, avec leur sortie : systemctl edit pour créer un drop-in, systemctl cat pour voir ce que systemd lit, systemctl show pour voir ce qu’il a chargé, et systemctl revert pour l’annuler. Un point mérite l’attention. systemctl edit recharge la configuration à votre place, et un rechargement conserve l’état d’exécution. Un service déjà lancé garde donc son ancien sandbox jusqu’à son redémarrage. L’erreur 4 du lab le démontre.
Lire la sortie de systemd-analyze security
L’outil systemd-analyze security analyse les réglages de sécurité et de sandboxing des unités de service. Il ne démarre rien. Il lit les réglages, puis les compare aux protections que systemd sait appliquer.

Un service ou tout le système
Sans nom d’unité, systemd-analyze security inspecte tous les services chargés qui tournent durablement et affiche un tableau concis, une ligne par unité. Avec un ou plusieurs noms d’unité, il affiche une analyse détaillée de chacune. Voici le tableau de tout le système, coupé après quatre lignes :
systemd-analyze security --no-pager
UNIT EXPOSURE PREDICATE HAPPY NetworkManager.service 7.8 EXPOSED 🙁 auditd.service 9.4 UNSAFE 😨 cockpit-wsinstance-http.service 5.4 MEDIUM 😐 cockpit.service 6.2 MEDIUM 😐 [...]
Les colonnes sont l’unité, son niveau d’exposition, une étiquette (le « predicate ») et un visage. Sur le banc de test, j’ai vu les étiquettes MEDIUM, EXPOSED et UNSAFE ; le manuel ne les définit pas. Fiez-vous au nombre, et voyez l’étiquette comme un repère visuel rapide.
Le tableau, ligne par ligne
Pour un service, la sortie a trois colonnes : NAME, DESCRIPTION et EXPOSURE. Chaque ligne commence par une marque : ✗ signifie que le réglage n’est pas appliqué, ✓ qu’il l’est. La dernière ligne donne le résultat global. Voici le début du tableau pour le service de démonstration du lab, avant tout sandboxing :
systemd-analyze security --no-pager demo-hardening-sandbox.service | head -n 6
NAME DESCRIPTION EXPOSURE ✗ RootDirectory=/RootImage= Service runs within the host's root directory 0.1 SupplementaryGroups= Service runs as root, option does not matter RemoveIPC= Service runs as root, option does not apply ✗ User=/DynamicUser= Service runs as root user 0.4 ✗ CapabilityBoundingSet=~CAP_SYS_TIME Service processes may change the system clock 0.2
Lisez-le comme une liste de tâches. Chaque ligne ✗ est une protection que vous pouvez encore ajouter, et le nombre à droite indique l’exposition qu’elle retirerait. Les lignes sans marque et sans nombre ne s’appliquent pas à cette unité : l’outil vous dit par exemple que RemoveIPC= n’a pas d’importance quand le service tourne en root. Pour ne garder que le travail restant, filtrez la sortie :
systemd-analyze security --no-pager demo-hardening-sandbox.service | grep '✗' | head -n 4
✗ RootDirectory=/RootImage= Service runs within the host's root directory 0.1 ✗ User=/DynamicUser= Service runs as root user 0.4 ✗ CapabilityBoundingSet=~CAP_SYS_TIME Service processes may change the system clock 0.2 ✗ NoNewPrivileges= Service processes may acquire new privileges 0.2
Ce que la note signifie, et ce qu’elle ne signifie pas
Le manuel décrit le niveau d’exposition global comme une estimation de 0,0 à 10,0. Une note élevée signale peu de sandboxing, une note basse des restrictions serrées. Soyez prudent avec ce nombre, car le manuel est explicite sur ses limites :
- Il ne couvre que les fonctions de sécurité par service que systemd implémente lui-même. Il ignore ce que le programme fait pour se protéger, et il ignore SELinux.
- Une note élevée ne veut pas dire que le service est vulnérable, et une note basse ne prouve pas qu’il est sûr. Elle indique seulement que le service pourrait profiter de réglages supplémentaires.
- Beaucoup de réglages peuvent être annulés individuellement par d’autres réglages. L’outil tient compte de certaines combinaisons, pas de toutes.
- Les réglages s’appliquent aux opérations propres du service. Les requêtes envoyées par IPC, comme D-Bus, peuvent atteindre des services qui ne sont pas restreints de la même façon : vérifiez donc séparément la politique d’accès IPC.
En pratique, traitez la note de systemd-analyze security comme une boussole, pas comme un objectif. En effet, elle aide à repérer les services sans aucune protection et confirme qu’un changement a produit l’effet voulu.
Les options à connaître
| Option | Ce qu’elle fait | Testée ici |
|---|---|---|
--offline=BOOL | Passe en revue des fichiers d’unité sans interroger le gestionnaire en cours d’exécution (ajoutée en version 250) | Oui : fonctionne avec un chemin de fichier et avec un nom d’unité |
--threshold=NOMBRE | Renvoie une erreur quand le niveau global dépasse la valeur (ajoutée en 250) | Oui : code de sortie 1 au-dessus du seuil, 0 en dessous |
--json=MODE | Affiche le tableau en JSON : off, pretty ou short | Oui : short |
--security-policy=CHEMIN | Applique vos propres exigences en JSON ; un poids de 0 signifie « ne pas contrôler » (ajoutée en 250) | Non testée |
--profile=CHEMIN | Avec --offline, prend en compte un profil portable (ajoutée en 250) | Non testée |
--no-pager | Affiche directement, sans pagineur | Oui |
Voici les résultats réels pour le service de démonstration une fois le drop-in du lab en place (sa note est 8,9, comme le montre l’étape 3 du lab). J’ai lancé deux fois --offline, sur le chemin du fichier puis sur le nom de l’unité :
systemd-analyze security --offline=true --no-pager /etc/systemd/system/demo-hardening-sandbox.service | tail -n 1 systemd-analyze security --offline=true --no-pager demo-hardening-sandbox.service | tail -n 1
→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁 → Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁
J’ai ensuite lancé --threshold deux fois pour vérifier le code de sortie :
systemd-analyze security --threshold=3 --no-pager demo-hardening-sandbox.service | tail -n 1; echo "exit=${PIPESTATUS[0]}"
systemd-analyze security --threshold=100 --no-pager demo-hardening-sandbox.service | tail -n 1; echo "exit=${PIPESTATUS[0]}"
→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁 exit=1 → Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁 exit=0
Le mode JSON donne un objet par ligne, avec les champs set, name, json_field, description et exposure. Voici les 330 premiers caractères :
systemd-analyze security --json=short --no-pager demo-hardening-sandbox.service | head -c 330
[{"set":false,"name":"RootDirectory=/RootImage=","json_field":"RootDirectoryOrRootImage","description":"Service runs within the host's root directory","exposure":"0.1"},{"set":null,"name":"SupplementaryGroups=","json_field":"SupplementaryGroups","description":"Service runs as root, option does not matter","exposure":null},{"set"
[...]
Un détail mérite un avertissement. Le manuel dit que le niveau global va de 0,0 à 10,0, mais il documente un seuil par défaut de 100, et précise que --offline renvoie une erreur au-dessus de ce seuil. Le seuil par défaut ne se déclenche donc jamais. Si vous utilisez --threshold dans un script, passez une valeur explicite et testez les deux côtés du seuil, comme je l’ai fait ci-dessus.
Contrôler une unité avant de la déployer
Deux commandes vous aident avant qu’un fichier n’entre dans /etc/systemd/system/. systemd-analyze verify détecte les sections et directives inconnues, les dépendances manquantes, les pages de manuel citées dans Documentation= qui n’existent pas, et les commandes de ExecStart= introuvables ou non exécutables. Ensuite, le mode hors ligne de systemd-analyze security (--offline=true) passe le fichier en revue sans interroger le gestionnaire en cours d’exécution. Voici une exécution propre sur l’unité du lab :
systemd-analyze verify /etc/systemd/system/demo-hardening-sandbox.service; echo "exit=$?"
exit=0
L’absence de message et le code 0 signifient que verify n’a rien à signaler. Cependant, le manuel ne donne aucun exemple de revue hors ligne d’un chemin de fichier. Cela fonctionne sur mon banc de test, mais essayez-le sur une copie avant de vous y fier.
Une boucle de durcissement sûre
Le durcissement tourne mal quand vous changez cinq choses à la fois et ne savez plus laquelle a cassé le service. La boucle ci-dessous l’évite. Elle commence par systemd-analyze security, puis ajoute une directive à la fois, et elle se termine par un test du vrai travail du service.

- Mesurer. Lancez
systemd-analyze security UNITÉet notez les lignes ✗ que vous comptez corriger. - Changer une directive dans un drop-in avec
sudo systemctl edit UNITÉ. - Redémarrer avec
sudo systemctl restart UNITÉ. Un simple rechargement ne donne pas son nouveau sandbox à un service déjà lancé. - Lire le résultat avec
systemctl status UNITÉetjournalctl -u UNITÉ. - Tester le vrai travail : se connecter, envoyer un fichier, lancer la sauvegarde, envoyer un courrier. Un service peut démarrer et échouer plus tard.
- Garder ou annuler. Gardez le drop-in dans votre gestion de configuration, ou supprimez-le, lancez
sudo systemctl daemon-reloadet redémarrez.
Les deux dernières étapes comptent le plus. Un sandbox peut laisser un service démarrer alors que l’une de ses tâches échoue plus tard, par exemple parce qu’elle a besoin d’un chemin désormais en lecture seule. C’est pourquoi la boucle se termine par un test du vrai travail, et non par un démarrage réussi.
Lire un échec
Quand un sandbox casse un service, le code de sortie et le message du service vous indiquent la directive à assouplir. Voici une unité de démonstration qui tourne avec ProtectSystem=strict et tente de créer un fichier dans /etc. La sortie de status est coupée après le résumé, car les lignes du journal qui suivent portent le nom de ma machine de test :
sudo systemctl start demo-hardening-fail.service systemctl status --no-pager -n 5 demo-hardening-fail.service
× demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless)
Loaded: loaded (/etc/systemd/system/demo-hardening-fail.service; static)
Active: failed (Result: exit-code) since Mon 2026-11-02 14:54:40 CET; 35ms ago
Invocation: 55376c422cb741a69786c3968cc455a2
Process: 2008 ExecStart=/bin/touch /etc/demo-hardening-fail (code=exited, status=1/FAILURE)
Main PID: 2008 (code=exited, status=1/FAILURE)
Mem peak: 1.2M
CPU: 5ms
[...]
Le journal montre le message du programme lui-même. -u sélectionne l’unité, -o cat n’affiche que le message et -n 6 limite le nombre de lignes :
journalctl -u demo-hardening-fail.service -o cat --no-pager -n 6
Starting demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless)... /bin/touch: cannot touch '/etc/demo-hardening-fail': Read-only file system demo-hardening-fail.service: Main process exited, code=exited, status=1/FAILURE demo-hardening-fail.service: Failed with result 'exit-code'. Failed to start demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless).
Le message dit Read-only file system : la directive responsable est donc ProtectSystem=. La solution est d’autoriser le chemin dont le programme a vraiment besoin avec ReadWritePaths=, que la partie 2 de la série détaille. Les messages de systemd restent en anglais sur le banc de test.
Lab pratique : voir trois directives à l’œuvre
Ce lab prend environ quinze minutes. Il utilise un service de démonstration qui se contente de lister, tester et créer des fichiers, et il supprime tout à la fin. Chaque sortie ci-dessous est une sortie réelle du banc de test. Les lignes qui commencent par --- dans les sorties sont des libellés que mon script de test a affichés entre les commandes, en anglais.
Étape 1 : créer le service de démonstration et le lancer sans sandbox
Créez le fichier /etc/systemd/system/demo-hardening-sandbox.service avec votre éditeur, par exemple sudo nano /etc/systemd/system/demo-hardening-sandbox.service. Les lignes qui commencent par # sont des commentaires. Mettez toujours un commentaire sur sa propre ligne, jamais après un réglage.
# Démonstration inoffensive : compte les entrées de /home, teste une écriture dans /etc et crée un fichier dans /tmp. [Unit] Description=Service de démonstration du sandboxing (inoffensif) [Service] # oneshot : systemd attend la fin de la commande ; l'unité ne reste jamais « active ». Type=oneshot # systemd ne lance pas de shell tout seul : la commande démarre donc /bin/sh explicitement. ExecStart=/bin/sh -c 'echo "entrées dans /home :"; ls -A /home | wc -l; touch /etc/demo-hardening-test && rm /etc/demo-hardening-test && echo "etc : écriture ok" || echo "etc : écriture refusée"; touch /tmp/demo-hardening-marker; echo "terminé"'
Demandez maintenant à systemd de lire le nouveau fichier, affichez-le, lancez le service et lisez sa sortie. Un service oneshot s’exécute une fois, et systemctl start attend sa fin :
sudo systemctl daemon-reload
systemctl cat demo-hardening-sandbox.service
sudo systemctl start demo-hardening-sandbox.service
journalctl -u demo-hardening-sandbox.service -o cat --no-pager
ls -l /tmp/demo-hardening-marker | awk '{print $1, $3, $NF}'
[...] Starting demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif)... entrées dans /home : 1 etc : écriture ok terminé demo-hardening-sandbox.service: Deactivated successfully. Finished demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif). -rw-r--r-- root /tmp/demo-hardening-marker
systemctl cat affiche le fichier précédé d’une ligne de commentaire avec son chemin. Le journal garde les exécutions précédentes du service sur le banc de test (celles de la variante anglaise de ce lab) : j’ai coupé ces anciennes lignes avec [...]. Lisez le résultat. Le service a vu une entrée dans /home, il a pu écrire dans /etc et il a créé son fichier témoin dans le vrai /tmp. Rien n’est encore protégé. Mesurez le point de départ :
systemd-analyze security --no-pager demo-hardening-sandbox.service | tail -n 1 systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -c '✗' systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -c '✓'
→ Overall exposure level for demo-hardening-sandbox.service: 9.6 UNSAFE 😨 75 4
Regardez enfin les trois lignes que l’étape suivante va corriger :
systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -E 'ProtectSystem|ProtectHome|PrivateTmp'
✗ ProtectSystem= Service has full access to the OS file hierarchy 0.2 ✗ ProtectHome= Service has full access to home directories 0.2 ✗ PrivateTmp= Service has access to other software's temporary files 0.2
Étape 2 : ajouter le drop-in
La méthode habituelle est l’éditeur interactif : sudo systemctl edit demo-hardening-sandbox.service ouvre un nouveau fichier nommé override.conf (non testé ici, car il demande un terminal). Pour que le lab reste reproductible, utilisez la forme non interactive. --stdin lit le contenu sur l’entrée standard et --drop-in=hardening choisit le nom du fichier :
sudo systemctl edit --stdin --drop-in=hardening demo-hardening-sandbox.service <<'EOF' [Service] # Tout le système de fichiers devient en lecture seule pour ce service (sauf /dev, /proc et /sys). ProtectSystem=strict # /home, /root et /run/user deviennent inaccessibles et vides. ProtectHome=true # /tmp et /var/tmp deviennent privés à ce service et sont supprimés à son arrêt. PrivateTmp=true EOF
Chaque ligne de ce fichier fait une chose. [Service] est l’en-tête de section obligatoire. ProtectSystem=strict monte toute la hiérarchie du système de fichiers en lecture seule, sauf /dev, /proc et /sys. ProtectHome=true rend /home, /root et /run/user inaccessibles et vides. PrivateTmp=true donne au service son propre /tmp et /var/tmp, supprimés à son arrêt. Systemd répond avec le chemin du nouveau fichier :
Successfully installed edited file '/etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf'.
Vérifiez maintenant ce que systemd lit et ce qu’il a chargé. La sortie de systemctl cat commence par le fichier principal de l’unité ; je coupe cette partie :
systemctl cat demo-hardening-sandbox.service systemctl show --property=ProtectSystem --property=ProtectHome --property=PrivateTmp demo-hardening-sandbox.service
[...] # /etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf [Service] # Tout le système de fichiers devient en lecture seule pour ce service (sauf /dev, /proc et /sys). ProtectSystem=strict # /home, /root et /run/user deviennent inaccessibles et vides. ProtectHome=true # /tmp et /var/tmp deviennent privés à ce service et sont supprimés à son arrêt. PrivateTmp=true PrivateTmp=yes ProtectHome=yes ProtectSystem=strict
systemctl show affiche yes là où le fichier dit true : les deux désignent le même booléen. Les trois dernières lignes sont les valeurs que systemd a chargées, et celles d’avant sont le fichier. Relancez le service et comparez avec l’étape 1 :
rm -f /tmp/demo-hardening-marker sudo systemctl start demo-hardening-sandbox.service journalctl -u demo-hardening-sandbox.service -n 7 -o cat --no-pager ls -l /tmp/demo-hardening-marker
entrées dans /home : 0 touch: cannot touch '/etc/demo-hardening-test': Read-only file system etc : écriture refusée terminé demo-hardening-sandbox.service: Deactivated successfully. Finished demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif). ls: cannot access '/tmp/demo-hardening-marker': No such file or directory
Trois choses ont changé, et elles correspondent au manuel. Le service voit maintenant 0 entrée dans /home. Son écriture dans /etc échoue avec Read-only file system. Et le fichier témoin qu’il a créé n’est pas visible depuis l’hôte, car le service a utilisé un /tmp privé.
Étape 3 : mesurer à nouveau
systemd-analyze security --no-pager demo-hardening-sandbox.service | tail -n 1 systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -E 'ProtectSystem|ProtectHome|PrivateTmp'
→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁 ✓ ProtectSystem= Service has strict read-only access to the OS file hierarchy ✓ PrivateTmp= Service has no access to other software's temporary files ✓ ProtectHome= Service has no access to home directories
La note de systemd-analyze security est passée de 9,6 UNSAFE à 8,9 EXPOSED, les trois lignes sont passées de ✗ à ✓, et les compteurs sont passés de 75 non appliqués et 4 appliqués à 71 et 8. Trois directives donnent un gain modeste, ce qui est normal : un service root garde beaucoup d’autres expositions, comme l’utilisateur sous lequel il tourne et ses capabilities. Les parties 3 et 4 de la série s’en occupent.
Étape 4 : essayer une directive à la fois avec systemd-run
systemd-run lance une commande comme unité transitoire, et -p fixe une propriété dans le même format que systemctl set-property. Les options ci-dessous sont celles du manuel : --quiet masque le message de démarrage, --pipe relie la commande à l’entrée et à la sortie de votre terminal, --wait attend la fin et --collect décharge l’unité ensuite, même après un échec. (Le manuel documente aussi -t pour un shell interactif ; je ne l’ai pas testé.) Le manuel ne donne aucun exemple de sandboxing pour systemd-run : les résultats ci-dessous sont donc mes propres tests. Les lignes echo affichent les libellés que vous voyez dans la sortie, et chaque ls contrôle ensuite le vrai système de fichiers :
RUN="sudo systemd-run --quiet --pipe --wait --collect"
echo "--- no sandbox: write to /etc"; $RUN /bin/touch /etc/demo-hardening-t1; echo "exit=$?"; ls /etc/demo-hardening-t1; rm -f /etc/demo-hardening-t1
echo "--- ProtectSystem=strict: write to /etc"; $RUN -p ProtectSystem=strict /bin/touch /etc/demo-hardening-t2; echo "exit=$?"; ls /etc/demo-hardening-t2 2>&1
echo "--- ProtectSystem=true: write to /etc, then to /usr"
$RUN -p ProtectSystem=true /bin/touch /etc/demo-hardening-t3; echo "etc exit=$?"; ls /etc/demo-hardening-t3; rm -f /etc/demo-hardening-t3
$RUN -p ProtectSystem=true /bin/touch /usr/demo-hardening-t4; echo "usr exit=$?"; ls /usr/demo-hardening-t4 2>&1
echo "--- ProtectSystem=full: write to /etc"; $RUN -p ProtectSystem=full /bin/touch /etc/demo-hardening-t5; echo "exit=$?"; ls /etc/demo-hardening-t5 2>&1
echo "--- /home entries seen by the service, without and with ProtectHome=true"
$RUN /bin/sh -c 'ls -A /home | wc -l'
$RUN -p ProtectHome=true /bin/sh -c 'ls -A /home | wc -l'
echo "--- ProtectHome=true: ls -ld /home inside the service"; $RUN -p ProtectHome=true /bin/ls -ld /home | awk '{print $1, $3, $4}'
echo "--- PrivateTmp=true: file created in /tmp by the service, seen from the host?"
rm -f /tmp/demo-hardening-t6
$RUN -p PrivateTmp=true /bin/touch /tmp/demo-hardening-t6; echo "service exit=$?"; ls /tmp/demo-hardening-t6 2>&1
echo "--- without PrivateTmp, same test"
$RUN /bin/touch /tmp/demo-hardening-t7; echo "service exit=$?"; ls /tmp/demo-hardening-t7 2>&1; rm -f /tmp/demo-hardening-t7
--- no sandbox: write to /etc exit=0 /etc/demo-hardening-t1 --- ProtectSystem=strict: write to /etc /bin/touch: cannot touch '/etc/demo-hardening-t2': Read-only file system exit=1 ls: cannot access '/etc/demo-hardening-t2': No such file or directory --- ProtectSystem=true: write to /etc, then to /usr etc exit=0 /etc/demo-hardening-t3 /bin/touch: cannot touch '/usr/demo-hardening-t4': Read-only file system usr exit=1 ls: cannot access '/usr/demo-hardening-t4': No such file or directory --- ProtectSystem=full: write to /etc /bin/touch: cannot touch '/etc/demo-hardening-t5': Read-only file system exit=1 ls: cannot access '/etc/demo-hardening-t5': No such file or directory --- /home entries seen by the service, without and with ProtectHome=true 1 0 --- ProtectHome=true: ls -ld /home inside the service d--------- root root --- PrivateTmp=true: file created in /tmp by the service, seen from the host? service exit=0 ls: cannot access '/tmp/demo-hardening-t6': No such file or directory --- without PrivateTmp, same test service exit=0 /tmp/demo-hardening-t7
Ce tableau lit les résultats comme le manuel les décrit :
| Directive | Ce que mon test a montré | Manuel |
|---|---|---|
ProtectSystem=true | L’écriture dans /etc fonctionne, l’écriture dans /usr est refusée | /usr et les répertoires du chargeur de démarrage en lecture seule |
ProtectSystem=full | L’écriture dans /etc est refusée | /etc en lecture seule également |
ProtectSystem=strict | L’écriture dans /etc est refusée | Toute la hiérarchie en lecture seule, sauf /dev, /proc, /sys |
ProtectHome=true | 0 entrée vue, et /home affiche le mode d--------- | Répertoires inaccessibles et vides |
PrivateTmp=true | Le fichier créé par le service est absent sur l’hôte | /tmp et /var/tmp non partagés avec les processus extérieurs |
Ces résultats confirment le manuel sur mon banc de test. Ils viennent d’un noyau WSL2 : confirmez-les sur votre propre serveur avant de vous appuyer sur une directive en production.
Étape 5 : annuler et nettoyer
systemctl revert supprime les drop-ins d’une unité. Il laisse en place un fichier d’unité qui n’a pas de version fournie par un paquet, comme notre fichier de démonstration. Supprimez ensuite les fichiers de démonstration et prévenez systemd :
sudo systemctl revert demo-hardening-sandbox.service sudo rm -f /etc/systemd/system/demo-hardening-sandbox.service sudo rm -f /tmp/demo-hardening-marker sudo systemctl daemon-reload
Removed '/etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf'. Removed '/etc/systemd/system/demo-hardening-sandbox.service.d'.
La première commande a affiché les deux lignes ci-dessus. Après le revert, le fichier d’unité était toujours là et le répertoire .d avait disparu. Après le nettoyage, j’ai vérifié qu’il ne restait aucune unité de démonstration, aucun fichier dans /tmp, /etc ou /usr, et aucune unité de démonstration chargée (tous les compteurs à 0).
Erreurs courantes
Chaque erreur ci-dessous s’est produite sur le banc de test, avec le message réel. Je les ai reproduites sur une unité jetable nommée demo-hardening-mistakes.service (un service oneshot qui lance /bin/true) et sur un service sleep, puis j’ai supprimé les deux.
Erreur 1 : un drop-in sans en-tête de section
Le drop-in a-no-header.conf ne contenait que ProtectSystem=strict. Systemd a ignoré la ligne, et la valeur chargée est restée no :
systemctl show --property=ProtectSystem demo-hardening-mistakes.service journalctl -b -o cat --no-pager --since "-1min" | grep -i a-no-header | sort -u
ProtectSystem=no /etc/systemd/system/demo-hardening-mistakes.service.d/a-no-header.conf:1: Assignment outside of section. Ignoring.
Erreur 2 : un drop-in sans le suffixe .conf
Un fichier nommé b-wrong-suffix.txt n’est jamais lu. systemctl cat ne liste que le fichier principal, et la valeur reste no :
systemctl cat demo-hardening-mistakes.service | grep '^#' systemctl show --property=ProtectSystem demo-hardening-mistakes.service
# /etc/systemd/system/demo-hardening-mistakes.service ProtectSystem=no
Erreur 3 : un commentaire sur la même ligne qu’un réglage
J’ai écrit ProtectSystem=strict # make everything read-only sur une seule ligne. Systemd a lu tout le texte comme la valeur, l’a rejeté et a gardé la valeur par défaut :
systemctl show --property=ProtectSystem demo-hardening-mistakes.service journalctl -b -o cat --no-pager --since "-1min" | grep -i c-inline-comment | sort -u
ProtectSystem=no /etc/systemd/system/demo-hardening-mistakes.service.d/c-inline-comment.conf:2: Failed to parse ProtectSystem=strict # make everything read-only, ignoring: Invalid argument
Erreur 4 : un rechargement sans redémarrage
Un service sleep tournait déjà. J’ai ajouté ProtectSystem=strict puis lancé daemon-reload. systemctl show a répondu strict, mais le processus a gardé le même PID et le même namespace de montage que le PID 1. Seul le redémarrage lui a donné un nouveau PID et son propre namespace :
PID=$(systemctl show --property=MainPID --value demo-hardening-sleep.service) readlink /proc/1/ns/mnt readlink /proc/$PID/ns/mnt
J’ai lancé ces trois lignes avant le changement, après le rechargement et après le redémarrage. Les résultats, avec leurs libellés, sont :
--- mount namespace of PID 1 and of the service pid1: mnt:[4026532218] service: mnt:[4026532218] --- after drop-in + daemon-reload (no restart) show ProtectSystem: strict same PID: 1886 (was 1886) service: mnt:[4026532218] --- after systemctl restart new PID: 1948 pid1: mnt:[4026532218] service: mnt:[4026532287]
Autres erreurs à éviter
- Modifier le fichier du paquet dans
/usr/lib/systemd/system/. Utilisez plutôt un drop-in dans/etc/systemd/system/; le manuel donne la priorité à/etc/sur/usr/lib/. - Juger un changement sur « le service démarre ». Testez le vrai travail, comme dans la boucle ci-dessus.
- Changer plusieurs directives à la fois. Vous ne saurez pas laquelle a cassé le service.
- Viser une note de 0. Le manuel précise qu’une note élevée ne signifie pas qu’un service est vulnérable. Servez-vous de la note de systemd-analyze security pour prioriser.
- Utiliser des directives de namespace de montage dans un service utilisateur. Le manuel avertit que certaines fonctions de sandboxing ne sont en général pas disponibles dans les services utilisateur.
Une checklist à suivre
- Travaillez d’abord sur une machine de test ou sur un service inoffensif.
- Lancez
systemd-analyze security UNITÉet notez la note et les lignes ✗. - Écrivez une directive dans un drop-in sous
/etc/systemd/system/UNITÉ.d/, dans un fichier.confavec un en-tête[Service]et des commentaires sur leur propre ligne. - Lancez
systemctl cat UNITÉpour vérifier que systemd lit votre fichier. - Lancez
sudo systemctl restart UNITÉ; un simple rechargement ne suffit pas pour un service déjà lancé. - Lisez
systemctl status UNITÉetjournalctl -u UNITÉ. - Testez le vrai travail du service, pas seulement son démarrage.
- Mesurez à nouveau avec
systemd-analyze security UNITÉ. - Gardez le drop-in dans votre gestion de configuration, ou supprimez-le avec
systemctl revertpuis redémarrez. - Recommencez avec la directive suivante.
Les familles de directives des prochaines parties
Les prochaines parties prennent chaque famille à tour de rôle. Chaque nom de directive ci-dessous est documenté dans la page de manuel systemd.exec ; IPAddressAllow= et IPAddressDeny= le sont dans systemd.resource-control.
| Partie | Famille | Directives couvertes | Question à laquelle elle répond |
|---|---|---|---|
| 2 | Système de fichiers | ProtectSystem, ProtectHome, PrivateTmp, ReadWritePaths, ReadOnlyPaths, InaccessiblePaths, TemporaryFileSystem, BindPaths | Que peut lire et écrire le service ? |
| 3 | Privilèges | NoNewPrivileges, User, DynamicUser, CapabilityBoundingSet, AmbientCapabilities, SecureBits, RestrictSUIDSGID | Que peut devenir le processus ? |
| 4 | Noyau et périphériques | ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs, ProtectClock, ProtectControlGroups, PrivateDevices, ProtectProc, ProcSubset | Que peut-il modifier dans le noyau ? |
| 5 | Namespaces et réseau | PrivateNetwork, PrivateUsers, RestrictNamespaces, RestrictAddressFamilies, IPAddressAllow, IPAddressDeny | Avec qui peut-il communiquer ? |
| 6 | Appels système | SystemCallFilter, SystemCallArchitectures, SystemCallErrorNumber | Quelles requêtes peut-il envoyer au noyau ? |
| 7 | Mémoire et divers | MemoryDenyWriteExecute, LockPersonality, RestrictRealtime, UMask, ProtectHostname, RemoveIPC, KeyringMode | Quels tours de passe-passe lui restent possibles ? |
| 8 | Toute la stack | Une matrice de chaque directive face à chaque service | Que dois-je appliquer où ? |
Ma stack, et par où commencer
Le durcissement demande un modèle de menace, même simple. Je pars du principe qu’un attaquant a déjà pris le contrôle du processus d’un service, et je me demande ce que ce processus pourrait faire ensuite : lire des secrets, écrire un mécanisme de persistance, joindre d’autres services ou obtenir plus de privilèges. Chaque directive retire l’une de ces options.
Mes serveurs font tourner une stack auto-hébergée typique, que les parties suivantes couvriront service par service :
- un reverse proxy devant chaque application publique, avec son bouncer CrowdSec ;
- des applications PHP-FPM, ainsi que des applications Node.js, Python, Go et .NET (un serveur multimédia et un serveur de bibliothèque) ;
- MariaDB, PostgreSQL et Valkey, avec pgBackRest pour les sauvegardes PostgreSQL ;
- un serveur de messagerie, un serveur SFTP et WebDAV, et une messagerie d’équipe ;
- des agents de sécurité et de supervision : un agent SIEM, une protection contre la force brute, un IDS et des exporters de métriques ;
- un VPN et une console d’administration web.
Je commence par les services qui réunissent trois critères : ils sont joignables depuis Internet, directement ou derrière le reverse proxy ; des tiers les ont écrits ; et ils n’ont aucune politique SELinux propre. Ce sont les applications web. Les bases de données et les agents viennent après, car le réseau ne peut pas les joindre directement.
Ce que je retiens
- Mesurez avant de changer. systemd-analyze security donne une note et une liste de tâches, sans rien modifier du service.
- Utilisez des drop-ins. Ils résistent mieux aux mises à jour de paquets qu’une modification du fichier du paquet, et en supprimer un suffit pour revenir en arrière.
- Redémarrez après un changement. Un simple rechargement laisse un processus déjà lancé dans son ancien sandbox.
- Changez une directive à la fois et testez le vrai travail du service.
- Voyez la note comme une boussole. Elle ne compte que ce que systemd restreint lui-même.
- Testez les affirmations. Chaque résultat de cet article vient d’une exécution réelle ; lancez le lab vous-même et comparez.
Sources
- systemd-analyze(1) : page de manuel freedesktop.org
- systemd.unit(5) : page de manuel freedesktop.org
- systemd.exec(5) : page de manuel freedesktop.org
- systemd.service(5) : page de manuel freedesktop.org
- systemd.resource-control(5) : page de manuel freedesktop.org
- systemctl(1) : page de manuel freedesktop.org
- systemd-run(1) : page de manuel freedesktop.org
- journalctl(1) : page de manuel freedesktop.org