NOPE LinkedIn

Catégories:
Blog
IA
Devsecops

asp-forge (1/3) — Pourquoi un SOC agentique self-hosted ? Architecture et choix techniques

asp-forge (1/3) — Pourquoi un SOC agentique self-hosted ? Architecture et choix techniques image

Rubrique: Blog Rubrique: IA Rubrique: Devsecops Tag: soc agentique Tag: llm local Tag: qwen Tag: lora Tag: langgraph Tag: pydantic-ai Tag: self-hosted Tag: wazuh Tag: souveraineté

Cet article ouvre une série de trois consacrée à asp-forge, un lab de Security Operations Center agentique entièrement auto-hébergé, conçu comme banc d’essai pour évaluer l’apport concret des modèles de langage dans le triage d’alertes. Cible : DevSecOps et analystes SOC qui s’interrogent sur l’industrialisation des LLM dans une chaîne de réponse aux incidents, sans dépendre d’un fournisseur cloud.

Le problème : la fatigue d’alertes ne se résout pas par plus d’analystes

Tout SOC un peu sérieux remonte plusieurs centaines à plusieurs milliers d’événements par jour depuis ses outils — Wazuh sur les serveurs, T-Pot sur les honeypots exposés, les EDR sur les postes, les WAF en bordure. Le ratio de bruit reste structurellement élevé. Les règles de corrélation filtrent une partie des faux positifs évidents, mais ce qui reste tombe sur le bureau d’un analyste L1 qui doit, dans l’urgence, décider :

  • est-ce une attaque réelle ?
  • est-ce qu’on bloque maintenant ?
  • est-ce qu’on déclenche un ticket ?
  • est-ce qu’on remonte au L2 ?

Le coût humain est connu : burn-out, alert fatigue, et surtout décisions inégales selon la fatigue de l’analyste à un instant T. La promesse des SOC managés (MDR) est précisément d’industrialiser cette ligne, mais avec un compromis désormais discuté : les logs partent vers un cloud tiers, souvent américain, pour une durée non négligeable.

Le projet asp-forge part d’une question simple : peut-on conserver les logs en interne tout en automatisant le triage L1 avec des modèles de langage spécialisés ? Pas un agent autonome qui prend toutes les décisions, mais un système typé qui fait gagner 80 % du temps de triage sur les classes d’alertes les plus répétitives, en gardant un humain dans la boucle pour les cas critiques.

L’idée centrale : déléguer le triage, pas la décision

Avant de parler de stack, il est important de cadrer ce qu’asp-forge n’est pas :

  • pas un agent autonome qui modifie le firewall sans contrôle ;
  • pas un substitut à un analyste L2 ou L3 ;
  • pas une plateforme de threat hunting proactif.

C’est une machine à transformer des alertes brutes en décisions typées, où chaque décision est :

  1. tracée (qui l’a prise — quel agent, quelle version),
  2. justifiée (raisonnement en langage naturel),
  3. réversible (chaque action a sa contre-action — un blocage peut être annulé après revue),
  4. validable (un humain peut intercepter avant l’action ou après).

Le LLM n’est pas le décideur final ; c’est l’analyste L1 qui propose une décision validée par les gardes-fous typés en aval.

Contraintes auto-imposées

Trois contraintes ont structuré l’ensemble des choix techniques.

Souveraineté des données. Les logs SOC contiennent des informations identifiantes (IPs internes, noms d’utilisateurs, configurations réseau). Toute architecture qui sort ces données du périmètre client est disqualifiée. Conséquence : LLM exécuté localement, pas d’API OpenAI / Anthropic / Mistral hébergée.

CPU-only — un choix philosophique avant d’être économique. Le contexte cible n’est pas une organisation avec parc GPU. La stack doit fonctionner sur des serveurs x86 ordinaires, sans accélérateur dédié. Conséquence directe : modèles ≤ 3 milliards de paramètres, quantifiés Q4 ou Q5, sur llama.cpp. Mais l’enjeu va au-delà du coût matériel : un agent qui ne réclame pas de GPU peut être banalisé. N’importe quelle VM x86 devient un emplacement valide ; on n’attend pas un slot GPU rare, on n’a pas de driver CUDA à maintenir, on n’est pas couplé à un fournisseur cloud particulier. Cette banalisation est ce qui rend possible le mode opératoire décrit plus loin — la création et la destruction d’agents à la volée selon la charge.

