SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-09-15

Sandboxer un service systemd avec ProtectSystem

ProtectSystem, PrivateTmp, DynamicUser et NoNewPrivileges : voyez ce que chaque directive bloque, ce qu’elle casse et comment corriger une unité qui ne démarre plus.

Ce que fait le sandboxing de systemd

Le sandboxing de systemd constitue la seconde moitié de la rédaction d’un unit file. Type= détermine comment un service démarre, tandis que des directives comme ProtectSystem= et PrivateTmp= déterminent ce à quoi il peut accéder une fois lancé. Il s’agit de fonctionnalités du kernel, de mount namespaces et de filtres seccomp, que systemd applique avant que votre processus ne prenne le contrôle. L’application ne les voit jamais et ne nécessite aucune modification de code.

Par défaut, rien n’est appliqué. Une unité sans sandboxing s’exécute avec les droits de root, peut écrire n’importe où, lire tous les fichiers du système et charger un kernel module. Si cette unité est une application web accessible depuis Internet, une faille d’upload de fichier peut compromettre l’ensemble du serveur. Avec l’unité ci-dessous, la même application dispose d’un file system en lecture seule, d’un /home vide, d’un /tmp qu’aucun autre processus ne peut voir et d’aucun moyen d’obtenir les droits de root, même si elle trouve un binaire setuid.

Tout cela s’applique à une seule unité à la fois. Renforcer un service ne protège pas les services voisins. Commencez donc par celui qui écoute sur un port public.

Le fichier d’unité, en une seule fois

notes est un petit service web. Il écoute sur localhost, conserve une base SQLite sous /var/lib/notes et se trouve derrière nginx. Type=exec convient, car le binaire reste au premier plan. La différence entre Type=simple, exec, forking et notify détermine la manière dont systemd suit le démarrage. Chaque directive ci-dessous est expliquée plus bas, avec ce qu’elle empêche et les problèmes qu’elle provoque couramment.

[Unit]
Description=Notes web service
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
ExecStart=/opt/notes/bin/notes --listen 127.0.0.1:8080
DynamicUser=yes
StateDirectory=notes
Environment=NOTES_DB=/var/lib/notes/notes.db

ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectProc=invisible

NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes

ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes

[Install]
WantedBy=multi-user.target

Écrivez-le dans /etc/systemd/system/notes.service, exécutez sudo systemctl daemon-reload, puis sudo systemctl restart notes.service. Pour une unité fournie par un paquet, ne modifiez pas le fichier du fournisseur. sudo systemctl edit notes.service ouvre un drop-in dans /etc/systemd/system/notes.service.d/override.conf. Celui-ci contient uniquement vos ajouts [Service] et est conservé lors des mises à niveau du paquet. systemctl cat notes.service affiche le résultat fusionné, en commençant par le fichier du fournisseur.

Identité d’exécution du service

Sans User=, un service s’exécute en tant que root, et toutes les autres directives servent à limiter les dégâts. Deux solutions permettent d’éviter cela.

Un utilisateur système statique. Créez-le avec sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes, puis définissez User=notes dans l’unité. L’UID reste identique après les redémarrages et les reboots. C’est important lorsque le service possède des fichiers en dehors de son propre répertoire d’état, ou lorsqu’une tâche de sauvegarde doit les lire. Le raisonnement qui justifie un compte non privilégié propre à chaque service s’applique ici sans changement.

DynamicUser=yes. systemd attribue un UID dans une plage réservée au démarrage du service, puis le libère à son arrêt. Aucun compte n’existe sur le disque. Il n’y a donc rien à supprimer lorsque vous retirez le service. Pendant l’exécution du service, getent passwd notes résout le nom via le module NSS (name service switch) de systemd. Après l’arrêt du service, le nom disparaît.

DynamicUser=yes active également quatre autres paramètres : RemoveIPC=yes, PrivateTmp=yes, ProtectSystem=strict et ProtectHome=read-only. Cela fournit l’essentiel d’une sandbox avec une seule ligne. C’est pourquoi l’unité d’exemple reste courte.

