Durcir un système ne se limite pas à configurer des permissions — cela commence, en amont, par des choix de conception qui déterminent à quel point une compromission locale peut se propager.

Conception monolithique contre compartimentée

Dans une conception monolithique, un seul module implémente l’ensemble des fonctionnalités. Le défaut structurel saute aux yeux : si l’une des fonctionnalités est compromise, c’est tout le système qui l’est avec elle.

L’exemple contrastant de Sendmail et Qmail

Le serveur de messagerie Sendmail, historiquement fondé sur la confiance, a longtemps omis la sécurité du cadre même de son développement. Les fonctionnalités réseau, les entrées utilisateur, l’affichage, les entrées-sorties fichiers — tout transitait par un unique processus s’exécutant en tant que root, faisant de chaque composant une porte d’entrée potentielle vers un privilège maximal.

Qmail répond à cette faiblesse par une conception radicalement compartimentée. L’isolation s’appuie directement sur les mécanismes de l’OS : chaque module s’exécute sous un utilisateur distinct, chacun n’ayant accès qu’à des ressources spécifiques. Le principe de moindre privilège y est poussé à son terme — un minimum de privilèges par UID, un seul programme setuid, un seul programme exécuté en tant que root. La leçon est directe : compartimenter au niveau du système d’exploitation lui-même, plutôt que de faire reposer toute la sécurité sur la seule qualité du code applicatif.

La règle des deux (Google)

Face au besoin de traiter du code potentiellement non fiable — un parseur, par exemple —, une règle simple s’impose : éviter de se retrouver simultanément à l’intersection de trois conditions dangereuses — du code qui traite une entrée non sûre, écrit dans un langage non sûr (C, C++), et qui ne s’exécute pas dans un sandbox (comme un navigateur). Réunir ces trois facteurs à la fois, c’est maximiser la probabilité qu’une vulnérabilité de parsing devienne une compromission complète.

Minimiser la surface d’attaque

Le principe se décline à plusieurs échelles. Au niveau du code : le nombre d’erreurs croît avec la taille du code, des routines simples et réutilisables valent mieux qu’un bloc monolithique conséquent, et la même logique s’applique aux dépendances — privilégier des bibliothèques simples, n’utiliser que le strict nécessaire.

Au niveau d’un système Linux, la minimisation s’opère selon trois axes : réduire les paquets installés, réduire les services en cours d’exécution, et réduire les fonctionnalités configurées par service.

Minimiser l’installation elle-même

Deux familles de distributions Linux abordent différemment cette réduction : les distributions à base de paquets (Debian, Red Hat), et les distributions à base de sources (Gentoo), qui permettent une compilation sur mesure n’incluant que le strict nécessaire.

TIP

L’usage de serveurs minimaux est la norme en environnement conteneurisé — Alpine Linux en est l’exemple le plus connu — avec un double bénéfice : une taille d’artefact réduite, et une surface d’attaque mécaniquement plus faible.

L’objectif ultime de cette démarche est de prévenir les attaques de type Living-Off-The-Land (LOTL), qui détournent des binaires légitimes déjà présents sur le système à des fins malveillantes — le projet GTFOBins en catalogue de nombreux exemples. Moins un système embarque de binaires exploitables de la sorte, moins cette classe d’attaque dispose de matière première.

Concrètement, on peut privilégier des paquets alternatifs allégés (vim-tiny plutôt que vim complet), supprimer la documentation et les compilateurs superflus du système en production, et supprimer les modules noyau inutiles — voire compiler un noyau monolithique sur mesure, taillé précisément pour le matériel et les usages réels de la machine.

Réduire la surface réseau en pratique

Deux commandes structurent cette réduction concrète. Lister les services actifs :

systemctl list-units --type=service

Et identifier ceux qui écoutent effectivement sur le réseau — la surface réellement exposée à un attaquant distant :

ss -latupn | grep LISTEN

Le modèle de permission : liste blanche plutôt que liste noire

Un modèle en liste d’autorisation (allow list) doit systématiquement être préféré à une liste d’interdiction, car il évite les omissions par construction. Deux conditions le rendent efficace : un refus par défaut de tout ce qui n’est pas explicitement listé, et des permissions à grain fin, nommément désignées plutôt que génériques.

WARNING

Les caractères génériques (wildcards) au sein d’une liste d’autorisation en sapent tout l’intérêt — ils réintroduisent, par la bande, la logique de liste noire qu’on cherchait précisément à éviter.