Latence acceptable, pas du temps réel. Le triage L1 n’a pas besoin d’être sub-seconde. Une cible de 15 à 30 secondes par alerte est parfaitement compatible avec l’usage opérationnel. Cette tolérance est fondamentale : elle permet d’utiliser des modèles 3 B au lieu de 1 B avec une marge confortable, et donc d’avoir un raisonnement utile.

Stack haut niveau

Vue d’ensemble du flux de données — chaque flèche est une frontière typée Pydantic à laquelle on validera strictement les contrats.

Internet ──► T-Pot CE (honeypots) ──┐
                                    │  Elasticsearch poll
Réseau lab ──► Wazuh manager ──webhook──► ASP Forwarder
                                   Redis streams (bus d'événements)
                              Heuristique pré-LLM (filtrage rapide)
                              Pipeline d'agents (cf. article 2)
                              OPNsense API ──► Règle firewall appliquée

Les composants en place :

Composant Rôle
T-Pot CE Honeypots multi-services (Cowrie, Dionaea, SentryPeer…) exposés Internet
Wazuh SIEM/EDR sur les VMs du lab
ASP Forwarder Passerelle webhook → Redis streams
Redis Bus d’événements temps réel
llama-server LLM local (Qwen 2.5 3B + adaptateurs LoRA dynamiques)
Pipeline agents Orchestration LangGraph + validation Pydantic AI
OPNsense Firewall WAN, exécutant les règles validées
CrowdSec Réputation collaborative en aval
OpenCTI Capitalisation des observables (IP, IOC)

Le choix du modèle : Qwen 2.5 3B Q4_K_M + LoRA dynamiques

Le marché des SLM (Small Language Models) est très actif depuis 2024. Le critère sélectif n’a pas été la performance académique sur des benchmarks publics, mais trois éléments très opérationnels :

  1. Performance CPU réelle sur un x86 AVX2 ordinaire. Sur Qwen 2.5 3B Q4_K_M via llama.cpp, on observe 15 à 35 tokens/s selon le batch et la longueur de contexte. Largement suffisant pour notre cible 30 s.
  2. Support des LoRA dynamiques par requête. C’est le point clé architectural — un même processus llama-server peut servir plusieurs spécialités d’agents (firewall OPNsense, configuration WireGuard, réputation CrowdSec) en chargeant un adaptateur de quelques dizaines de mégaoctets par requête, sans relancer le service.
  3. API OpenAI-compatible native. Cela évite le vendor lock inverse (le protocole de communication n’enferme pas dans une stack particulière).

Une alternative testée — onnxruntime-genai avec MultiLoRA — a été abandonnée. Bonne intention, mais performance CPU décevante (~5 t/s sur Qwen 0.5B int4) et stack d’export Olive trop fragile pour notre niveau de criticité.

L’organisation des poids sur disque suit donc ce schéma :

gguf/
├── qwen2.5-3b-q4km.gguf          (~1.7 GB, base partagée, read-only)
├── lora-opnsense.gguf             (~30 MB)
├── lora-wireguard.gguf            (~30 MB)
└── lora-crowdsec.gguf             (~30 MB)

Le surcoût mémoire d’un nouveau métier d’agent est de l’ordre de 30 mégaoctets. Cela permet, sur le même serveur, de spécialiser finement les agents sans dupliquer le modèle de base, et d’itérer sur chaque LoRA indépendamment.

Élasticité horizontale : créer et détruire des agents à la demande

C’est ici que la philosophie CPU prend tout son sens. La charge d’un SOC n’est pas linéaire : la majorité du temps, un seul agent par métier suffit. Mais il existe des épisodes — campagne de scans massifs, exploitation d’une CVE fraîchement publiée, déploiement applicatif bruyant — où la file Redis enfle.

Sur un parc GPU, la réponse à un pic est inconfortable : soit on sur-provisionne en permanence (cartes oisives 95 % du temps), soit on accepte la latence d’allocation d’un slot GPU rare. Sur du CPU banalisé, on instancie un nouveau worker sur n’importe quelle VM disponible, on charge le LoRA correspondant à son métier (Triage, SOC, ou CERT), et il rejoint son équipe en s’attachant au consumer group Redis approprié. Quelques secondes de boot, pas de réservation matérielle, pas de file d’attente vers une ressource rare.

Le cycle de vie d’un agent en surcharge se résume alors à quatre étapes :

  1. la profondeur de file Redis dépasse un seuil pendant N secondes ;
  2. l’orchestrateur lance un llama-server sur une VM disponible, avec le LoRA métier monté à chaud ;
  3. le worker rejoint son consumer group et tire des messages comme ses pairs — sa décision est tracée à l’identique ;
  4. quand la file redescend pendant T secondes, les workers excédentaires sont arrêtés ; seuls les workers de base persistent.

Aucun état n’est perdu lors d’un arrêt : les décisions et leurs justifications sont persistées dans Redis et dans la base de traçabilité, pas dans la mémoire des workers. Un agent qui meurt n’emporte rien avec lui — c’est aussi ce qui permet de redéployer une nouvelle version d’un LoRA sans interruption de service.

Le choix d’orchestration : LangGraph + Pydantic AI

Le second choix structurant concerne comment les agents s’enchaînent. Deux paradigmes sont en compétition dans l’écosystème agentique en 2026 :

  • Conversationnel émergent type AutoGen — plusieurs agents se parlent, le flux de contrôle émerge de la discussion ;
  • Graphes d’états type LangGraph — flux explicite, transitions conditionnelles, persistance globale.

Pour un usage SOC, le conversationnel émergent est interdit en production. La raison est simple : un attaquant qui découvre les prompts peut influencer le flux de contrôle par injection. Avec un graphe d’états, le routage suit des arêtes typées sur des chaînes strictes (continue, escalate, dismiss) qu’aucune sortie LLM ne peut détourner.

Sur ce graphe, chaque transition entre nœuds franchit une frontière typée Pydantic. La Boundary Rule qui sous-tend la conception est simple : à l’intérieur d’un nœud, les structures de données peuvent être souples (un dictionnaire de drafts) ; aux frontières, on valide sans concession. Une ValidationError ne fait pas planter le graphe — elle déclenche une boucle locale de correction (le LLM réécrit son JSON jusqu’à conformité, dans la limite d’un compteur de tentatives).

Cette discipline est ce qui distingue un démonstrateur fragile d’un système sur lequel on peut bâtir une chaîne d’audit.

Anti-patterns évités

Trois pièges classiques ont été écartés dès le départ :

  1. Donner trop d’outils à un seul agent. Un agent généraliste qui peut interroger le SIEM, modifier le firewall et alerter Telegram en même temps est ingérable. On préfère une mosaïque d’agents Leaf avec un blast radius limité, chacun ayant son contrat d’entrée/sortie typé.
  2. Faire confiance à la sortie LLM telle quelle. Toute action sur un système réel passe par une whitelist d’opérations + une validation de schéma. Le LLM peut proposer ; il ne peut pas exécuter.
  3. Persister l’historique conversationnel intégralement. Cela sature le contexte et augmente le coût. On utilise des state reducers qui agrègent sélectivement — par exemple, conserver les trois derniers échanges et un résumé des plus anciens.

Ce qui suit

Cette architecture macro pose la base. Mais le cœur d’asp-forge — et le plus instructif d’un point de vue ingénierie — est la cascade Triage → SOC → CERT qui pilote effectivement les décisions sur chaque alerte. C’est l’objet du second article de la série.

Le troisième et dernier article reviendra sur les leçons apprises : ce qui a fonctionné, ce qui s’est révélé surprenant, et ce qu’on garde pour la suite — notamment dans le projet successeur de la même équipe.