Ce que cela casse. Un UID dynamique ne peut pas posséder des fichiers dans des emplacements arbitraires, car le nombre est réattribué à d’autres services. Les données persistantes doivent passer par StateDirectory=, CacheDirectory= ou LogsDirectory=, que systemd crée et dont il modifie le propriétaire avec chown pour l’UID courant à chaque démarrage. Avec DynamicUser=yes, le répertoire réel est /var/lib/private/notes, et /var/lib/notes est un lien symbolique qui pointe vers celui-ci. /var/lib/private a le mode 0700 et appartient à root. Une sauvegarde exécutée par un utilisateur normal obtient donc Permission denied sur un chemin qui semble parfaitement lisible pour root. Tout élément qui nécessite un propriétaire fixe, une clé SSH, une exportation NFS ou un fichier lu par un second service doit utiliser un utilisateur statique.

À quoi ressemble le système de fichiers : ProtectSystem, ProtectHome, PrivateTmp

ProtectSystem= accepte trois valeurs. yes monte /usr et les répertoires de démarrage en lecture seule. full ajoute /etc. strict monte toute la hiérarchie en lecture seule, à l’exception des répertoires d’API du noyau /dev, /proc et /sys, que d’autres directives couvrent. Commencez par strict et ouvrez des exceptions, car commencer avec une configuration permissive pour la resserrer ensuite n’arrive jamais.

Les exceptions sont ReadWritePaths=/srv/notes/uploads. Vous en avez besoin de moins que prévu : StateDirectory=, LogsDirectory=, CacheDirectory= et RuntimeDirectory= restent automatiquement accessibles en écriture avec ProtectSystem=strict, ce qui explique pourquoi l’unité d’exemple ne contient aucune ligne ReadWritePaths=. Avec strict, /tmp est également en lecture seule, sauf si PrivateTmp=yes fournit à l’unité son propre répertoire accessible en écriture.

Ce que cela casse. Toute écriture en dehors de ces chemins échoue avec Read-only file system. Les applications qui réenregistrent leur configuration dans /etc, créent directement un fichier PID dans /run ou décompressent un plugin dans /opt sont concernées. Relevez le chemin dans l’erreur et ajoutez ce chemin précis, pas son répertoire parent. Un chemin indiqué dans ReadWritePaths= qui n’existe pas provoque un échec du démarrage, et non un avertissement. Préfixez donc les chemins facultatifs par un tiret : ReadWritePaths=-/srv/notes/uploads.

La lecture seule ne signifie pas que le contenu est masqué. Avec ProtectSystem=strict, le service peut toujours lire /etc/passwd ainsi que tout secret lisible par tous appartenant à une autre application. InaccessiblePaths=/etc/ssh /srv/otherapp retire complètement un sous-arbre de la vue de l’unité. Pour les secrets du service, LoadCredential=dbpass:/etc/notes/dbpass copie le fichier dans un répertoire propre à l’unité, que seul ce service peut lire. L’application le trouve sous $CREDENTIALS_DIRECTORY.

ProtectHome=yes fait apparaître /home, /root et /run/user comme des répertoires vides. Un service web n’a aucune raison d’accéder à un répertoire personnel. Cela empêche une vulnérabilité de traversée de répertoires d’atteindre /root/.ssh. read-only et tmpfs sont les valeurs moins restrictives. Cette protection casse les applications dont les données résident réellement dans un répertoire personnel, ce qui concerne de nombreuses applications installées manuellement sous /home/app. Déplacez les données vers /var/lib, ou définissez ProtectHome=read-only et acceptez une protection moindre.

PrivateTmp=yes fournit au service son propre /tmp et son propre /var/tmp. Ils sont créés au démarrage du service et supprimés à son arrêt. Cela élimine toute la catégorie des problèmes de concurrence liés aux fichiers temporaires entre services. Après un crash, les secrets ne peuvent plus rester dans un répertoire que tous les utilisateurs de la machine peuvent lister.

Ce que cela casse. Tout ce qui utilise /tmp comme point de rendez-vous. Un service configuré pour joindre MySQL via /tmp/mysql.sock renvoie désormais Can't connect to local MySQL server through socket '/tmp/mysql.sock', car la base de données a créé son socket dans le /tmp de l’hôte alors que le service le cherche dans son propre répertoire. Pointez-le vers 127.0.0.1 ou vers le chemin réel du socket sous /run. Le même problème apparaît lors du dépannage : les fichiers que le service écrit dans /tmp n’apparaissent pas dans le /tmp de votre shell. Pour les consulter, entrez dans l’espace de noms de montage du service.

