La protection du système de fichiers par systemd décide quels fichiers un service peut lire, écrire ou même voir. Cet article est la partie 2 d’une série de huit, « Durcissement systemd, directive par directive ». J’y traite les directives qui façonnent l’arborescence d’un service : ProtectSystem, ProtectHome, PrivateTmp, ReadWritePaths et leurs cousines.
Quand un service est compromis, ou simplement bogué, c’est dans l’arborescence que les dégâts se produisent. Un service qui ne peut pas écrire dans /etc ne peut pas réécrire sa propre configuration, et un service qui ne voit pas /home ne peut pas lire vos clés SSH. La protection du système de fichiers coûte quelques lignes dans un fichier drop-in. Mais chaque ligne peut aussi bloquer une écriture légitime. Pour chaque directive, j’explique donc ce qu’elle fait, ce qu’elle peut casser et comment elle touche ma propre pile. Aucune expérience de systemd n’est nécessaire : chaque étape vient avec la commande complète et un commentaire.
Banc d’essai. Chaque sortie de cet article vient d’une vraie exécution sur Rocky Linux 10.2 avec systemd 257, dans une machine virtuelle WSL2 (noyau 6.18.54.1-microsoft-standard-WSL2, exécution en root). SELinux y est désactivé, et quelques fonctions du noyau diffèrent d’un serveur classique. Je le signale chaque fois que cela compte. Mes fichiers et répertoires de test commencent par _sd2_, ce qui permet de les retrouver et de les supprimer facilement. Quand je coupe une ligne d’une sortie, je la remplace par [...], et je marque ce que je n’ai pas exécuté comme non testé.

