Depuis plusieurs semaines, je travaille avec une petite équipe sur Synthaea, une plateforme de détection et de réponse sur les postes de travail et les serveurs (EDR, avec l'ambition de devenir un XDR) entièrement open source, sous licence Apache-2.0. Le principe tient dans une phrase du site du projet : une sécurité des endpoints qui se comporte comme un organisme vivant. Des capteurs au niveau du noyau (eBPF sur Linux, ETW sur Windows), des règles rattachées aux techniques ATT&CK, de l'apprentissage automatique exécuté sur la machine elle-même, et un agent qui se défend. Le projet est honnête sur son état : c'est une implémentation en cours, pas un produit fini.

Ma part, c'est le côté Linux, et surtout la validation sur de vrais systèmes : Debian, Ubuntu, Fedora. Ce billet n'est pas une présentation de l'architecture, le site du projet s'en charge. C'est le récit de quatre moments où ce que je croyais savoir n'a pas survécu à une mesure.

Site du projet — synthaea.com
Dépôt GitHub — synthaea-lab/edr

Un build vert qui ne contient rien

Premier moment, le plus bête et le plus instructif. Pour valider l'agent sur une machine virtuelle Ubuntu, je lance le script de test : compilation, installation, démarrage. La compilation affiche « terminé », tout est vert. Pourtant, au démarrage, l'agent annonce que l'eBPF est indisponible et bascule sur un mode dégradé, qui échoue à son tour sur une erreur de permission.

La vraie cause n'avait rien à voir avec les permissions. Le disque de la machine virtuelle était trop petit pour installer la chaîne d'outils, et un composant (bpf-linker, qui fabrique les programmes eBPF) n'avait jamais été installé. Le projet compile alors sans erreur, mais sans embarquer aucune sonde : un agent qui tourne, qui ne voit rien, et qui se plaint à côté du problème. Toute une série de mesures est partie à la poubelle avant que je remonte au premier message d'erreur plutôt que de m'arrêter au dernier.

Le script de test refuse maintenant de continuer sans ce composant. La règle que j'en retiens : pour un logiciel qui dépend de la machine où il tourne, « ça compile » ne dit presque rien. Il faut un test qui prouve que le capteur produit un événement, pas seulement qu'il démarre.

Debian boucle, Ubuntu non : ma supposition était fausse

Autre surprise, sur le service systemd livré avec l'agent. Il tourne sous un utilisateur sans privilèges, avec un jeu de capacités noyau réduit au strict nécessaire, ce qui est exactement ce qu'on veut d'un agent de sécurité. Sur Debian 13, ce service redémarre en boucle : l'attache des sondes échoue.

Après quelques mesures, le coupable est un réglage du noyau, perf_event_paranoid, que Debian règle par défaut à un niveau plus strict que celui qu'accepte le service avec ses capacités actuelles (c'est mon interprétation ; ce que j'ai mesuré, c'est qu'en le baissant d'un cran le service démarre et tient). J'étais alors convaincu qu'Ubuntu, dont le réglage par défaut est encore plus strict, aurait le même défaut. J'ai mesuré : non. Sur Ubuntu 24.04, avec son réglage par défaut, le même service démarre, attache tout et voit les événements. Les deux distributions ne traitent visiblement pas ce réglage de la même façon, et mon intuition sur « le plus strict casse davantage » n'avait aucune valeur.

Les options pour la suite ne sont pas gratuites : baisser le réglage protège moins toute la machine, accorder une capacité très large au service ressemble à le faire tourner en root, et réécrire l'attache sans cette fonction du noyau représenterait une cinquantaine de programmes à convertir. Ce n'est donc pas à moi de trancher seul ; le problème est décrit, chiffres à l'appui, dans une issue ouverte, pour que l'équipe choisisse.

Se tromper en public, puis corriger

Sur Fedora, qui active SELinux en mode strict par défaut, je voulais prouver que l'agent n'était bloqué par aucune règle de la politique de sécurité. Mon script comptait les refus avec l'outil habituel, et affichait zéro. J'ai écrit « 0 refus » dans plusieurs revues de code.