pid=$(systemctl show --property=MainPID --value notes.service)
sudo nsenter --target "$pid" --mount ls -l /tmp

PrivateDevices=yes remplace /dev par un petit ensemble de pseudo-périphériques comme /dev/null, /dev/zero et /dev/urandom. Les périphériques physiques ne sont tout simplement pas présents. Les nœuds de disque, /dev/kvm, /dev/net/tun, les cartes son et les GPU disparaissent tous. Un serveur multimédia qui effectue un transcodage matériel ne peut donc pas ouvrir /dev/dri/renderD128 et bascule vers le logiciel ou s’arrête. Lorsqu’un service a réellement besoin d’un nœud de périphérique, désactivez PrivateDevices= pour cette unité et indiquez le nœud avec DeviceAllow=/dev/dri/renderD128 rw. Cette configuration reste beaucoup plus restrictive que la valeur par défaut, qui autorise tous les périphériques du système.

Ce que le service peut devenir : NoNewPrivileges et capabilities

NoNewPrivileges=yes définit un indicateur de processus que le kernel ne réinitialise jamais. À partir de ce moment, le processus et chacun de ses processus enfants ne peuvent plus obtenir de privilèges via un binaire setuid ou une file capability. C’est la ligne unique la plus importante de l’unité, car elle arrête la plupart des chaînes d’escalade de privilèges locale dès leur première étape.

Ce que cela casse. Tout ce qui appelle sudo depuis le service, qui affiche alors sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?. Les vérifications de mot de passe PAM (pluggable authentication modules) qui lancent unix_chkpwd échouent de la même manière. Il en va de même pour les outils de conteneurs rootless qui ont besoin de newuidmap. Si votre service dépend de l’un de ces éléments, supprimez cette dépendance plutôt que la directive.

CapabilityBoundingSet= limite les capabilities que les processus de l’unité peuvent détenir. Une affectation vide les supprime toutes. Pour un service qui s’exécute déjà avec un utilisateur non-root, c’est un deuxième verrou plutôt que le premier, puisque NoNewPrivileges=yes bloque la méthode habituelle pour obtenir une capability. Conservez les deux, car ils échouent différemment et vous voulez que l’échec soit détecté deux fois.

La capability dont un service web a souvent besoin est CAP_NET_BIND_SERVICE, pour utiliser un port inférieur à 1024. Un processus non-root doit la recevoir, et pas seulement être autorisé à l’utiliser. Les deux lignes sont donc nécessaires.

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

systemd applique les capabilities ambiantes avant de supprimer les privilèges. Cette configuration fonctionne donc avec NoNewPrivileges=yes. Sans ces lignes, le service démarre, puis se termine avec une erreur indiquant le port, par exemple listen tcp :443: bind: permission denied. Sur la plupart des VPS, il vaut mieux écouter sur 127.0.0.1:8080 et laisser nginx ou Caddy gérer 443. La capability ne figure ainsi pas du tout dans l’unité. Un ~ en tête inverse la liste : CapabilityBoundingSet=~CAP_SYS_ADMIN bloque cette capability et autorise les autres. Préférez la forme d’autorisation. Une liste d’interdiction vieillit mal, car de nouvelles capabilities sont régulièrement ajoutées.

Ce que le noyau expose

ProtectKernelTunables=yes rend /proc/sys, /sys et les fichiers accessibles en écriture qu’ils contiennent accessibles en lecture seule pour cette unité. Cela interrompt tout service qui définit un paramètre sysctl au démarrage : le script de démarrage affiche sysctl: setting key "vm.max_map_count": Read-only file system, puis abandonne. Placez plutôt la valeur dans /etc/sysctl.d/, où elle doit être définie et où elle sera conservée après un redémarrage, puis laissez la directive activée.

ProtectKernelModules=yes bloque le chargement des modules. Une unité qui exécute modprobe reçoit modprobe: ERROR: could not insert 'nf_conntrack': Operation not permitted. Chargez le module au démarrage via /etc/modules-load.d/ plutôt que depuis le service.