Avant de commencer
Lisez d’abord cette courte liste. Elle vous évite de durcir un service de production du premier coup.
- Une machine de test. Utilisez une machine virtuelle ou un serveur de rechange qui fait tourner systemd. Je n’ai testé que sur Rocky Linux 10.2. Les directives appartiennent à systemd lui-même, donc les autres distributions devraient se comporter pareil, mais je ne l’ai pas vérifié.
- **Un shell avec
sudo.** Chaque commande qui modifie le système commence parsudo. Mon banc tournait en root, donc mes sorties viennent de commandes root. - Un service de démonstration. Le labo plus bas crée
demo-hardening-fs.service, un service inoffensif qui essaie seulement de créer des fichiers vides. Vous ne touchez jamais à un vrai service. - Un retour en arrière. Chaque changement vit dans un fichier drop-in.
systemctl revertle supprime, et le labo se termine par un nettoyage. - Un coup d’œil à votre version. Lancez
systemctl --version. Je n’ai pu tester que la version 257 : si votre systemd rejette une directive, lisez son entrée dans la page de manuelsystemd.exec.
Un petit glossaire
| Terme | Ce que cela veut dire ici |
|---|---|
| Espace de noms de montage | Une copie privée de la liste des systèmes de fichiers montés. Un changement fait à l’intérieur n’apparaît pas sur l’hôte. |
| Lecture seule | Un chemin où la lecture marche et où l’écriture échoue avec le message « Read-only file system ». |
| tmpfs | Un système de fichiers gardé en mémoire. Il démarre vide et disparaît quand on le démonte. |
| Montage lié (bind mount) | Le même répertoire rendu visible à un second endroit de l’arborescence. |
| Systèmes de fichiers d’API | /proc, /sys et /dev, fournis par le noyau. Un service en a besoin pour tourner. |
| Drop-in | Un petit fichier .conf dans un répertoire .d qui modifie une unité sans toucher au fichier d’origine. |
| Directive | Un réglage d’un fichier d’unité, comme ProtectSystem=strict. |
| Unité transitoire | Une unité temporaire que systemd-run crée à la volée. Elle disparaît quand elle s’arrête. |
| Utilisateur du service | Le compte qui exécute le service, défini par User= dans l’unité. |
En un coup d’œil
Ce tableau est la carte de la protection du système de fichiers dans cet article. La dernière colonne vient de mes notes de homelab : elle dit où la directive protège déjà, ou a déjà cassé, un de mes services.
| Directive | Ce qu’elle fait | Ce qu’elle peut casser | Dans ma pile |
|---|---|---|---|
ProtectSystem= | Monte /usr, puis /etc, puis toute l’arborescence en lecture seule | Toute écriture hors de vos exceptions | Kavita, Navidrome, mktxp, mox et Etherpad tournent avec strict |
ProtectHome= | Cache ou fige /home, /root et /run/user | Les programmes qui gardent caches ou clés dans leur répertoire personnel | Kavita utilise yes ; Oxidized a eu besoin de read-only |
PrivateTmp= | Donne au service son propre /tmp et /var/tmp | Les services qui partagent des fichiers par /tmp | Kavita ouvre plutôt le /tmp de l’hôte |
PrivateMounts= | Place le service dans son propre espace de noms de montage | Les montages que le service fait pour d’autres | Mes notes n’en parlent pas |
ReadWritePaths= | Ouvre des exceptions inscriptibles | Un chemin absent arrête le service | Le correctif le plus fréquent de mes notes |
ReadOnlyPaths= | Fige un chemin dans une zone inscriptible | Une écriture à cet endroit échoue | Mes notes n’en parlent pas |
InaccessiblePaths= | Cache complètement un chemin | Toute lecture à cet endroit échoue | Mes notes n’en parlent pas |
ExecPaths= et NoExecPaths= | Autorisent ou interdisent d’exécuter des programmes depuis un chemin | Les binaires d’un chemin interdit (erreur 203) | Mes notes n’en parlent pas |
BindPaths= et BindReadOnlyPaths= | Montrent un répertoire à un second endroit | Une mauvaise source ou destination | Mes notes n’en parlent pas |
TemporaryFileSystem= | Remplace un répertoire par un tmpfs vide | Tout ce que ce répertoire contenait | Mes notes n’en parlent pas |
StateDirectory= et cie | Créent des répertoires gérés avec le bon propriétaire | Des surprises de propriétaire et de mode | Navidrome liste aujourd’hui ces chemins à la main |
RestrictFileSystems= | N’autorise que certains types de systèmes de fichiers | Demande une fonction du noyau (BPF LSM) | Mes notes n’en parlent pas |
RootDirectory=, RootImage= | Lancent le service dans un chroot ou une image disque | Tout ce que le service s’attend à trouver | Mes notes n’en parlent pas |
Protection du système de fichiers : comment un service obtient sa propre vue
Quand systemd démarre un service, il peut d’abord lui donner un espace de noms de montage privé. En clair, le service reçoit sa propre copie de la liste des montages. Systemd modifie ensuite cette copie : il remonte certains chemins en lecture seule, en cache d’autres et ajoute des répertoires temporaires privés. L’hôte ne change pas. Seuls les processus de ce service voient la différence.
Trois conséquences comptent pour un débutant. D’abord, les règles s’appliquent à tous les processus du service, root compris. Mon banc tournait en root, et les écritures ont quand même été refusées. Ensuite, une règle n’est pas une permission : le fichier reste inscriptible pour tous les autres processus de la machine. Enfin, un accès bloqué dit rarement « Permission denied ». Dans mes essais, il disait « Read-only file system » ou « No such file or directory », donc lisez ces messages avec cela en tête.
La méthode est toujours la même. On ferme d’abord une grande zone, puis on ouvre les quelques exceptions dont le service a besoin. ProtectSystem, ProtectHome et PrivateTmp ferment. Les directives de chemins et la famille *Directory= ouvrent. La suite de l’article suit cet ordre.
ProtectSystem : trois niveaux de lecture seule
ProtectSystem= est le cœur de la protection du système de fichiers, et il prend quatre valeurs. no est la valeur par défaut et ne change rien. yes (aussi écrit true) monte /usr et les répertoires du chargeur de démarrage en lecture seule. full ajoute /etc. strict monte toute l’arborescence en lecture seule, sauf /dev, /proc et /sys. Les exceptions inscriptibles viennent de ReadWritePaths=, des réglages *Directory= et de PrivateTmp=.
J’ai lancé une sonde par valeur. Elle essaie de créer un fichier vide à quatre endroits. L’option -P de systemd-run affiche la sortie dans votre terminal, donc vous voyez le résultat tout de suite. Voici la commande pour strict :
# Lancer une commande dans une unité temporaire qui a ProtectSystem=strict
# -P (--pipe) affiche la sortie ici, --wait attend la fin, -G (--collect) supprime l'unité ensuite
sudo systemd-run --quiet --pipe --wait --collect -p ProtectSystem=strict \
sh -c 'for d in /usr /etc /var/tmp /tmp; do
if err=$(touch "$d/_sd2_test" 2>&1); then echo "$d: write OK"
else echo "$d: write refused -> ${err##*: }"; fi
done'
# Avec ProtectSystem=no, les fichiers de test existent vraiment sur l'hôte : on les supprime
sudo rm -f /usr/_sd2_test /etc/_sd2_test /var/tmp/_sd2_test /tmp/_sd2_test
Et voici sa sortie :
/usr: write refused -> Read-only file system /etc: write refused -> Read-only file system /var/tmp: write refused -> Read-only file system /tmp: write refused -> Read-only file system
J’ai lancé la même sonde pour les autres valeurs et pour deux combinaisons. Ce tableau résume les résultats, et chaque cellule est une vraie réponse :
| Valeur | /usr | /etc | /var/tmp | /tmp |
|---|---|---|---|---|
no | écriture OK | écriture OK | écriture OK | écriture OK |
yes | refusée | écriture OK | écriture OK | écriture OK |
full | refusée | refusée | écriture OK | écriture OK |
strict | refusée | refusée | refusée | refusée |
strict + PrivateTmp=yes | refusée | refusée | écriture OK | écriture OK |
strict + ReadWritePaths=/etc | refusée | écriture OK | refusée | refusée |
Deux leçons ressortent de ce tableau. D’abord, strict fige aussi /tmp et /var/tmp, et beaucoup de programmes ne démarrent pas sans répertoire temporaire. Donc strict va en général avec PrivateTmp=yes. Ensuite, une exception de ReadWritePaths= n’ouvre que le chemin nommé : avec /etc ouvert, /tmp est resté en lecture seule.
Une surprise : /home et /root restent inscriptibles sous strict
Le manuel décrit strict comme une arborescence en lecture seule, hors systèmes de fichiers d’API. Mon banc le contredit pour deux répertoires. J’ai lancé une seconde sonde :
sudo systemd-run --quiet --pipe --wait --collect -p ProtectSystem=strict \
sh -c 'for d in /home /root /opt /srv /var/lib; do
if err=$(touch "$d/_sd2_diag" 2>&1); then echo "$d: write OK"
else echo "$d: write refused -> ${err##*: }"; fi
done'
sudo rm -f /home/_sd2_diag /root/_sd2_diag
/home: write OK /root: write OK /opt: write refused -> Read-only file system /srv: write refused -> Read-only file system /var/lib: write refused -> Read-only file system
Dans le service, findmnt montre la différence : /home est un montage séparé en lecture-écriture, alors que le montage qui contient /etc est en lecture seule.
# Quel montage contient /home et /etc dans le service, et avec quelles options ? sudo systemd-run --quiet --pipe --wait --collect -p ProtectSystem=strict findmnt -T /home -o TARGET,FSTYPE,OPTIONS sudo systemd-run --quiet --pipe --wait --collect -p ProtectSystem=strict findmnt -T /etc -o TARGET,FSTYPE,OPTIONS
TARGET FSTYPE OPTIONS /home ext4 rw,relatime,discard,errors=remount-ro,data=ordered TARGET FSTYPE OPTIONS / ext4 ro,relatime,discard,errors=remount-ro,data=ordered
Je ne sais pas si cela vient de WSL2, de ce build de systemd ou de la façon dont le système de fichiers racine est monté, et je n’ai pas creusé plus loin. La règle pratique est simple : ne comptez jamais sur strict pour protéger les répertoires personnels. Ajoutez ProtectHome= et vérifiez le résultat sur votre propre serveur avec la sonde ci-dessus.
Quelle valeur choisir
Pour un service de longue durée, partez de strict et ajoutez les exceptions une par une. Mes notes montrent ce que cela donne en pratique. L’exporteur mktxp essayait d’écrire un fichier de configuration par défaut dans son répertoire de configuration, et strict l’en empêchait jusqu’à ce que j’ajoute ce répertoire à ReadWritePaths=. Navidrome avait un problème plus discret. Son répertoire de sauvegarde manquait dans ReadWritePaths=, donc sa propre tâche de sauvegarde échouait chaque nuit avec « unable to open database file ». Personne ne l’a remarqué pendant environ une semaine, et chaque archive nocturne pesait 122 octets. La leçon : après avoir ajouté strict, lisez pendant quelques jours le journal du service à la recherche de « Read-only file system ».
ProtectHome : cacher, figer ou remplacer /home
ProtectHome= étend la protection du système de fichiers aux données personnelles. Il agit sur /home, /root et /run/user. Avec yes (ou true), les trois répertoires paraissent vides et rien de ce qu’ils contiennent n’est accessible. Avec read-only, ils restent visibles mais figés. Avec tmpfs, le service voit à la place des systèmes de fichiers temporaires vides, et BindPaths= peut ramener des répertoires choisis.

