Systemd, bien que très décrié, permet la mise en place de certaines mesures de sécurité de manière assez simple. Cet article présente comment Systemd peut intervenir directement dans le hardening de services web en environnement déjà bien sécurisé.

Lors d’un audit de configuration récent, ACCEIS a oeuvré dans un contexte assez particulier : celui du vote électronique à valeur légale. Pour obtenir les agréments et certifications nécessaires et ainsi faire partie de ce secteur, les démarches peuvent être complexes. Par ailleurs,les contraintes sur la traçabilité des actions menées sur les systèmes informatiques sont très fortes. Concrètement, toute action réalisée sur un serveur doit être horodatée et scellée cryptographiquement pour pouvoir être opposable judiciairement.

Contexte

Manpage systemd.exec contenant les options de hardening d'un service systemd.

Extrait des manpages de Systemd (systemd.exec).

Dans le cadre de cet audit, le client dispose de serveurs Nginx portant l’application web depuis laquelle les utilisateurs peuvent prendre parti aux votes et disposer des résultats. Ce serveur Nginx est la seule application présente sur le système. De plus, pour des raisons de sécurité, l’ensemble des recommandations de l’ANSSI et des guides CIS applicables sont mises en oeuvre (excepté la mise en place de SElinux …). Chaque campagne de vote fait l’objet de la mise en place d’un serveur dédié, dont la durée de vie sera très courte : celle du vote (quelques semaines ou mois au maximum).

Dans ce contexte précis, comment implémenter et renforcer les mesures de défense en profondeur du système ? Une piste est de restreindre les droits du service Nginx au strict nécessaire, et pour cela, Systemd est notre allié ! Le choix d’utiliser cette solution se confrontait à une autre : docker. Seulement pour un système à courte durée de vie, où le strict minimum doit être installé pour faire tourner le service web voulu, nous trouvions dommage de rajouter autant de surface d’attaque en installant docker, le tout pour conteneuriser un service qui était de toute façon déjà tout seul sur son socle système.

Cela n’aurait fait que compliquer le problème : un hardening docker spécifique aurait été nécessaire en plus du hardening du socle système sous-jacent.

Hardening Systemd

Systemd supporte un nombre impressionnant de paramètres optionnels, permettant notamment d’améliorer la sécurité du système en sandboxant les services configurés. Et lorsque l’on parle de sandbox, il n’est pas question que d’une manière de parler, certains paramètres de configuration permettent effectivement la conteneurisation du service dans une certaine mesure (chrooting entre autres).

Voici la configuration finale proposée :

[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
ProtectHome=read-only
ReadWritePaths=/home/www/log /var/run
ProtectSystem=full
PrivateDevices=yes
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
PrivateTmp=true

Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID

[Install]
WantedBy=multi-user.target
Unit file Systemd pour Nginx

Grâce à cette configuration, le service ici configuré dispose de plusieurs mesures de sécurité :

  • protectHome=read-only : Tout le filesystem /home apparaît en read-only pour le service.
  • ReadWritePaths : Cette directive permet de redéfinir des dossiers en RW via une liste blanche qui va outrepasser les interdictions définies dans d’autres directives. Ici entre autres, cette liste blanche comprend un dossier dans lequel l’applicatif va écrire des logs.
  • ProtectSystem=full : Cette configuration rend les dossiers /boot, /usr et /etc accessibles en read-only pour le service.
  • PrivateDevices=true : Ne rend pas accessible le filesystem /dev auprès du service, et monte à la place un /dev minimaliste ne contenant que le strict nécessaire (à savoir /dev/zero, /dev/null, /dev/random et autres devices standards). Cela permet de ne jamais exposer les devices physiques à un service qui n’en a pas besoin.
  • PrivateTmp=true : Monte un dossier /tmp propre au service, qu’il sera le seul à pouvoir accéder, et dans lequel ne se trouveront que les fichiers créés depuis ce service.

Des directives permettent de limiter les interactions avec le kernel :

  • ProtectControlGroups=true : Interdit la modification de cgroups.
  • ProtectKernelTunables=yes : Rend toute la configuration kernel read-only (tout le filesystem /proc à peu de choses près).
  • ProtectKernelModules=yes : Rend impossible toute modification des modules kernel chargés (chargement et déchargement).

D’autres directives de sécurité très appréciables sont les suivantes :

  • NoNewPrivileges=true : Empêche toute élévation de privilège de l’applicatif par quelque moyen que ce soit. Cette option est particulièrement intéressante car elle empêche un potentiel attaquant de lancer des exécutables avec des droits élevés, par exemple via un programme SUID appartenant à root.
  • Le combo ReadWritePaths, ReadOnlyPaths, InnaccessiblePaths permet de définir finement, au dossier près, les droits d’accès au filesystem. Ces options peuvent être combinées à la configuration ProtectSystem=strict, qui rend tout le filesystem read-only par défaut.
  • PrivateNetwork et JoinsNamespaceOf permettent de déclarer le service dans un namespace réseau séparé du système. Il est possible de faire cohabiter plusieurs services dans les mêmes namespaces réseaux pour ne pas les exposer au système ou aux autres services. La directive JoinsNamespaceOf attend en paramètre le nom d’un autre service avec lequel partager les namespaces (réseaux entre autres).

Il peut ainsi être assez simple de faire du hardening sur un service disposant déjà d’un UnitFile Systemd. Le plus complexe reste souvent d’identifier la liste de tous les fichiers accédés par le service. Cela peut se faire via strace :

strace --trace=open,openat 2>&1 /usr/sbin/httpd    \
    | pcregrep -o1 'open(?>at)?\(.+?\"(\/.+?)\"' | sort | uniq  
Un one-liner pas très propre pour extraire tous les fichiers faisant l'objet d'une ouverture par le programme voulu.

L’ensemble des options de configuration ainsi que leur documentation officielle peuvent se trouver dans les manpage Systemd, dans la section systemd.exec.