systemd : durcissement d'un service
systemd peut isoler un service pour limiter les dégâts en cas de faille : systèmes de fichiers en lecture seule, privilèges retirés, appels système filtrés. Les exemples utilisent un service fictif monapp.
Mesurer : systemd-analyze security
Section intitulée « Mesurer : systemd-analyze security »systemd-analyze security --no-pager | head -15 # score de tous les servicessystemd-analyze security monapp --no-pager # détail pour un serviceLe score va de 0 (très sécurisé) à 10 (exposé). Chaque directive de durcissement absente est listée avec son poids. Un score inférieur à 5 est un bon objectif pour un service exposé ; il ne s’agit pas d’atteindre 0 à tout prix.
Un point de départ solide
Section intitulée « Un point de départ solide »[Service]User=monappGroup=monappNoNewPrivileges=truePrivateTmp=truePrivateDevices=trueProtectSystem=strictProtectHome=trueProtectKernelTunables=trueProtectKernelModules=trueProtectControlGroups=trueRestrictSUIDSGID=trueRestrictRealtime=trueLockPersonality=trueRestrictAddressFamilies=AF_INET AF_INET6 AF_UNIXSystemCallFilter=@system-serviceCapabilityBoundingSet=ReadWritePaths=/srv/monappAppliquer progressivement, une directive à la fois, en testant après chacune (sudo systemctl daemon-reload && sudo systemctl restart monapp, puis systemctl status et journalctl -u monapp).
Signification des directives
Section intitulée « Signification des directives »| Directive | Effet |
|---|---|
NoNewPrivileges=true |
Le processus et ses enfants ne peuvent pas gagner de privilèges (pas de setuid) |
PrivateTmp=true |
/tmp et /var/tmp privés au service |
PrivateDevices=true |
Pas d’accès aux périphériques physiques (/dev minimal) |
ProtectSystem=strict |
Tout le système de fichiers en lecture seule, sauf ReadWritePaths |
ProtectSystem=full |
/usr, /boot et /etc en lecture seule (moins strict) |
ProtectHome=true |
/home, /root et /run/user inaccessibles (read-only : en lecture seule) |
ReadWritePaths=/srv/monapp |
Chemins restés en écriture avec ProtectSystem=strict |
ReadOnlyPaths=, InaccessiblePaths= |
Chemins en lecture seule, ou invisibles |
ProtectKernelTunables, ProtectKernelModules, ProtectControlGroups |
Interdit de modifier /proc/sys, de charger des modules, de modifier les cgroups |
RestrictAddressFamilies=… |
Types de sockets autorisés (retirer AF_NETLINK, AF_PACKET si inutiles) |
SystemCallFilter=@system-service |
Limite les appels système à un ensemble courant pour un service (man systemd.exec) |
CapabilityBoundingSet= |
Retire toutes les capacités du noyau (vide = aucune) |
LockPersonality, RestrictRealtime, RestrictSUIDSGID |
Interdictions diverses |
Vérification : avec ProtectSystem=strict et ReadWritePaths=/srv/monapp, un service qui écrit dans /srv/monapp réussit, et dans /etc échoue avec Read-only file system (testé).
Ports inférieurs à 1024 sans root
Section intitulée « Ports inférieurs à 1024 sans root »Un service non root ne peut pas écouter sur le port 80. Au lieu de le lancer en root, lui accorder une seule capacité :
[Service]User=monappAmbientCapabilities=CAP_NET_BIND_SERVICECapabilityBoundingSet=CAP_NET_BIND_SERVICENoNewPrivileges=trueTesté : un serveur HTTP lancé par un utilisateur sans privilège écoute bien sur le port 80 avec cette configuration. En pratique, un reverse proxy (Caddy, Nginx) écoute sur 80/443 et l’application reste sur un port élevé.
Utilisateur éphémère : DynamicUser
Section intitulée « Utilisateur éphémère : DynamicUser »[Service]DynamicUser=yesStateDirectory=monappLogsDirectory=monappCacheDirectory=monappRuntimeDirectory=monappsystemd crée un utilisateur système temporaire pour la durée du service, sans entrée dans /etc/passwd, et gère les dossiers :
| Directive | Dossier créé |
|---|---|
StateDirectory=monapp |
/var/lib/monapp (données persistantes) |
LogsDirectory=monapp |
/var/log/monapp |
CacheDirectory=monapp |
/var/cache/monapp |
RuntimeDirectory=monapp |
/run/monapp (supprimé à l’arrêt) |
DynamicUser=yes active automatiquement ProtectSystem=strict, PrivateTmp et NoNewPrivileges. Convient aux services sans fichiers partagés avec d’autres utilisateurs. Testé : le service s’exécute sous un identifiant temporaire et écrit dans son StateDirectory (/var/lib/monapp, lien vers /var/lib/private/monapp).
Limiter les ressources
Section intitulée « Limiter les ressources »[Service]MemoryMax=512MMemoryHigh=400MCPUQuota=50%TasksMax=200LimitNOFILE=65536IOWeight=100| Directive | Effet |
|---|---|
MemoryMax |
Limite dure : le processus est tué (oom-kill) au-delà |
MemoryHigh |
Limite souple : le service est ralenti au-delà |
CPUQuota=50% |
Pas plus de la moitié d’un cœur |
TasksMax |
Nombre maximal de processus et threads (protège d’une fork bomb) |
LimitNOFILE |
Nombre de fichiers ouverts |
Contrôle : systemctl show monapp -p MemoryMax,CPUQuotaPerSecUSec ; la consommation en direct avec systemd-cgtop.
[Service]IPAddressDeny=anyIPAddressAllow=localhostIPAddressAllow=10.0.0.0/8PrivateNetwork=trueIPAddressDeny=any+IPAddressAllow=…: liste blanche d’adresses (filtrage par le noyau).PrivateNetwork=true: le service n’a aucun accès réseau (utile pour une tâche locale).
Méthode pour durcir un service existant
Section intitulée « Méthode pour durcir un service existant »systemd-analyze security monapp: noter le score de départ.- Ajouter
NoNewPrivileges,PrivateTmp,ProtectSystem=strict+ReadWritePaths,ProtectHome. - Redémarrer, tester toutes les fonctions de l’application.
- En cas d’erreur
Permission deniedouRead-only file system:journalctl -u monapp -e, ajouter le chemin manquant àReadWritePaths. - Ajouter
SystemCallFilter=@system-servicepuisCapabilityBoundingSet=; en cas de plantage (SIGSYS), assouplir (SystemCallFilter=~@privilegedplutôt que la liste blanche). - Contrôler à nouveau le score.
Vérifier l’effet des restrictions
Section intitulée « Vérifier l’effet des restrictions »systemctl show monapp -p ProtectSystem,NoNewPrivileges,PrivateTmp,User --no-pagersudo systemd-run --pty --property=ProtectSystem=strict --property=ReadWritePaths=/tmp /bin/sh # essayer à la mainsudo nsenter -t "$(systemctl show -p MainPID --value monapp)" -m ls /etc # vue du serviceDépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
Read-only file system |
Chemin d’écriture absent de ReadWritePaths (ou utiliser StateDirectory) |
status=226/NAMESPACE |
Un chemin de ReadWritePaths, ReadOnlyPaths ou BindPaths n’existe pas : le créer, ou préfixer par - (ReadWritePaths=-/chemin) |
status=31/SYS ou Bad system call |
SystemCallFilter trop strict : voir journalctl -k / audit, assouplir |
Operation not permitted |
Capacité retirée par CapabilityBoundingSet : en ajouter une précise |
L’application ne peut plus écrire dans /tmp |
PrivateTmp=true : /tmp est privé (utiliser StateDirectory pour des fichiers partagés) |
| Impossible d’écouter sur le port 80 | AmbientCapabilities=CAP_NET_BIND_SERVICE (voir plus haut) |
Le service ne trouve plus /home/utilisateur/... |
ProtectHome=true : déplacer les données dans /srv ou StateDirectory |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Les directives et les scores de systemd-analyze security sont comparables sur systemd 255 et 259 (un même service a obtenu le même score de 4,5 sur les deux versions lors des tests). Sur Ubuntu 26.04, /tmp est en mémoire : PrivateTmp=true donne aussi à un service un /tmp privé en mémoire.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Manuel :
man systemd.exec,man systemd.resource-control,man systemd-analyze - Suite : Dépannage