J’ai créé un faux répertoire personnel avec un fichier, puis lancé une sonde par valeur. Changez la valeur après ProtectHome= pour la répéter :
# Préparer un faux répertoire personnel avec un fichier
sudo mkdir -p /home/_sd2_user && echo hello | sudo tee /home/_sd2_user/hello.txt > /dev/null
# Changer la valeur après ProtectHome= : no, yes, read-only, tmpfs
sudo systemd-run --quiet --pipe --wait --collect -p ProtectHome=yes \
sh -c 'echo "ls -A /home:"; ls -A /home 2>&1
echo "cat hello.txt:"; cat /home/_sd2_user/hello.txt 2>&1
if err=$(touch /home/_sd2_user/new.txt 2>&1); then echo "write: OK"
else echo "write: refused -> ${err##*: }"; fi'
# Supprimer le faux répertoire personnel
sudo rm -rf /home/_sd2_user
Avec yes, le service ne voit rien :
ls -A /home: cat hello.txt: cat: /home/_sd2_user/hello.txt: No such file or directory write: refused -> No such file or directory
Avec read-only, le service lit le fichier mais ne peut pas écrire. Une ligne qui nomme le vrai répertoire personnel de mon compte est coupée :
ls -A /home: [...] _sd2_user cat hello.txt: hello write: refused -> Read-only file system
| Valeur | Liste du répertoire | Lecture du fichier | Écriture |
|---|---|---|---|
no | visible | OK | OK |
yes | vide | No such file or directory | No such file or directory |
read-only | visible | OK | Read-only file system |
tmpfs | vide | No such file or directory | No such file or directory |
yes + ReadWritePaths= sur le sous-répertoire | vide | No such file or directory | No such file or directory |
read-only + ReadWritePaths= sur le sous-répertoire | visible | OK | OK |
tmpfs + BindPaths= sur le sous-répertoire | seulement ce sous-répertoire | OK | OK |
Les trois dernières lignes sont celles à retenir. ReadWritePaths= ne peut pas rouvrir un chemin sous ProtectHome=yes. Il marche sous read-only, et tmpfs avec BindPaths= marche aussi. Mes notes décrivent le même piège sur un vrai service : Oxidized, qui sauvegarde les configurations d’équipements réseau, ne pouvait pas utiliser son propre répertoire personnel sous ProtectHome=true, bien que ReadWritePaths= le liste. Passer à read-only a réglé le problème. Sur mon serveur, l’erreur était « permission denied » ; sur le banc, c’était « No such file or directory ». Les deux signifient que le chemin est hors d’atteinte. Mon banc tournait en root, ce qui peut expliquer la différence de message.
Nommer le sous-répertoire, pas /home
Un second piège : ReadWritePaths= demande le sous-répertoire exact. J’ai essayé le parent d’abord, puis le sous-répertoire, tous deux avec ProtectHome=read-only. La sonde essaie d’écrire aux deux endroits :
# Deux essais avec ProtectHome=read-only : ReadWritePaths sur /home lui-même, puis sur un sous-répertoire
sudo mkdir -p /home/_sd2_sub
for rw in /home /home/_sd2_sub; do
echo "ReadWritePaths=$rw"
sudo systemd-run --quiet --pipe --wait --collect -p ProtectHome=read-only -p ReadWritePaths=$rw \
sh -c 'for d in /home /home/_sd2_sub; do
if err=$(touch "$d/_sd2_p" 2>&1); then echo " $d: write OK"
else echo " $d: write refused -> ${err##*: }"; fi
done'
done
sudo rm -rf /home/_sd2_sub /home/_sd2_p
ReadWritePaths=/home /home: write refused -> Read-only file system /home/_sd2_sub: write refused -> Read-only file system ReadWritePaths=/home/_sd2_sub /home: write refused -> Read-only file system /home/_sd2_sub: write OK
Lister /home n’a rien ouvert, pas même le sous-répertoire qu’il contient. Lister le sous-répertoire l’a ouvert, et /home est resté en lecture seule.
Quand le programme veut son répertoire personnel
Beaucoup de programmes écrivent des caches ou des clés sous $HOME. Si ProtectHome=yes le cache, ils échouent de façon déroutante. Deux cas réels de mes notes : Kavita n’arrivait pas à enregistrer son premier utilisateur, car le magasin de clés .NET utilise par défaut un dossier du répertoire personnel du compte de service. L’outil de build pnpm d’Etherpad ne trouvait pas non plus de répertoire personnel inscriptible sous strict. Dans les deux cas, je n’ai pas élargi le bac à sable. J’ai défini Environment=HOME= vers un répertoire déjà inscriptible, dans le dossier de l’application. Cela garde la protection et satisfait le programme.
PrivateTmp et PrivateMounts : des espaces privés
PrivateTmp=yes donne au service son propre /tmp et /var/tmp. Systemd les crée au démarrage du service et les supprime à son arrêt. Les autres services, et vous, ne voient pas leur contenu. Cela bloque toute une classe d’attaques qui reposent sur des noms de fichiers prévisibles dans /tmp. J’ai lancé la même commande sans, puis avec. Un fichier nommé _sd2_host_file existait déjà dans le /tmp de l’hôte :
touch /tmp/_sd2_host_file # Même commande deux fois : sans, puis avec PrivateTmp=yes sudo systemd-run --quiet --pipe --wait --collect \ sh -c 'echo "files named _sd2_* seen in /tmp:"; ls -A /tmp | grep _sd2_; touch /tmp/_sd2_in_unit && echo "created /tmp/_sd2_in_unit"' # Supprimer le fichier que ce premier essai a créé sur l'hôte sudo rm -f /tmp/_sd2_in_unit sudo systemd-run --quiet --pipe --wait --collect -p PrivateTmp=yes \ sh -c 'echo "files named _sd2_* seen in /tmp:"; ls -A /tmp | grep _sd2_; touch /tmp/_sd2_in_unit && echo "created /tmp/_sd2_in_unit"' # Que voit l'hôte ensuite ? ls -A /tmp | grep _sd2_ sudo rm -f /tmp/_sd2_host_file /tmp/_sd2_in_unit
files named _sd2_* seen in /tmp: _sd2_host_file created /tmp/_sd2_in_unit files named _sd2_* seen in /tmp: created /tmp/_sd2_in_unit _sd2_host_file
Sans PrivateTmp, le service voit le fichier de l’hôte. Avec lui, le service voit un /tmp vide, crée son propre fichier et ne laisse rien derrière lui. La dernière ligne vient de l’hôte : il a toujours son fichier d’origine et rien d’autre. Rappel : strict met /tmp en lecture seule, mais PrivateTmp=yes en redonne un privé et inscriptible.
Une troisième valeur existe dans les versions récentes, PrivateTmp=disconnected. J’ai seulement testé si elle est acceptée. Via systemd-run -p, systemd 257 l’a refusée sur mon banc :
# La troisième valeur, via systemd-run sudo systemd-run --quiet --pipe --wait --collect -p PrivateTmp=disconnected true
Failed to parse PrivateTmp: disconnected
Écrite dans un fichier d’unité, la même valeur a été acceptée et le service one-shot est allé au bout sans se plaindre. Je n’ai pas testé ce qu’elle change : son comportement est non testé.
Dans ma pile, Kavita ouvre le /tmp de l’hôte avec ReadWritePaths= au lieu d’utiliser PrivateTmp=yes. Un /tmp privé serait plus strict. Je ne l’ai pas encore essayé.
PrivateMounts
PrivateMounts=yes place le service dans son propre espace de noms de montage et ne fait rien d’autre. Les montages que le service réalise ne fuient pas vers l’hôte. Je l’ai testé avec un tmpfs monté depuis l’intérieur du service :
sudo mkdir -p /mnt/_sd2_pm # Monter un tmpfs dans le service, puis regarder depuis l'hôte sudo systemd-run --quiet --pipe --wait --collect -p PrivateMounts=yes \ sh -c 'mount -t tmpfs none /mnt/_sd2_pm && touch /mnt/_sd2_pm/x && findmnt -n -o TARGET,FSTYPE /mnt/_sd2_pm' findmnt /mnt/_sd2_pm || echo "not mounted on the host" sudo rmdir /mnt/_sd2_pm
/mnt/_sd2_pm tmpfs not mounted on the host
La plupart des directives de cet article activent d’elles-mêmes un espace de noms de montage privé, donc il est rare de devoir écrire PrivateMounts=yes. Il compte pour un service qui monte des systèmes de fichiers et ne doit pas perturber l’hôte.
ReadWritePaths, ReadOnlyPaths et InaccessiblePaths
Ces trois directives vous laissent régler la protection du système de fichiers chemin par chemin. Chacune prend une liste de chemins séparés par des espaces. Vous pouvez les répéter, et les listes s’additionnent. ReadWritePaths= rend un chemin à nouveau inscriptible sous ProtectSystem. ReadOnlyPaths= fige un chemin dans une zone par ailleurs inscriptible. InaccessiblePaths= le cache. J’ai testé les deux dernières sur un répertoire jetable :
D=/var/tmp/_sd2_dir
mkdir -p $D && echo data > $D/file.txt
# Lecture seule : le fichier reste lisible, aucun fichier ne peut être créé, le reste de /var/tmp reste inscriptible
sudo systemd-run --quiet --pipe --wait --collect -p ReadOnlyPaths=$D \
sh -c 'cat /var/tmp/_sd2_dir/file.txt
if err=$(touch /var/tmp/_sd2_dir/new 2>&1); then echo "write in dir: OK"
else echo "write in dir: refused -> ${err##*: }"; fi
touch /var/tmp/_sd2_other && echo "write elsewhere in /var/tmp: OK"'
# Inaccessible : le répertoire paraît vide et rien ne peut être lu
sudo systemd-run --quiet --pipe --wait --collect -p InaccessiblePaths=$D \
sh -c 'ls -A /var/tmp/_sd2_dir 2>&1; echo "---"; cat /var/tmp/_sd2_dir/file.txt 2>&1'
rm -rf $D /var/tmp/_sd2_other
data write in dir: refused -> Read-only file system write elsewhere in /var/tmp: OK --- cat: /var/tmp/_sd2_dir/file.txt: No such file or directory
Le premier essai montre ReadOnlyPaths= : le fichier est lisible, un nouveau fichier dans le répertoire est refusé, et le reste de /var/tmp reste inscriptible. Le second montre InaccessiblePaths= : le répertoire paraît vide, puis le fichier ne peut pas être lu. Il existe toujours sur l’hôte.
Le tiret initial
Un chemin qui n’existe pas au démarrage du service arrête le service. Un tiret devant le chemin demande à systemd de l’ignorer s’il est absent :
# Un chemin qui n'existe pas, sans puis avec le tiret initial sudo systemd-run --quiet --pipe --wait --collect -p ReadWritePaths=/nonexistent_sd2 true; echo "exit code: $?" sudo systemd-run --quiet --pipe --wait --collect -p ReadWritePaths=-/nonexistent_sd2 true; echo "exit code: $?"
exit code: 226 exit code: 0
Le code de sortie 226 est la façon dont systemd signale un échec pendant la construction de l’espace de noms. Sans le tiret, le service n’a même pas démarré.
Trouver le chemin manquant
Dans mes notes, ReadWritePaths= est le correctif le plus fréquent. Kavita a demandé deux ajouts après sa première version. Il écrit sa base SQLite dans un dossier config sous son répertoire de travail, un chemin que je n’avais pas listé. Il réécrit aussi wwwroot/index.html à chaque démarrage, ce qui ne produisait qu’une erreur sans gravité mais bruyante. Chaque fois, le journal disait « Read-only file system ». Ma routine est donc courte : démarrer le service, lancer journalctl -u UNITÉ | grep -i 'read-only file system', ajouter le seul chemin qui apparaît, puis recommencer. Je n’ai pas lancé cette commande exacte sur le banc, mais elle ne fait que lire le journal.
ExecPaths et NoExecPaths : où les programmes peuvent tourner
NoExecPaths= interdit d’exécuter des programmes depuis un chemin, et ExecPaths= creuse une exception. Une application web qui accepte des envois de fichiers est un bon cas d’usage : rien de ce qui est envoyé ne doit jamais s’exécuter. J’ai copié true dans un répertoire jetable et essayé de le lancer trois fois :
D=/var/tmp/_sd2_dir mkdir -p $D && cp /usr/bin/true $D/runme # Lancer la copie de true : sans restriction, puis avec NoExecPaths, puis NoExecPaths sur le parent avec une exception ExecPaths sudo systemd-run --quiet --pipe --wait --collect $D/runme; echo "no restriction, exit code: $?" sudo systemd-run --quiet --pipe --wait --collect -p NoExecPaths=$D $D/runme; echo "NoExecPaths, exit code: $?" sudo systemd-run --quiet --pipe --wait --collect -p NoExecPaths=/var/tmp -p ExecPaths=$D $D/runme; echo "with ExecPaths, exit code: $?" rm -rf $D
no restriction, exit code: 0 NoExecPaths, exit code: 203 with ExecPaths, exit code: 0
Le code de sortie 203 signifie que systemd n’a pas pu exécuter le programme. Le dernier essai montre le schéma : interdire une grande zone, puis autoriser le seul sous-répertoire qui doit exécuter des programmes. Mes notes ne parlent pas encore de ces deux directives, donc ce sont des candidates pour les répertoires d’envoi de mes applications web.
BindPaths, BindReadOnlyPaths et TemporaryFileSystem
BindPaths= montre un répertoire à un second endroit, au format SOURCE:DESTINATION. BindReadOnlyPaths= fait pareil, mais en lecture seule. J’ai créé un répertoire avec un fichier et je l’ai lié deux fois :
S=/var/tmp/_sd2_src
mkdir -p $S && echo hi > $S/hi.txt
# Montrer $S à un second endroit : en écriture, puis en lecture seule
sudo systemd-run --quiet --pipe --wait --collect -p BindPaths=$S:/mnt/_sd2_dst \
sh -c 'ls -A /mnt/_sd2_dst; if echo new > /mnt/_sd2_dst/new.txt; then echo "write OK"; fi'
# L'écriture est arrivée jusqu'à la source sur l'hôte
ls -A $S
sudo systemd-run --quiet --pipe --wait --collect -p BindReadOnlyPaths=$S:/mnt/_sd2_dst \
sh -c 'if err=$(touch /mnt/_sd2_dst/x 2>&1); then echo "write OK"; else echo "write refused -> ${err##*: }"; fi'
# Systemd a créé le répertoire de destination sur l'hôte
ls -ld /mnt/_sd2_dst
sudo rmdir /mnt/_sd2_dst
hi.txt write OK hi.txt new.txt write refused -> Read-only file system drwxr-xr-x 2 root root 4096 Nov 2 15:13 /mnt/_sd2_dst
Le premier essai a listé le fichier et en a écrit un nouveau. Le listing de l’hôte juste après montre que new.txt est arrivé dans le répertoire source. La variante en lecture seule a refusé l’écriture. Systemd a aussi créé sur l’hôte le répertoire de destination vide /mnt/_sd2_dst, et la dernière commande le montre. Supprimez-le vous-même après le test, comme le fait l’extrait.
TemporaryFileSystem= remplace un répertoire par un tmpfs vide. Combiné à un montage lié, il donne une liste blanche : cacher toute une zone, puis montrer seulement ce dont le service a besoin. Ici /var/tmp est caché, puis un répertoire revient :
S=/var/tmp/_sd2_src mkdir -p $S && echo hi > $S/hi.txt # Cacher tout /var/tmp ... sudo systemd-run --quiet --pipe --wait --collect -p TemporaryFileSystem=/var/tmp:ro \ sh -c 'echo "ls -A /var/tmp:"; ls -A /var/tmp' # ... puis laisser passer un seul répertoire sudo systemd-run --quiet --pipe --wait --collect -p TemporaryFileSystem=/var/tmp:ro -p BindReadOnlyPaths=$S \ sh -c 'echo "ls -A /var/tmp:"; ls -A /var/tmp; cat /var/tmp/_sd2_src/hi.txt' rm -rf $S
ls -A /var/tmp: ls -A /var/tmp: _sd2_src hi
Le premier listing est vide, et le second ne montre que _sd2_src, dont le fichier est lisible. C’est la forme la plus précise de la protection du système de fichiers, et aussi la plus laborieuse. Je ne l’utilise nulle part pour l’instant.
StateDirectory et les autres directives de répertoire
Cinq directives créent un répertoire géré pour le service et l’ouvrent en écriture, même sous ProtectSystem=strict. Systemd définit aussi une variable d’environnement avec le chemin.
| Directive | Répertoire | Variable | Supprimé à l’arrêt ? |
|---|---|---|---|
StateDirectory= | /var/lib/NOM | STATE_DIRECTORY | non |
CacheDirectory= | /var/cache/NOM | CACHE_DIRECTORY | non |
LogsDirectory= | /var/log/NOM | LOGS_DIRECTORY | non |
RuntimeDirectory= | /run/NOM | RUNTIME_DIRECTORY | oui |
ConfigurationDirectory= | /etc/NOM | CONFIGURATION_DIRECTORY | non |
J’ai lancé les cinq d’un coup avec l’utilisateur nobody, sous strict. La sonde affiche les variables, écrit dans chaque répertoire, puis liste ce qui reste sur l’hôte :
N=sd2-demo
# Les cinq réglages *Directory=, sous ProtectSystem=strict, avec l'utilisateur nobody
sudo systemd-run --quiet --pipe --wait --collect -p ProtectSystem=strict -p User=nobody \
-p StateDirectory=$N -p CacheDirectory=$N -p LogsDirectory=$N -p RuntimeDirectory=$N -p ConfigurationDirectory=$N \
sh -c 'env | grep -E "^(STATE|CACHE|LOGS|RUNTIME|CONFIGURATION)_DIRECTORY" | sort
for d in "$STATE_DIRECTORY" "$CACHE_DIRECTORY" "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY" "$CONFIGURATION_DIRECTORY"; do
if err=$(touch "$d/x" 2>&1); then echo "$d: write OK"; else echo "$d: write refused -> ${err##*: }"; fi
done'
# Que reste-t-il sur l'hôte une fois le service arrêté ? (propriétaire et mode)
for p in /var/lib/$N /var/cache/$N /var/log/$N /run/$N /etc/$N; do stat -c "%n %a %U:%G" $p 2>&1; done
CACHE_DIRECTORY=/var/cache/sd2-demo CONFIGURATION_DIRECTORY=/etc/sd2-demo LOGS_DIRECTORY=/var/log/sd2-demo RUNTIME_DIRECTORY=/run/sd2-demo STATE_DIRECTORY=/var/lib/sd2-demo /var/lib/sd2-demo: write OK /var/cache/sd2-demo: write OK /var/log/sd2-demo: write OK /run/sd2-demo: write OK /etc/sd2-demo: write OK /var/lib/sd2-demo 755 nobody:nobody /var/cache/sd2-demo 755 nobody:nobody /var/log/sd2-demo 755 nobody:nobody stat: cannot statx '/run/sd2-demo': No such file or directory /etc/sd2-demo 755 nobody:nobody
Les cinq variables existent et les cinq répertoires étaient inscriptibles sous strict. Une fois le service arrêté, quatre répertoires restent et appartiennent à l’utilisateur du service. Le répertoire d’exécution a disparu, car systemd le supprime à l’arrêt. Le mode par défaut est 755, et StateDirectoryMode= le change (les autres directives ont des réglages ...DirectoryMode= équivalents) :
# StateDirectoryMode= change le mode (755 par défaut) sudo rm -rf /var/lib/sd2-demo sudo systemd-run --quiet --pipe --wait --collect -p User=nobody -p StateDirectory=sd2-demo -p StateDirectoryMode=0700 \ stat -c "%n %a %U:%G" /var/lib/sd2-demo
/var/lib/sd2-demo 700 nobody:nobody
Un résultat diffère de mon attente. D’après ma lecture du manuel, ConfigurationDirectory= se distingue des quatre autres pour le propriétaire, et je m’attendais à root. Sur mon banc, le répertoire appartenait à nobody, comme les autres. Je l’ai lancé seul pour vérifier, d’abord avec User=nobody, puis sans aucun User= :
# ConfigurationDirectory= seul : avec User=nobody, puis sans User= sudo systemd-run --quiet --pipe --wait --collect -p User=nobody -p ConfigurationDirectory=sd2-cfg true stat -c "%n %a %U:%G" /etc/sd2-cfg; sudo rm -rf /etc/sd2-cfg sudo systemd-run --quiet --pipe --wait --collect -p ConfigurationDirectory=sd2-cfg true stat -c "%n %a %U:%G" /etc/sd2-cfg; sudo rm -rf /etc/sd2-cfg
/etc/sd2-cfg 755 nobody:nobody /etc/sd2-cfg 755 root:root
Le répertoire a donc suivi l’utilisateur du service sur ce système. Vérifiez-le sur votre version avant de vous y fier. Le manuel décrit aussi RuntimeDirectoryPreserve= pour garder le répertoire d’exécution après un arrêt ; je ne l’ai pas testé, il est non testé. Nettoyez maintenant les répertoires de test :
# Nettoyer les répertoires laissés par les tests sudo rm -rf /var/lib/sd2-demo /var/cache/sd2-demo /var/log/sd2-demo /run/sd2-demo /etc/sd2-demo
Ces directives rendent la protection du système de fichiers plus facile à maintenir. Elles remplacent les lignes ReadWritePaths= écrites à la main et les tâches mkdir et chown qui vont avec. Navidrome, dans ma pile, liste aujourd’hui /var/lib/navidrome et /var/log/navidrome à la main, ce qui en fait un bon candidat pour StateDirectory= et LogsDirectory=.
RestrictFileSystems : une liste blanche de types de systèmes de fichiers
RestrictFileSystems= limite les types de systèmes de fichiers qu’un service peut utiliser, par exemple ext4 xfs seulement, ou ~tmpfs pour interdire tmpfs. Le manuel dit qu’elle repose sur une fonction du noyau, le module de sécurité BPF. Mon banc ne semble pas l’avoir, donc j’ai testé ce qui se passe :
# Le module de sécurité BPF est-il actif ? (RestrictFileSystems= en a besoin)
cat /sys/kernel/security/lsm
# Écrire dans un tmpfs (/dev/shm) alors que RestrictFileSystems=~tmpfs interdit tmpfs
sudo systemd-run --quiet --pipe --wait --collect -p RestrictFileSystems=~tmpfs \
sh -c 'if err=$(touch /dev/shm/_sd2_rfs 2>&1); then echo "write to tmpfs OK"; else echo "write to tmpfs refused -> ${err##*: }"; fi'
sudo rm -f /dev/shm/_sd2_rfs
cat: /sys/kernel/security/lsm: No such file or directory write to tmpfs OK
Le fichier qui liste les modules de sécurité actifs n’existe même pas sur ce noyau. L’écriture dans un tmpfs a réussi alors que j’avais interdit tmpfs, et systemd n’a levé aucune erreur. La directive n’a donc eu aucun effet, en silence. Sur un noyau doté du module BPF, elle devrait fonctionner, mais je n’ai pas pu le tester. Avant de vous y fier, lancez cat /sys/kernel/security/lsm sur votre serveur et cherchez bpf dans la liste.
RootDirectory, RootImage et MountAPIVFS
Ces directives vont plus loin que le reste de l’article : elles lancent le service dans une autre racine. RootDirectory= fonctionne comme un chroot, RootImage= monte une image disque comme racine, et MountAPIVFS=yes y ajoute /proc, /sys et /dev. Un tel service ne voit que ce que vous placez dans la nouvelle racine, en général les bibliothèques et outils dont il a besoin. La protection est forte, mais elle demande une vraie préparation, et je n’ai testé aucune des trois. Elles restent non testées ici, et mes services ne les utilisent pas.
Un flux de travail sûr pour tester chaque changement
La partie 1 de la série explique la méthode en détail. Voici la version courte appliquée à l’arborescence. Vous modifiez l’unité par un drop-in, redémarrez, lisez le journal et, si besoin, revenez en arrière :
# Ouvrir (ou créer) le drop-in override.conf d'une unité dans votre éditeur ; systemd le recharge à l'enregistrement sudo systemctl edit myapp.service # Afficher l'unité et tous ses drop-ins, fusionnés, dans l'ordre où systemd les lit systemctl cat myapp.service # Un rechargement ne suffit pas pour un processus en cours : redémarrer le service, puis lire le journal sudo systemctl restart myapp.service journalctl -u myapp.service -n 20 --no-pager -o cat # Annuler : supprimer tous les drop-ins de l'unité sudo systemctl revert myapp.service
Vous pouvez aussi essayer une directive sans aucun fichier. systemd-run -p démarre une unité temporaire avec les propriétés choisies, et systemctl show -p vous dit ce que systemd a réellement appliqué :
# Démarrer une unité temporaire de longue durée avec trois propriétés sudo systemd-run --quiet --unit=demo-hardening-fs-show --collect \ -p ProtectSystem=strict -p ReadWritePaths=/var/tmp -p PrivateTmp=yes sleep 30 # Demander à systemd ce qu'il a réellement appliqué systemctl show demo-hardening-fs-show.service -p ProtectSystem -p ReadWritePaths -p PrivateTmp sudo systemctl stop demo-hardening-fs-show.service systemctl is-active demo-hardening-fs-show.service
ReadWritePaths=/var/tmp PrivateTmp=yes ProtectSystem=strict inactive
systemctl show affiche les propriétés dans son propre ordre, pas dans celui des options -p. Après systemctl stop, l’unité transitoire avait disparu et systemctl is-active répondait « inactive ».
Labo pratique : voir l’arborescence changer
Ce labo montre la protection du système de fichiers à l’œuvre sur un service inoffensif qui essaie seulement de créer un fichier vide à cinq endroits. Vous le lancez huit fois, avec un drop-in différent à chaque fois. Commencez par créer le script et l’unité :
# 1. Un script inoffensif : il essaie seulement de créer un fichier vide à cinq endroits
sudo mkdir /home/demo-hardening-dir
sudo tee /usr/local/bin/demo-hardening-fs.sh > /dev/null <<'EOF'
#!/bin/sh
echo "entries in /home: $(ls -A /home | wc -l)"
for d in /etc /home /home/demo-hardening-dir /tmp /var/tmp; do
if err=$(touch "$d/demo-hardening-test" 2>&1); then echo "$d: write OK"
else echo "$d: write refused (${err##*: })"; fi
done
EOF
sudo chmod 755 /usr/local/bin/demo-hardening-fs.sh
# 2. Un service one-shot qui lance le script
sudo tee /etc/systemd/system/demo-hardening-fs.service > /dev/null <<'EOF'
[Unit]
Description=Filesystem protection demo (harmless)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/demo-hardening-fs.sh
EOF
sudo systemctl daemon-reload
Définissez ensuite une petite fonction d’aide. Elle remplace le drop-in par le texte que vous lui donnez, lance le service et affiche le journal :
# try : remplacer le drop-in par le texte lu sur l'entrée standard, lancer l'unité, afficher les 9 dernières lignes du journal
try() {
sudo rm -rf /etc/systemd/system/demo-hardening-fs.service.d
local c; c="$(cat)"
if [ -n "$c" ]; then
sudo mkdir -p /etc/systemd/system/demo-hardening-fs.service.d
printf '%s\n' "$c" | sudo tee /etc/systemd/system/demo-hardening-fs.service.d/override.conf > /dev/null
fi
sudo systemctl daemon-reload
sudo systemctl start demo-hardening-fs.service
sudo journalctl -u demo-hardening-fs.service -n 9 --no-pager -o cat
# supprimer les fichiers de test créés par le script
sudo rm -f /etc/demo-hardening-test /home/demo-hardening-test /home/demo-hardening-dir/demo-hardening-test \
/tmp/demo-hardening-test /var/tmp/demo-hardening-test
}
Lancez la référence et la première expérience :
# E0 : aucun drop-in, rien n'est restreint : | try # E1 : toute l'arborescence en lecture seule printf '[Service]\nProtectSystem=strict\n' | try
Sans drop-in (E0), les cinq écritures réussissent. Avec ProtectSystem=strict (E1), j’ai obtenu ce journal. Trois écritures sur cinq sont refusées, mais /home et le répertoire de démonstration qu’il contient restent inscriptibles, comme dans la sonde précédente :
Starting demo-hardening-fs.service - Filesystem protection demo (harmless)... entries in /home: 2 /etc: write refused (Read-only file system) /home: write OK /home/demo-hardening-dir: write OK /tmp: write refused (Read-only file system) /var/tmp: write refused (Read-only file system) demo-hardening-fs.service: Deactivated successfully. Finished demo-hardening-fs.service - Filesystem protection demo (harmless).
Ajoutez maintenant PrivateTmp=yes (E2), puis ouvrez /etc (E3), et lisez l’unité fusionnée :
# E2 : ajouter un /tmp et un /var/tmp privés printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\n' | try # E3 : ouvrir aussi /etc printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\nReadWritePaths=/etc\n' | try # L'unité et son drop-in, fusionnés systemctl cat demo-hardening-fs.service --no-pager
# /etc/systemd/system/demo-hardening-fs.service [Unit] Description=Filesystem protection demo (harmless) [Service] Type=oneshot ExecStart=/usr/local/bin/demo-hardening-fs.sh # /etc/systemd/system/demo-hardening-fs.service.d/override.conf [Service] ProtectSystem=strict PrivateTmp=yes ReadWritePaths=/etc
La vue fusionnée prouve que systemd a lu votre drop-in. En E2, /tmp et /var/tmp acceptent à nouveau les écritures. En E3, /etc aussi. Voyons maintenant le répertoire personnel, en quatre expériences :
# E4 : cacher /home complètement printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\nProtectHome=yes\n' | try # E5 : garder /home visible mais en lecture seule printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\nProtectHome=read-only\n' | try # E6 : /home en lecture seule, avec un sous-répertoire inscriptible printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\nProtectHome=read-only\nReadWritePaths=/home/demo-hardening-dir\n' | try # E7 : /home caché, avec le même sous-répertoire inscriptible printf '[Service]\nProtectSystem=strict\nPrivateTmp=yes\nProtectHome=yes\nReadWritePaths=/home/demo-hardening-dir\n' | try
En E4, ProtectHome=yes cache tout, et le répertoire de démonstration est absent :
Starting demo-hardening-fs.service - Filesystem protection demo (harmless)... entries in /home: 0 /etc: write refused (Read-only file system) /home: write refused (Read-only file system) /home/demo-hardening-dir: write refused (No such file or directory) /tmp: write OK /var/tmp: write OK demo-hardening-fs.service: Deactivated successfully. Finished demo-hardening-fs.service - Filesystem protection demo (harmless).
En E6, read-only plus le sous-répertoire exact est la combinaison qui marche :
Starting demo-hardening-fs.service - Filesystem protection demo (harmless)... entries in /home: 2 /etc: write refused (Read-only file system) /home: write refused (Read-only file system) /home/demo-hardening-dir: write OK /tmp: write OK /var/tmp: write OK demo-hardening-fs.service: Deactivated successfully. Finished demo-hardening-fs.service - Filesystem protection demo (harmless).
Ce tableau résume les huit essais. Chaque cellule est une réponse des vrais journaux :
| Essai | /etc | /home | /home/demo-hardening-dir | /tmp | /var/tmp |
|---|---|---|---|---|---|
| E0 aucun drop-in | OK | OK | OK | OK | OK |
E1 ProtectSystem=strict | refusée | OK | OK | refusée | refusée |
E2 + PrivateTmp=yes | refusée | OK | OK | OK | OK |
E3 + ReadWritePaths=/etc | OK | OK | OK | OK | OK |
E4 + ProtectHome=yes | refusée | refusée | absent | OK | OK |
E5 + ProtectHome=read-only | refusée | refusée | refusée | OK | OK |
E6 read-only + sous-répertoire inscriptible | refusée | refusée | OK | OK | OK |
E7 yes + sous-répertoire inscriptible | refusée | refusée | absent | OK | OK |
La ligne E7 est de nouveau le piège : le sous-répertoire inscriptible est invisible sous ProtectHome=yes. Enfin, annulez tout. La première commande montre ce que systemctl revert supprime :
# Annuler le drop-in avec systemctl revert, puis supprimer tout ce que le labo a créé sudo systemctl revert demo-hardening-fs.service sudo rm -f /etc/systemd/system/demo-hardening-fs.service /usr/local/bin/demo-hardening-fs.sh sudo rmdir /home/demo-hardening-dir sudo systemctl daemon-reload ls /etc/systemd/system | grep demo-hardening-fs || echo "no demo-hardening-fs unit file left"
Removed '/etc/systemd/system/demo-hardening-fs.service.d/override.conf'. Removed '/etc/systemd/system/demo-hardening-fs.service.d'. no demo-hardening-fs unit file left
La dernière ligne confirme qu’aucun fichier d’unité ne reste. J’ai aussi vérifié que le script de démonstration, le répertoire de démonstration et les fichiers de test dans /etc, /home, /tmp et /var/tmp avaient disparu.
Un drop-in complet pour une application web typique
Assemblons les morceaux. Ce drop-in convient à un service qui garde ses données dans /var/lib, écrit un journal et enregistre une sauvegarde dans un répertoire supplémentaire. J’ai testé chacune de ses directives séparément, et ProtectSystem, PrivateTmp et ProtectHome ensemble dans le labo ci-dessus. Je n’ai pas lancé ce fichier exact sur une vraie application : adaptez donc les noms et testez-le comme dans la section sur le flux de travail.
[Service] # Rendre toute l'arborescence en lecture seule, puis ouvrir les exceptions ci-dessous ProtectSystem=strict # Cacher /home, /root et /run/user ProtectHome=yes # /tmp et /var/tmp privés, supprimés à l'arrêt du service PrivateTmp=yes # systemd crée /var/lib/myapp, le donne à l'utilisateur du service et le garde inscriptible StateDirectory=myapp # Idem pour /var/log/myapp LogsDirectory=myapp # Un chemin inscriptible de plus ; le tiret initial ignore le chemin s'il n'existe pas encore ReadWritePaths=-/srv/backup/myapp