En relisant la documentation du labo pour un tout autre sujet, je tombe sur cette phrase : sur cette machine, l'outil de recherche d'audit est aveugle, parce que le démon d'audit n'y tourne pas. Il lit un fichier qui n'existe pas et répond « aucune correspondance » quoi qu'il arrive. Mon zéro ne prouvait rien. J'ai refait la mesure à la bonne source, le journal système : seize refus depuis le démarrage de la machine, dont le plus récent date du 30 septembre (ils venaient d'anciens défauts du paquet, depuis corrigés), et aucun le jour de mes tests. Le chiffre était donc bon, mais je n'avais pas le droit de l'affirmer à l'époque.

J'ai posté la correction sur les revues concernées, avec la méthode, et le script compte désormais depuis le journal. Rien d'héroïque : c'est le genre d'erreur qu'on évite en se demandant, avant d'écrire un résultat négatif, si l'outil de mesure aurait pu dire autre chose.

Lire les logs d'un serveur web, sans noyer l'analyste

Le chantier sur lequel je passe le plus de temps : faire lire à l'agent les journaux d'accès d'un serveur web (nginx, Apache) et les erreurs d'une base MySQL ou MariaDB, pour détecter des attaques que le capteur noyau ne voit pas, comme un fichier de contrôle déposé sur un serveur web puis exécuté. Le problème d'ingénierie est une question de volume : un site actif écrit des milliers de lignes par minute, et envoyer chaque requête au serveur de détection serait à la fois coûteux et inutile.

Le choix retenu : ne produire d'événement que pour une requête qui ressemble à une attaque (injection SQL, remontée de répertoires, requête type « webshell », signature d'outil de scan), et résumer tout le reste en un seul événement toutes les soixante secondes (nombre de requêtes, erreurs 4xx et 5xx, clients distincts, clients qui échouent le plus). Un plafond limite en plus le nombre d'événements par type de signature et par fenêtre. Il est volontairement par signature : un scanner qui envoie mille tentatives d'injection ne doit pas pouvoir épuiser le quota de la seule requête de webshell qui compte vraiment.

Ligne du journal d'accès (nginx / Apache)
↓
Analyse de la ligne · comptée dans le résumé de 60 s
↓
Ressemble à une attaque ?
Plafond par signature
↓
Événement « requête suspecte »
↓
Corrélation avec le shell lancé par le serveur web → alerte T1505.003

Du journal à l'alerte : seule la requête qui précède un shell lancé par le serveur web remonte comme preuve.

Le vrai test, c'est un vrai nginx. J'y envoie une requête de chaque famille, puis je copie un interpréteur de commandes sous le nom du serveur web pour qu'il lance un shell. L'agent a bien produit les quatre événements attendus, le résumé d'une minute, et l'alerte de web shell. Mais elle nommait la mauvaise requête : l'injection SQL envoyée en premier, pas la requête de webshell qui précédait le shell.

Explication : l'agent horodate une requête au moment où il lit la ligne, pas au moment où elle a été reçue. Quand plusieurs lignes sont lues d'un seul coup, elles partagent le même horodatage, et la règle de corrélation prenait « la plus proche » dans le temps, c'est-à-dire la première d'une série d'égalités. Le correctif tient en une phrase (à égalité, on retient la dernière lue, celle qui est dans l'ordre du journal) et le test unitaire qui l'accompagne reproduit exactement ce cas. Aucun test sur des données simulées ne l'aurait trouvé : c'est la rafale de requêtes d'un vrai serveur qui l'a fait apparaître.

Ce que je retiens

Dans ces quatre histoires, le point commun n'est pas la difficulté technique. C'est l'écart entre « ça devrait marcher » et « j'ai vu que ça marche, et je sais ce que mon outil de mesure aurait dit si ça n'avait pas marché ». Pour un logiciel de sécurité, cet écart est celui entre une promesse et une protection. Les outils de test du projet vont dans ce sens : un script par scénario, un résultat PASS ou FAIL par vérification, un journal conservé pour qu'un autre puisse le relire.

Synthaea est encore tôt dans sa vie : les capteurs et le moteur de règles existent, la couche qui rassemble les cas sur plusieurs machines et le moteur qui rend chaque installation unique restent à construire. Si le sujet vous parle, le code et les discussions sont ouverts, et les retours de terrain, y compris les mauvaises surprises, sont exactement ce dont un projet comme celui-ci a besoin. Comme pour le reste de ce blog, ces techniques sont à utiliser sur ses propres systèmes ou dans un cadre autorisé.