~/blog/fr/wazuh-soc-homelab-lxc
Ajouter Wazuh à un SOC de homelab : agents, CVE et bruit

Ma chaîne de journaux répond bien à trois questions : que s’est-il passé (Loki), combien et pendant combien de temps (Prometheus), et qui attaque (CrowdSec). Elle répond mal à une quatrième : qu’est-ce qui est installé sur ces machines, et est-ce que quelque chose est vulnérable ?
C’est le rôle de Wazuh. Chaque hôte fait tourner un agent qui remonte son inventaire de paquets, les modifications de fichiers et sa configuration face à un référentiel CIS. Le serveur confronte l’inventaire aux flux de vulnérabilités et en tire des alertes. Ce billet couvre son installation dans un conteneur Proxmox à côté du reste du SOC, et la première semaine de constats, dont la plupart étaient du bruit.
Dimensionnement : il n’aurait pas tenu il y a un mois
L’installation tout-en-un de Wazuh, ce sont trois services : un indexer (un fork d’OpenSearch), le manager auquel parlent les agents, et un dashboard. L’indexer est une JVM, et les JVM aiment la mémoire.
J’ai donné au conteneur 4 vCPU, 6 Go de RAM et 40 Go de disque. Après l’installation, il s’est stabilisé autour de 2 Go de RAM et 15 Go de disque. 9,3 Go de ce disque sont le flux de détection de vulnérabilités dans /var/ossec/queue/vd, ce qui est normal pour cette version et c’est le chiffre à prévoir.
Tant que la RAM de l’hyperviseur n’était pas passée de 8 à 16 Go, ça ne tenait pas. Le lab avait déjà manqué de mémoire une fois, et un nœud OpenSearch par-dessus en aurait fait un événement hebdomadaire. Si votre hôte est juste, c’est l’indexer qu’il faut budgéter, pas les agents.
Installation
L’assistant officiel fait tout, à condition d’utiliser la bonne URL :
curl -sO https://packages.wazuh.com/4.14/wazuh-install.sh
bash wazuh-install.sh -a
Utilisez le chemin versionné. Le …/4.x/wazuh-install.sh non versionné que montrent les anciens guides renvoie désormais 403. L’assistant prévient aussi que Debian 13 n’est pas un système recommandé, puis s’installe sans problème.
Dans un conteneur LXC, l’exigence d’OpenSearch sur vm.max_map_count est un réglage du noyau de l’hôte, puisque les conteneurs partagent le noyau. Vérifiez-le sur l’hôte Proxmox (sysctl vm.max_map_count) ; le mien était déjà à 1 048 576, le conteneur n’a donc rien eu à faire.
Les mots de passe générés se retrouvent dans wazuh-install-files.tar à côté du script. Déplacez-le là où seul root peut le lire, et ne l’affichez pas dans l’historique de votre terminal.
Enrôler les agents
Par défaut, quiconque peut joindre le port 1515 peut enrôler un agent. Activez d’abord le mot de passe d’enrôlement :
<auth>
<use_password>yes</use_password>
</auth>
avec le mot de passe dans /var/ossec/etc/authd.pass sur le manager, et le même fichier sur chaque agent. Puis, sur chaque hôte :
WAZUH_MANAGER=10.99.99.15 WAZUH_AGENT_NAME=web-proxy apt-get install -y wazuh-agent
apt-mark hold wazuh-agent
systemctl enable --now wazuh-agent
Deux habitudes qui ont payé :
- Nommez chaque agent comme l’hôte est étiqueté partout ailleurs. Mes agents portent les mêmes noms que l’étiquette
remote_hostdans Loki et l’étiquettehostdans Prometheus, si bien qu’une alerte Wazuh, une ligne de journal et une métrique pour la même machine se rejoignent sur une seule chaîne. - Bloquez le paquet de l’agent. Un manager refuse les agents plus récents que lui, donc un innocent
apt upgradesur un hôte peut le déconnecter en silence. Mettez d’abord le manager à jour, puis débloquez, mettez à jour et rebloquez les agents. Je bloque aussi les paquets serveur (wazuh-indexer,wazuh-manager,wazuh-dashboard,filebeat), parce que Wazuh les met à jour dans un ordre précis.
Les machines hors du LAN (le serveur public, un portable) joignent le manager par le tailnet, si bien que le port 1514 n’est jamais exposé à Internet.
Un piège pendant le déploiement : j’ai bouclé ssh sur les hôtes depuis un script passé par un heredoc, et seul le premier hôte a reçu l’agent. Chaque ssh lit la suite du script sur l’entrée standard. Utilisez ssh -n ou </dev/null dans ce genre de boucle.
Rétention : effacer l’historique, jamais l’état
Wazuh écrit des index quotidiens : alertes, archives, monitoring, statistiques. Rien ne les supprime par défaut. Une politique Index State Management s’en charge :
{
"policy": {
"description": "Delete Wazuh event indices after 30 days",
"default_state": "hot",
"states": [
{ "name": "hot",
"actions": [],
"transitions": [{ "state_name": "delete", "conditions": { "min_index_age": "30d" } }] },
{ "name": "delete", "actions": [{ "delete": {} }], "transitions": [] }
],
"ism_template": [{
"index_patterns": ["wazuh-alerts-*", "wazuh-archives-*", "wazuh-monitoring-*", "wazuh-statistics-*"]
}]
}
}
Laissez wazuh-states-* en dehors. Ces index ne sont pas de l’historique : ce sont l’inventaire de paquets actuel et la liste actuelle des vulnérabilités. Faites-les expirer et Wazuh oublie ce qui est installé.
Créer la politique a aussi créé un index système avec un réplica, qu’un cluster à un seul nœud ne peut jamais placer, et le cluster est passé en jaune. Régler auto_expand_replicas: 0-1 sur cet index a réglé le problème. Même l’utilisateur admin reçoit un 403 sur les index système ; modifier leurs réglages demande le certificat admin (curl --cert admin.pem --key admin-key.pem …).
Ne pousser que ce qui mérite un réveil
Wazuh note chaque alerte de 0 à 15. Au bout d’une journée, ma répartition ressemblait à ceci :
| Niveau | Nombre | Ce que c’était |
|---|---|---|
| 3 | ~1 500 | connexions de routine |
| 7 | ~1 800 | constats rootcheck et CIS |
| 10 | 9 | rafales de HTTP 400 contre le reverse proxy |
| 12+ | 0 |
Le seuil de notification push est donc le niveau 12, avec le 15 marqué urgent. Le démon d’intégration de Wazuh exécute tout script nommé custom-* dans /var/ossec/integrations/, en lui passant l’alerte dans un fichier JSON. Le mien est un court script Python qui envoie la description de la règle à un topic ntfy, avec un jeton en écriture seule pour ce seul topic :
<integration>
<name>custom-ntfy</name>
<hook_url>https://ntfy.example.org/wazuh</hook_url>
<level>12</level>
<alert_format>json</alert_format>
</integration>
Tout ce qui est sous 12 reste sur le tableau de bord. Un canal push qui sonne au niveau 7 serait mis en sourdine en moins d’une semaine.
Wazuh dans Grafana
Le dashboard de Wazuh est bon, mais c’est un onglet de plus. Je voulais les alertes et la liste des CVE à côté des journaux et des métriques, donc Grafana lit directement l’indexer avec le plugin de source de données OpenSearch :
- Un utilisateur indexer en lecture seule pour Grafana, dont le rôle ne peut lire que les index
wazuh-*. Les écritures et l’index de sécurité renvoient 403. - L’API HTTP de l’indexer sur l’adresse LAN, le transport toujours sur la boucle locale :
http.host: ["127.0.0.1", "10.99.99.15"]. Lier le transport à une adresse LAN fait passer OpenSearch en mode production, avec des vérifications de démarrage qu’un conteneur peut échouer. nftables bloque ensuite le port 9200 pour tout sauf le conteneur de supervision. - Un tableau de bord, SOC (Wazuh) : alertes par niveau et par hôte, règles les plus fréquentes, CVE Critical/High par hôte, les paquets qui les portent, et un fil des alertes de niveau 7 et plus.
Trois bizarreries du plugin m’ont coûté du temps :
- Le test de santé de la source dit « Index not found » avec des motifs d’index à joker, alors que toutes les requêtes fonctionnent. Jugez-la sur une vraie requête.
- Une métrique a besoin d’une agrégation en buckets. Un nombre unique est un histogramme de dates sommé sur la période.
- L’index des vulnérabilités est un état, pas des événements. Avec le sélecteur de temps habituel, la plupart des CVE tombent hors des « dernières 24 heures », donc ces panneaux forcent la plage à dix ans.
3 700 constats, et lesquels étaient réels
Le premier scan complet a signalé 456 vulnérabilités Critical et 3 248 High sur dix hôtes. On dirait une urgence. C’était surtout du bruit, et comprendre pourquoi m’en a plus appris que le tableau de bord.
Des noyaux qui ne tournent jamais (187 Critical). Un conteneur, le point d’accès WiFi, avait trois paquets linux-image-rt-amd64 installés. Le métapaquet wireguard dépend de « wireguard-modules », et apt l’a satisfait avec un noyau temps réel complet. Un conteneur fait tourner le noyau de l’hôte, donc ces paquets ne démarrent jamais, mais Wazuh compte à juste titre leurs CVE. Je les ai purgés en gardant wireguard-tools. Le noyau avait aussi posé un faux drapeau « redémarrage requis » dans le conteneur.
Un éditeur sans correctif (1 102 constats). Debian 13 n’a pas de Vim corrigé ; les correctifs n’existent que dans testing. Je l’ai purgé de tous les conteneurs, puis réinstallé le même après-midi, parce que j’édite avec Vim et que nano n’est pas une mesure de sécurité. Ces constats restent jusqu’à ce que Debian publie un correctif, et je l’accepte en connaissance de cause.
Les vrais. Il restait le noyau cloud du serveur public et python3-jwt, tous deux déjà à la version Debian la plus récente, sans correctif disponible. Ses unattended-upgrades installeront les correctifs à leur sortie ; le noyau demandera un redémarrage, que le tableau de bord des mises à jour affiche.
Un nombre de CVE est une liste de tâches, pas une note. Chaque constat pose trois questions : le paquet est-il vraiment utilisé, existe-t-il un correctif, et sinon, est-ce que j’accepte le risque ?
Quand Wazuh scanne
Avec les valeurs par défaut, que j’ai gardées :
| Quoi | Quand |
|---|---|
| Analyse des journaux | en continu |
| Inventaire des paquets (syscollector) | toutes les heures, et au démarrage de l’agent |
| Détection de vulnérabilités | à chaque changement d’inventaire, plus un rescan complet après chaque mise à jour du flux |
| Intégrité des fichiers (FIM), rootcheck, CIS (SCA) | toutes les 12 heures, et au démarrage de l’agent |
Un paquet corrigé disparaît donc de la liste des CVE en une heure environ, pas immédiatement. Les scans de 12 heures se comptent depuis le démarrage de chaque agent, pas depuis une heure fixe, donc ils dérivent après les redémarrages.
Pièges
- L’URL d’installation
4.xrenvoie 403. Utilisez la version explicite. vm.max_map_countest un réglage de l’hôte pour un indexer dans un conteneur.- Un agent plus récent que le manager est refusé. Bloquez le paquet de l’agent et mettez le manager à jour d’abord.
sshdans une boucle heredoc mange le script.ssh -n.- N’appliquez jamais de rétention à
wazuh-states-*. - Un nœud unique ne peut pas placer de réplicas.
auto_expand_replicas: 0-1sur les index qui en demandent un. - Le test de santé du plugin Grafana se trompe avec les motifs à joker ; testez avec une requête.
- Les paquets de noyau dans les conteneurs sont du bruit CVE qui ne tourne jamais. Vérifiez
dpkg -l 'linux-image-*'avant de croire le nombre de Critical. - Un dashboard public est une page de connexion sur Internet. Le mien est derrière le reverse proxy avec une limite de débit sur la connexion (10 tentatives par minute et par IP, puis 429). Une liste blanche d’IP ou un second facteur devant reste sur ma liste.