Chaque chemin qui reste inscriptible dans cette unité est une décision écrite dans le fichier. C’est tout l’intérêt de la protection du système de fichiers : l’unité documente ce que le service peut toucher.
Erreurs courantes
La plupart des échecs de la protection du système de fichiers viennent de six erreurs. Je les ai toutes faites au moins une fois.
Erreur 1 : un chemin inscriptible qui n’existe pas encore
Le service refuse de démarrer avec le code de sortie 226. Créez d’abord le répertoire, utilisez StateDirectory= ou ajoutez le tiret initial.
Erreur 2 : ReadWritePaths sous ProtectHome=yes
L’exception est invisible, sans message. Utilisez ProtectHome=read-only avec le sous-répertoire exact, ou tmpfs avec BindPaths=.
Erreur 3 : strict sans PrivateTmp
/tmp passe en lecture seule et le programme échoue à son premier fichier temporaire. Ajoutez PrivateTmp=yes.
Erreur 4 : se fier à strict pour les répertoires personnels
Sur mon banc, /home et /root sont restés inscriptibles sous strict. Ajoutez ProtectHome= et mesurez.
Erreur 5 : ouvrir tout un répertoire parent
ReadWritePaths=/var ouvre bien plus que ce dont le service a besoin. Nommez le seul sous-répertoire, ou laissez un réglage *Directory= le créer.
Erreur 6 : oublier de redémarrer
Un rechargement ne change pas un processus en cours. Redémarrez le service, puis testez son vrai travail et pas seulement son démarrage.
Une liste de contrôle à suivre
Suivez ces étapes chaque fois que vous ajoutez la protection du système de fichiers à un service.
- Lancez
systemctl cat UNITÉet notez où le service écrit aujourd’hui. - Ajoutez
ProtectSystem=strictetPrivateTmp=yesdans un drop-in. - Ajoutez
ProtectHome=yes, ouread-onlysi le service a besoin d’un chemin sous/home. - Remplacez les répertoires faits à la main par
StateDirectory=,CacheDirectory=etLogsDirectory=. - Ajoutez
ReadWritePaths=pour chaque chemin restant, avec un tiret pour les chemins facultatifs. - Redémarrez le service et exercez son vrai travail, sauvegardes et tâches planifiées comprises.
- Lisez
journalctl -u UNITÉà la recherche de « Read-only file system » et reprenez à l’étape 5. - Gardez le drop-in sous gestion de versions, pour que la décision reste documentée.
Ce que j’en retiens
- La protection du système de fichiers est une liste de décisions. Chaque chemin inscriptible de l’unité est une décision, et l’unité la documente.
- Fermer d’abord, ouvrir ensuite.
strictplus quelques exceptions se relit plus facilement qu’une longue liste d’interdictions. - Les erreurs ont un autre visage. « Read-only file system » et « No such file or directory » signalent le plus souvent une règle du bac à sable, pas une permission.
- Les exceptions demandent des chemins exacts. Le parent d’un chemin n’ouvre pas ce chemin, et
ProtectHome=yesne se rouvre pas. - Laissez systemd créer les répertoires. Les réglages
*Directory=donnent le bon propriétaire et le bon mode. - Ne faites pas confiance aveuglément à une directive. Plusieurs résultats diffèrent ici de mon attente :
/homesousstrict, le propriétaire deConfigurationDirectory=,PrivateTmp=disconnectedviasystemd-runetRestrictFileSystems=. Mesurez sur votre propre système. - Surveillez les échecs silencieux. Une sauvegarde qui ne peut pas écrire échoue sans aucune alarme, comme l’ont montré les archives de 122 octets.
Sources
- systemd.exec(5) : page de manuel freedesktop.org
- systemd-run(1) : page de manuel freedesktop.org
- systemctl(1) : page de manuel freedesktop.org
- journalctl(1) : page de manuel freedesktop.org
- Mes notes de homelab : les entrées Kavita, Navidrome, mktxp, Oxidized, Etherpad et mox de mon wiki (privé).
Plus tôt dans la série
Articles liés
- <a href= »https://blog.neurozone.fr/?p=2045″>Pas à pas : installer, configurer et vérifier un serveur mail avec mox</a>
- <a href= »https://blog.neurozone.fr/?p=2930″>ZDI-CAN-33367 : quand une copie évitée écrit dans un fichier en lecture seule</a>
- <a href= »https://blog.neurozone.fr/?p=2047″>Pas à pas : Suricata en IPS avec NFQUEUE, sans casser son serveur</a>













