Aller au contenu

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.

Fenêtre de terminal
systemd-analyze security --no-pager | head -15 # score de tous les services
systemd-analyze security monapp --no-pager # détail pour un service

Le 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.

[Service]
User=monapp
Group=monapp
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
CapabilityBoundingSet=
ReadWritePaths=/srv/monapp

Appliquer 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).

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é).

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=monapp
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

Testé : 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é.

[Service]
DynamicUser=yes
StateDirectory=monapp
LogsDirectory=monapp
CacheDirectory=monapp
RuntimeDirectory=monapp

systemd 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).

[Service]
MemoryMax=512M
MemoryHigh=400M
CPUQuota=50%
TasksMax=200
LimitNOFILE=65536
IOWeight=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=any
IPAddressAllow=localhost
IPAddressAllow=10.0.0.0/8
PrivateNetwork=true
  • IPAddressDeny=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).
  1. systemd-analyze security monapp : noter le score de départ.
  2. Ajouter NoNewPrivileges, PrivateTmp, ProtectSystem=strict + ReadWritePaths, ProtectHome.
  3. Redémarrer, tester toutes les fonctions de l’application.
  4. En cas d’erreur Permission denied ou Read-only file system : journalctl -u monapp -e, ajouter le chemin manquant à ReadWritePaths.
  5. Ajouter SystemCallFilter=@system-service puis CapabilityBoundingSet= ; en cas de plantage (SIGSYS), assouplir (SystemCallFilter=~@privileged plutôt que la liste blanche).
  6. Contrôler à nouveau le score.
Fenêtre de terminal
systemctl show monapp -p ProtectSystem,NoNewPrivileges,PrivateTmp,User --no-pager
sudo systemd-run --pty --property=ProtectSystem=strict --property=ReadWritePaths=/tmp /bin/sh # essayer à la main
sudo nsenter -t "$(systemctl show -p MainPID --value monapp)" -m ls /etc # vue du service
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.

  • Manuel : man systemd.exec, man systemd.resource-control, man systemd-analyze
  • Suite : Dépannage