ProtectKernelLogs=yes supprime dmesg. ProtectControlGroups=yes rend /sys/fs/cgroup accessible en lecture seule, ce que les runtimes de conteneurs et tout composant qui gère ses propres cgroups détectent immédiatement. ProtectProc=invisible masque les processus des autres utilisateurs dans /proc. Un service compromis ne peut donc pas lire la ligne de commande d’un autre daemon ni le mot de passe qu’une personne lui a transmis. Les agents de supervision qui parcourent /proc sont ceux pour lesquels cette directive doit être désactivée.

RestrictNamespaces=yes empêche le service de créer de nouveaux namespaces. C’est nécessaire à un runtime de conteneurs et c’est aussi ce qu’un attaquant utilise pour construire une évasion. LockPersonality=yes bloque la modification du domaine d’exécution du noyau, et RestrictSUIDSGID=yes empêche le service de créer des fichiers setuid. Ces deux directives sont peu coûteuses et interrompent rarement une application classique.

Ce que le service peut contacter

RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 autorise les sockets locaux ainsi qu’IPv4 et IPv6, et fait échouer socket() avec EAFNOSUPPORT pour tout le reste. Il s’agit d’un filtre seccomp. Il fonctionne donc sur x86-64 et arm64, ce qui couvre tous les VPS actuels.

Ce que cela casse. AF_NETLINK, beaucoup plus souvent qu’on ne le pense. Le getifaddrs() de glibc utilise une socket netlink. Les chemins d’énumération des interfaces des runtimes Go, Java et .NET l’utilisent également. Un service qui voulait seulement connaître sa propre adresse IP échoue alors avec OSError: [Errno 97] Address family not supported by protocol ou l’équivalent dans son langage. Dans ce cas, la liste devient AF_UNIX AF_INET AF_INET6 AF_NETLINK, ce qui reste beaucoup plus restrictif que la configuration par défaut. AF_PACKET est nécessaire à la capture de paquets bruts. Presque aucun autre service ne devrait jamais en disposer.

IPAddressDeny=any avec IPAddressAllow=localhost repose sur un mécanisme différent : un filtre BPF attaché au cgroup de l’unité. Il s’applique à chaque unité et reste invisible pour nft list ruleset. C’est donc une bonne solution pour un service qui ne doit accéder qu’à une base de données située sur le même hôte, mais elle peut compliquer le diagnostic pour l’administrateur suivant. Sur un noyau dépourvu de la prise en charge de cgroup BPF, systemd consigne que l’unité configure un pare-feu IP alors que le système local ne prend pas en charge le filtrage BPF/cgroup. Les règles restent alors inactives sans message évident. Consultez donc le journal au lieu de le supposer.

Pourquoi une unité durcie cesse-t-elle de démarrer ?

Chaque directive ci-dessus peut transformer un service fonctionnel en service défaillant, et l’erreur ne ressemble souvent pas du tout à la directive qui l’a provoquée. La méthode est toujours la même. Consultez le journal, assouplissez une seule directive, puis testez à nouveau.

sudo systemctl restart notes.service
systemctl status notes.service
sudo journalctl -u notes.service -n 50 --no-pager

Les lignes qui définissent le sandbox se présentent ainsi :

notes.service: Failed to set up mount namespacing: No such file or directory
notes.service: Failed at step NAMESPACE spawning /opt/notes/bin/notes: No such file or directory
notes.service: Main process exited, code=exited, status=226/NAMESPACE

226/NAMESPACE signifie que systemd n’a pas pu construire la vue du système de fichiers. Votre binaire n’a donc jamais été exécuté. La cause habituelle est un chemin indiqué dans ReadWritePaths=, BindPaths= ou InaccessiblePaths= qui n’existe pas. 228/SECCOMP indique que l’application de SystemCallFilter= ou SystemCallArchitectures= a échoué. code=killed, status=31/SYS est différent : le processus a démarré, puis a exécuté un appel système refusé par le filtre, et le kernel l’a tué. Gardez la référence des codes de sortie systemd pour les unités qui ne démarrent pas ouverte pendant vos vérifications, car le nombre permet de distinguer rapidement un problème de namespace d’un problème applicatif.

Assouplissez une seule directive à la fois, dans un drop-in, afin que le résultat soit exploitable. Exécutez sudo systemctl edit notes.service et ajoutez-y une seule ligne :

[Service]
ProtectSystem=full

