SELinux Sans Peur: Politiques Personnalisees pour Services Critiques
De l'audit avec audit2allow aux modules de policy versionnes maintenus en production, sans tomber dans le permissive eternel.

A chaque fois qu'un service casse sur RHEL ou Rocky, le reflexe de l'astreinte est le meme: setenforce 0, probleme regle, ticket ferme. Six mois plus tard le cluster entier tourne en permissive, personne ne se rappelle pourquoi, et le rapport de conformite devient de la science-fiction. L'equipe Basilisk OffSec a passe deux ans a demolir des environnements comme ca en red teams autorises, et la conclusion est nette: un SELinux desactive est l'un des chemins les plus fiables d'une simple RCE a la compromission totale. Ce billet montre comment ecrire des policies personnalisees pour des services critiques sans casser la prod, et pourquoi setenforce 0 n'est pas un correctif mais une facture reportee.
Enforcing plutot que permissive: pourquoi ca compte
SELinux est du Mandatory Access Control: meme quand un processus tourne en root, la policy limite les types qu'il peut lire, ecrire et executer. C'est exactement ce qui casse une chaine d'exploit, car un httpd_t compromis ne peut tout simplement pas lire shadow_t ni ecrire dans bin_t. Permissive ne fait que journaliser, il ne bloque rien, donc un cluster en permissive est fonctionnellement sans protection. L'etat cible est toujours Enforcing, verifiable avec getenforce et sestatus. Un hote enforcing bien tenu n'est pas un obstacle a l'exploitation, c'est la derniere defense de perimetre quand une vulnerabilite applicative est exploitee; il complete le travail de base de Hardening de Serveur Linux: CIS Benchmark Applique Sans Casser la Prod.
Lire la policy existante avant d'ecrire
Avant d'ecrire la moindre policy, il faut lire ce qui existe deja. La commande seinfo -t liste environ 5 000 types sur RHEL 9 standard, et sesearch --allow -s httpd_t montre exactement ce qu'Apache peut toucher. seinfo -ahttpd_t -x resout les attributs d'un domaine, et sesearch --allow -s httpd_t -t etc_t -c file repond precisement si un acces est deja autorise. On commence chaque investigation avec ces requetes, car dans 80% des cas un type ou un boolean adapte existe deja, et tu n'as pas besoin d'ecrire une nouvelle policy du tout, seulement de corriger le label ou d'actionner l'interrupteur.
Capturer les AVC proprement
Quand quelque chose manque vraiment, tu captures les denials au lieu de deviner. Lance le service avec ausearch -m AVC -ts recent en parallele et mets le domaine concerne en mode permissive TEMPORAIRE, jamais le systeme entier: semanage permissive -a httpd_t. Ainsi seul ce service tourne sans entrave et journalise chaque violation, tandis que le reste du systeme reste enforcing. Reproduis le cas d'usage complet (demarrage, reload, tous les chemins de code), collecte les AVC et retire l'exception aussitot apres avec semanage permissive -d httpd_t. Une entree permissive oubliee est aussi dangereuse qu'un SELinux desactive globalement.
audit2allow est a double tranchant
audit2allow est une arme a double tranchant. Lancer ausearch -m AVC | audit2allow -M monmodule genere un .te qui compile et fonctionne, mais accorde frequemment des permissions absurdes comme allow httpd_t shadow_t:file read. Notre checklist interne exige que chaque .te passe par une revue manuelle avant semodule -i. Cherche les regles touchant shadow_t, etc_t, kernel_t ou self:capability sys_admin, ce sont des drapeaux rouges. Autoriser un denial parce que le service ne demarre pas sinon est commode et souvent l'origine meme du trou dont un attaquant aura besoin plus tard. Pour chaque regle demande: pourquoi le processus veut-il cela, et l'acces est-il vraiment necessaire?
Policy depuis zero avec refpolicy
Pour les nouveaux services, on prefere ecrire la policy depuis zero avec le langage macro de refpolicy. Un module typique a trois fichiers: monservice.te avec les regles, monservice.fc avec les file contexts et monservice.if avec les interfaces pour les autres domaines. make -f /usr/share/selinux/devel/Makefile genere le .pp que tu installes avec semodule -i. On versionne ces trois fichiers dans git a cote d'Ansible, et chaque PR passe par la meme revue que le code applicatif. Ainsi la policy reste tracable, reproductible et auditable, au lieu de pourrir comme du travail manuel non documente sur un seul hote.
Ports et file contexts: 80% des cas
Les services qui ouvrent des sockets sur des ports non standard sont le cas le plus frequent de casse silencieuse. Postgres sur 5433 par exemple necessite semanage port -a -t postgresql_port_t -p tcp 5433, pas une nouvelle policy. Nginx servant des fichiers hors de /var/www veut semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?" suivi de restorecon -Rv /srv/app. Quatre-vingts pourcent des cas qu'on voit sont des problemes de label et de port, pas des regles allow manquantes. Verifie donc toujours d'abord avec ls -Z et semanage port -l si un mauvais label ou un port non enregistre est la cause, avant meme de penser a un .te.
Booleans plutot que policy custom
Beaucoup d'exigences apparemment complexes sont deja couvertes par un boolean. getsebool -a | grep httpd montre des dizaines d'interrupteurs; httpd_can_network_connect autorise les connexions sortantes, httpd_can_network_connect_db seulement vers la base. Fixe-les de facon persistante avec setsebool -P httpd_can_network_connect_db on. Un boolean est toujours preferable a un module custom, car il est maintenu par les mainteneurs de la distribution, documente et conserve lors des mises a jour. La policy custom est le dernier recours, pas le premier reflexe; ceux qui cherchent d'abord les booleans ecrivent en pratique bien moins de fichiers .te maison.
Confined vs. unconfined: la fausse evidence
Une erreur tres repandue est de croire qu'un service est protege juste parce que SELinux est enforcing. Beaucoup de processus auto-lances tournent en unconfined_service_t ou init_t et sont de fait sans frein. Verifie avec ps -eZ | grep monservice dans quel domaine le service tourne reellement. Un binaire sous /usr/local/bin porte souvent bin_t au lieu d'un domaine propre, donc aucune transition n'a lieu. Tout l'effort d'une policy custom ne vaut rien si le processus ne transitionne jamais vers le domaine confined; c'est exactement a cela que servent le fichier .fc plus une type_transition depuis l'init_t qui le lance. Verifie toujours la transition au lieu de la supposer.
Tester et faire un rollback
Une policy est du code et se teste comme du code. Installe d'abord en staging avec semodule -i monservice.pp, exerce le cas d'usage complet et verifie ausearch -m AVC -ts recent pour zero nouveau denial. Liste les modules charges avec semodule -l, et retire un module defectueux aussitot avec semodule -r monservice. Garde en tete le mecanisme de priorites: semodule -X 400 -i charge a plus haute priorite et remplace la version de la distribution de facon controlee. Pour iterer vite, mets brievement le domaine cible en permissive, collecte les AVC restants en une passe et ajoute-les avec justification, au lieu de tomber dans dix rounds de deploy-et-prie.
Observabilite et policy drift
La maintenance en production exige de l'observabilite. On configure setroubleshoot-server en mode silent qui forward les AVC vers le SIEM via journald, avec des regles Sigma calibrees pour les denials inattendus. Quand un deploy casse, l'alerte arrive avant que l'utilisateur se plaigne. On lance aussi sealert -a /var/log/audit/audit.log chaque semaine en staging pour attraper la policy drift avant qu'elle atteigne la prod. Cote forensique, quand un denial evoque une vraie attaque, DFIR sous Linux: Triage Vivant avec UAC et Velociraptor prend le relais, et la policy signee s'inscrit dans la chaine de Supply Chain Security: Signature Sigstore et SBOM Reels en CI/CD.
Pieges
Les pieges les plus frequents: setenforce 0 comme etat permanent au lieu d'un diagnostic de 15 minutes; utiliser chcon au lieu de semanage fcontext plus restorecon, ce qui perd le label au prochain restorecon ou relabel; appliquer la sortie d'audit2allow a l'aveugle; et oublier les regles dontaudit qui cachent des denials pertinents (desactivees temporairement avec semodule -DB). Autre classique: un label perdu lors d'un restore tar; utilise tar --selinux ou relabelle explicitement ensuite. La base de recon cote attaquant est dans Nmap Avance: Scripts NSE pour Recon Interne dans un Lab Corporatif Simule.
Checklist
Avant qu'une policy custom parte en production: (1) cherche d'abord un type et un boolean adaptes; (2) AVC captures seulement en permissive par domaine, systeme reste enforcing; (3) chaque .te relu a la main, aucune regle shadow_t/sys_admin sans justification; (4) file contexts via semanage fcontext plus restorecon, jamais chcon; (5) ports enregistres; (6) trois fichiers versionnes dans git; (7) .pp signe et deploye via Ansible; (8) alerte SIEM sur denials inattendus active; (9) enforcing confirme via getenforce; (10) aucune entree permissive laissee derriere.
FAQ
Pourquoi ne pas remplacer SELinux par AppArmor? Sur la famille RHEL, SELinux est le chemin supporte et integre; changer jette les policies de la distribution. Enforcing coute-t-il de la performance? Le surcout est de l'ordre du pourcent a un chiffre et negligeable en pratique face au gain de securite. Et un service qui refuse obstinement de demarrer? Mets du permissive par domaine, reproduis le flux complet, collecte les AVC, ecris la policy, relis-la, repasse en enforcing, verifie. Ne laisse jamais tout le systeme en permissive.
A retenir cote pratique: ne lance jamais setenforce 0 en production plus de 15 minutes. Utilise semanage permissive -a domaine_t pour isoler le probleme, capture les AVC avec ausearch, relis la sortie d'audit2allow a la main, commit le .te dans git, signe le .pp et deploie via Ansible. Si tu ne peux pas justifier chaque allow rule en code review, la policy n'est pas prete. SELinux n'est pas un obstacle, c'est la derniere defense de perimetre entre une simple vulnerabilite et la perte totale de l'hote.