Redémarrez le service. S’il démarre maintenant, vous savez quelle directive restreindre au lieu de la supprimer. Remettez-la à strict, ajoutez la ligne ReadWritePaths= pour le seul chemin dont l’application a réellement besoin, puis redémarrez à nouveau. Supprimer le bloc parce que le service ne démarrait pas est le meilleur moyen de laisser une unité sans protection et avec un commentaire dont plus personne ne se souvient.

Pour tester un sandbox sans utiliser de service, lancez un shell à l’intérieur :

sudo systemd-run --pty -p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /bin/bash

Dans ce shell, touch /etc/test renvoie Read-only file system et ls /home n’affiche rien. C’est le moyen le plus rapide de voir ce qu’une application pourra utiliser, car vous pouvez exécuter ses commandes manuellement et observer celle qui échoue.

Un mode d’échec ne produit aucune erreur. Une directive mal orthographiée génère seulement un avertissement, puis le service démarre sans l’appliquer :

/etc/systemd/system/notes.service:14: Unknown key name 'ProtectSytem' in section 'Service', ignoring.

L’unité fonctionne, le sandbox est absent et aucun autre message ne le signale. Deux commandes permettent de le détecter. sudo systemd-analyze verify /etc/systemd/system/notes.service affiche cet avertissement à la demande, et systemctl show affiche ce que le service en cours d’exécution a réellement reçu :

systemctl show notes.service -p User -p ProtectSystem -p PrivateTmp -p NoNewPrivileges

Si cette commande renvoie ProtectSystem=no alors que vous avez écrit strict, l’unité n’a pas chargé ce que vous pensez.

Utiliser systemd-analyze security comme liste de contrôle

sudo systemd-analyze security notes.service affiche tous les paramètres de sandboxing qu’une unité peut utiliser, indique ce que cette unité fait actuellement avec chacun d’eux et fournit une évaluation pour chaque ligne. Exécutez la commande sans argument pour lister tous les services chargés sur le serveur. Ajoutez --offline=true suivi du chemin d’une unité pour vérifier son fichier avant de l’installer.

Lisez la sortie comme une liste de tâches à effectuer. Parcourez les lignes signalées et posez-vous une seule question pour chacune : ce service a-t-il besoin de cet accès ? La plupart du temps, la réponse est non et vous ajoutez la directive correspondante. Parfois, la réponse est oui. Un serveur multimédia a besoin d’un nœud de périphérique. Un agent de sauvegarde doit pouvoir lire /home. Ces lignes resteront signalées en permanence. C’est le résultat attendu, pas un échec.

Le nombre récapitulatif final correspond à une synthèse des lignes précédentes. Il ne sait pas ce que fait votre service, quelles données il contient ni si l’application comporte un bug. Une unité peut satisfaire toutes les vérifications de l’outil et rester malgré tout l’élément le plus faible du serveur, car l’outil mesure l’exposition du fichier d’unité, et non celle du logiciel. Un gestionnaire de mots de passe auto-hébergé illustre clairement cette différence : une unité Vaultwarden peut satisfaire toutes les vérifications alors que son token d’administration et son fichier de sauvegarde déterminent toujours si le coffre est réellement protégé. Chercher à améliorer ce nombre pousse à copier-coller des directives que l’on ne comprend pas. Ce sont précisément celles qui cassent après une mise à niveau de paquet, sans personne capable d’expliquer pourquoi la ligne avait été ajoutée.

Limites du sandboxing

Ces directives contrôlent les ressources auxquelles un service peut accéder. Elles ne disent rien de la quantité de ressources qu’il peut consommer. Une unité entièrement isolée peut donc toujours utiliser tous les cœurs et toute la mémoire de la machine. Il s’agit d’un autre ensemble de paramètres, présenté dans le guide des limites CPU et mémoire pour un service systemd.

Elles ne remplacent pas non plus le contrôle d’accès obligatoire. Un namespace est propre à chaque unité et défini par la personne qui écrit le fichier d’unité, tandis que SELinux applique une politique à l’ensemble du système. Les deux mécanismes sont complémentaires. Aucun ne dispense de l’autre.

Enfin, ces directives couvrent uniquement les processus que systemd démarre dans cette unité. Un service qui délègue le traitement à un daemon auxiliaire via un socket n’a pas isolé ce daemon. Ajoutez également le même bloc dans l’unité concernée, puis vérifiez le résultat avec systemctl show au lieu de vous fier au fichier.

FAQ

Que rend réellement le paramètre ProtectSystem=strict accessible en lecture seule ?

Toute la hiérarchie du système de fichiers, à l’exception de /dev, /proc et /sys, qui sont couverts respectivement par PrivateDevices=, ProtectKernelTunables= et ProtectControlGroups=. Cela inclut /etc, /var, /srv, /opt et /tmp. Les exceptions que systemd rétablit automatiquement correspondent aux répertoires qu’il gère : StateDirectory=, CacheDirectory=, LogsDirectory= et RuntimeDirectory=. Tout autre emplacement dans lequel le service doit écrire nécessite une entrée ReadWritePaths= explicite. Notez que lecture seule ne signifie pas illisible : un fichier contenant un secret situé ailleurs sur le serveur reste donc accessible au service, sauf si vous le mentionnez dans InaccessiblePaths=.

Pourquoi mon service échoue-t-il avec status=226/NAMESPACE ?

systemd n’a pas pu créer le mount namespace, et l’exécutable n’a donc jamais démarré. La ligne précédente du journal contient généralement Failed to set up mount namespacing: No such file or directory. Dans presque tous les cas, un chemin indiqué dans ReadWritePaths=, BindPaths= ou InaccessiblePaths= n’existe pas sur le disque. Créez le répertoire ou préfixez l’entrée par un tiret (ReadWritePaths=-/srv/notes/uploads) pour que systemd l’ignore lorsqu’il est absent. Si le chemin existe, vérifiez qu’il n’y a pas de faute de frappe dans l’unité et confirmez la configuration avec systemctl cat notes.service, car un drop-in peut ajouter une ligne que vous ne voyez pas.

Un service utilisant PrivateTmp peut-il quand même partager des fichiers via /tmp ?

Non, c’est précisément le but de cette option. Le service obtient un /tmp et un /var/tmp distincts, qui n’existent que pendant son exécution. Un socket ou un fichier créé par un autre processus dans le /tmp de l’hôte lui est donc invisible. Un socket de base de données situé dans /tmp/mysql.sock est le cas le plus courant. Pour corriger le problème, connectez-vous via 127.0.0.1 ou indiquez au client le véritable socket situé sous /run. Pour inspecter les fichiers temporaires du service, récupérez son PID principal avec systemctl show --property=MainPID --value, puis entrez dans son mount namespace avec sudo nsenter --target <pid> --mount.

Comment un service sandboxé peut-il écouter sur le port 443 sans root ?

Accordez-lui une seule capability au lieu de l’ensemble des privilèges du compte root. Définissez AmbientCapabilities=CAP_NET_BIND_SERVICE avec CapabilityBoundingSet=CAP_NET_BIND_SERVICE, conservez User= ou DynamicUser=yes, et le processus pourra effectuer un bind sur les ports inférieurs à 1024, sans rien de plus. Définir uniquement le bounding set est l’erreur la plus courante : la capability est alors autorisée, mais jamais accordée, et le service s’arrête avec une erreur de permission indiquant le port. Sur un VPS qui exécute déjà un reverse proxy, il est plus propre de faire écouter le service sur 127.0.0.1:8080 et de laisser nginx gérer le port 443. L’unité n’a alors besoin d’aucune capability.

Dois-je utiliser DynamicUser plutôt que créer un utilisateur système ?

Utilisez-le lorsque le service conserve toutes ses données dans StateDirectory=, CacheDirectory= ou LogsDirectory=, ce qui couvre la plupart des petites applications web auto-hébergées. Vous obtenez un UID qui n’existe que pendant l’exécution du service, ainsi que PrivateTmp=, ProtectSystem=strict, ProtectHome=read-only et RemoveIPC= activés automatiquement. Utilisez un utilisateur système statique lorsque l’UID doit rester stable : fichiers appartenant à l’utilisateur en dehors de ces répertoires, clé SSH, montage NFS ou second processus qui lit les mêmes données. N’oubliez pas qu’avec DynamicUser=yes, les données se trouvent réellement dans /var/lib/private/notes. Ce répertoire est en mode 0700 et appartient à root, ce qui fait échouer les tâches de sauvegarde exécutées sans privilèges root.