Un SOC commence par une carte
Mon run-book se présente lui-même comme une « CMDB simplifiée ». C'est un fichier que je maintiens depuis des mois, qui liste les hôtes, les services, les ports et les dépendances. Le jour où j'ai voulu écrire sur mon SOC, c'est le premier document que j'ai rouvert.
Parce qu'un SOC qui ne sait pas ce qu'il protège ne détecte rien. Il produit du bruit, et le bruit finit toujours par être ignoré.
J'ai donc repris mon schéma de défense en profondeur, celui que j'ai en tête depuis que j'ai monté ce lab, et je l'ai confronté au code. Rôle par rôle, chart par chart, Jenkinsfile par Jenkinsfile. Chaque contrôle devait produire sa preuve, sinon il perdait son statut.
Le résultat est moins flatteur que le schéma, et beaucoup plus utile.
Le parcours de ce billet suit cet exercice. D'abord la carte générale, sept couches du câble au tableau de bord. Puis le chemin d'un attaquant à travers ces couches, avec les deux endroits où il passerait sans trop d'effort. Vient ensuite un zoom sur la couche applicative et le pipeline, là où j'ai le plus investi, avant la télémétrie et la réponse, c'est-à-dire le SOC au sens strict. Et pour finir, le bilan chiffré et la liste de ce qui manque.
Si les notions de SOC, de SIEM et de SOAR ne sont pas familières, la théorie est traitée dans SOC, SIEM et SOAR, et le principe de défense en profondeur dans Fondamentaux : DICP, risque et défense en profondeur. Ici, on regarde ce que ça donne sur du vrai matériel, avec de vraies lacunes.
Un schéma d'architecture qui ne distingue pas ce qui tourne de ce qui est prévu n'est pas une carte. C'est une plaquette commerciale.
Méthode et légende
Chaque badge de cet article a été gagné, pas attribué. La méthode tient en trois passes.
Première passe : le code. J'ai cherché les directives dans les rôles Ansible, les valeurs dans les charts Helm, les étapes dans les Jenkinsfiles, les contraintes dans les changelogs Liquibase. Si un contrôle a une occurrence dans un dépôt, il est versionné, donc reproductible.
## Un contrôle sans occurrence dans un dépôt retombe en badge « manuel »
rg -l "SecRuleEngine On" roles/
rg -l "PeerAuthentication" manifests/
rg -l "@PreAuthorize" --glob "*.java" | wc -l
Deuxième passe : le run-book de juillet 2026, pour tout ce qui n'existe que dans une interface web. Les règles de l'UDM Pro, les seuils de Harbor, les entrées Graylog. Ces contrôles existent réellement, mais ils ne survivraient pas à une réinstallation sans que quelqu'un se souvienne de les recliquer.
Troisième passe : l'honnêteté. Quand un contrôle est déployé mais que je ne sais pas s'il bloque ou s'il se contente d'observer, il ne mérite pas un badge vert.
| Badge | Signification | Preuve exigée |
|---|---|---|
| ✅ | En place et versionné | Rôle Ansible, chart Helm, Jenkinsfile ou migration Liquibase |
| 🔶 | En place mais partiel, non bloquant, ou mode effectif à confirmer | Trace dans le code, sans garantie d'effet |
| ⚪ | Configuré à la main dans une interface, non versionné | Entrée dans le run-book |
| ❌ | Cible du schéma, pas encore en place | Aucune |
Cette légende sert dans tout l'article, y compris dans les schémas : le cuivre plein pour le versionné, le crème bordé pour le partiel, le crème clair pour le manuel, le rouge pour le manquant.
Le badge le plus intéressant n'est pas le rouge, c'est le blanc. Un contrôle cliqué à la main n'existe qu'aussi longtemps que la mémoire de celui qui l'a cliqué.
La carte générale : sept couches
Sept couches, du câble jusqu'au tableau de bord. Chacune a sa logique propre, ses outils et son niveau de maturité. Le schéma ci-dessous montre les contrôles représentatifs de chaque couche, avec leur statut réel.
Voici maintenant le détail, couche par couche. La couche 7 a sa propre section plus bas.
L1 Physique
C'est la couche que je maîtrise le mieux et que je versionne le moins. Tout se pilote depuis des interfaces constructeur : iDRAC pour le contrôleur RAID, l'interface ESXi pour les VM, celle de QNAP pour le NAS.
Le NAS mérite une mention particulière : son export NFS /lab ne sert pas qu'à mes archives, il porte aussi les PVC du cluster Kubernetes. Un volume monté par des dizaines de pods, sans chiffrement au repos.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| RAID 1 matériel | Dell PowerEdge T350, contrôleur PERC H345 | ⚪ | iDRAC9 |
| Virtualisation | ESXi 7.0U3, une dizaine de VM | ⚪ | Interface ESXi |
| Stockage partagé et PVC | NAS QNAP TS-453D, export NFS /lab |
⚪ | Interface QNAP |
| Continuité électrique | Onduleurs APC et Eaton, arrêt contrôlé | ⚪ | Interface des onduleurs |
| Chiffrement du stockage, TPM | aucun | ❌ | nulle part |
Le RAID 1 vient d'une migration en urgence racontée dans Migration urgence vers RAID 1 SSD. La disponibilité de cette couche a coûté un week-end. Sa confidentialité, elle, n'a encore rien coûté du tout.
L2 Périmètre
Deux gardiens à l'entrée. L'UDM Pro fait le travail réseau : routage, pare-feu, IDS/IPS, DHCP multi-VLAN, portail WireGuard. Le reverse proxy nginx sur vmmgnt fait le travail applicatif, avec ModSecurity et la terminaison TLS.
Le filtrage géographique se fait au niveau de l'appliance, pas au niveau de nginx. Ce n'est pas un oubli : le module geoip est explicitement retiré des options de compilation du rôle. Un choix que j'assume, mais qui déporte un contrôle du versionné vers le cliqué.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| Routage, pare-feu, IDS/IPS, DHCP, WireGuard | UDM Pro | ⚪ | Interface UniFi |
| Politique inter-VLAN « tout interdit par défaut » | UDM Pro | ⚪ | Interface UniFi |
| Reverse proxy, WAF et terminaison TLS | nginx, ModSecurity, Let's Encrypt | ✅ | Rôle Ansible |
| CA interne distribuée | install-ca-cert-ansible, avuru-ca-cluster-issuer |
✅ | Rôle Ansible et chart |
| Filtrage géographique | UDM Pro, geoip retiré du build nginx | ⚪ | Interface UniFi |
| Isolation de la DMZ | VLAN dédié | ⚪ | Interface UniFi |
La CA interne est le contrôle dont je suis le plus content sur cette couche. Un rôle Ansible la distribue sur les hôtes, un ClusterIssuer la présente au cluster. Résultat : les certificats internes sont valides partout, et personne dans la maison n'a pris l'habitude de cliquer sur « continuer malgré l'avertissement ».
L3 Réseau
Une dizaine de VLAN : management, IoT, VPN, main, guest, CCTV, pro, lab et DMZ. Le plan que j'avais publié en 2025 sur la topologie réseau du lab a évolué depuis, et je ne redonne pas les sous-réseaux ici.
Au-dessus, le cluster ajoute sa propre couche réseau. Istio en mode Ambient, avec ztunnel déployé en DaemonSet et du mTLS automatique via HBONE, sans sidecar. Le PeerAuthentication est en mode STRICT sur les deux namespaces applicatifs, et des AuthorizationPolicy isolent les namespaces entre eux, avec une règle explicite deny-staging-to-prod.
Le détail qui me gêne : ces manifestes sont appliqués à la main. Ils existent, ils sont dans un dépôt, mais aucun rôle ni aucun chart ne les pose. Une réinstallation du cluster les oublierait.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| Segmentation en une dizaine de VLAN | UDM Pro | ⚪ | Interface UniFi |
| Pare-feu interne et IDS | UDM Pro | ⚪ | Interface UniFi |
Résolution interne devops.lab et k8s.lab |
PowerDNS | ✅ | Rôle Ansible, mis à jour par la CI |
| Journalisation des requêtes DNS | PowerDNS | ❌ | nulle part |
| mTLS de service à service | Istio Ambient 1.30.3, ztunnel, HBONE, PeerAuthentication STRICT |
✅ | Manifestes appliqués à la main |
Isolation namespaces, deny-staging-to-prod |
AuthorizationPolicy Istio |
✅ | Manifestes appliqués à la main |
| NetworkPolicies Kubernetes | Chart Kafka uniquement | 🔶 | Chart Helm Kafka |
Sur les NetworkPolicies, je suis à moitié honnête quand je dis « partiel ». Seul le chart Kafka en embarque. Aucun namespace applicatif n'en a. Ce sont les AuthorizationPolicy d'Istio qui font le travail d'isolation, à la couche 7 plutôt qu'à la couche 3. Ça marche, mais ça repose entièrement sur le mesh : si ztunnel tombe ou si un pod sort du mesh, il n'y a plus rien en dessous.
L'absence de journalisation DNS est un angle mort classique. Un résolveur interne qui voit tout et n'écrit rien, c'est une caméra débranchée.
L4 Hôtes
La couche la plus rouge de la carte, et j'en revendique une partie.
Le durcissement CIS, l'authentification SSH par clé seule, fail2ban et auditd : rien de tout ça n'est en place. L'intégrité des fichiers est couverte autrement, par les agents Wazuh, ce qui compense partiellement l'absence d'auditd sans la remplacer.
Les mises à jour automatiques sont absentes par choix. Après un incident où une mise à jour a cassé la chaîne ModSecurity en pleine nuit, j'ai posé un apt-mark hold sur les paquets critiques et je fais les montées de version manuellement, dans une fenêtre choisie. Un badge rouge assumé vaut mieux qu'un badge vert qui casse la production le mardi matin.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| Durcissement système (CIS, clé SSH, fail2ban, auditd) | aucun | ❌ | nulle part |
| Mises à jour non surveillées | apt-mark hold, fenêtre manuelle |
❌ | Choix documenté au run-book |
| Conteneurs non privilégiés | securityContext commenté, limites CPU et mémoire posées |
🔶 | Charts Helm |
| Scan des images | Harbor v2.14.0 et Trivy | ✅ | Rôle Ansible |
| Seuil de blocage CVE critique | Harbor | ⚪ | Interface Harbor |
| Gestion des secrets | Secrets Kubernetes, credentials Jenkins, Passbolt, yopass, Ansible Vault | ✅ | Charts, Jenkins, un rôle Ansible |
| Coffre centralisé (Vault, sealed-secrets) | aucun | ❌ | nulle part |
Le securityContext mérite une explication. Il est présent dans tous mes charts, correctement écrit, avec les bons champs. Et commenté. Ligne par ligne. Le vestige d'un débogage qui a fini par devenir la norme, pendant que les limites CPU et mémoire, elles, sont restées actives.
Côté secrets, la situation est correcte sans être bonne : Passbolt et yopass tournent, Ansible Vault chiffre un rôle, Jenkins gère ses credentials. Mais des mots de passe par défaut traînent encore dans des defaults/main.yml, et il n'y a pas de coffre centralisé qui ferait autorité sur l'ensemble.
L5 Applications
C'est la couche où j'ai le plus investi, et ça se voit. Trois gardiens successifs : le WAF en bordure, la gateway d'API, et l'autorisation dans le code.
ModSecurity v3.0.8 tourne devant nginx 1.24 avec l'OWASP CRS v4.0.0 et SecRuleEngine On posé par le rôle. Son journal d'audit JSON part vers Graylog. Ce que je n'ai pas revalidé depuis l'incident de mise à jour, c'est son mode effectif : est-ce qu'il bloque vraiment, ou est-ce qu'il se contente de journaliser ? Tant que je n'ai pas rejoué un test, le badge reste orange.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| WAF applicatif | ModSecurity v3.0.8, nginx 1.24, OWASP CRS v4.0.0 | ✅ | Rôle Ansible |
| Mode effectif du WAF (blocage ou détection) | ModSecurity | 🔶 | À revalider |
| Journal d'audit du WAF | JSON expédié vers Graylog | ✅ | Rôle Ansible |
| Authentification centralisée | Keycloak 24.0.1, OIDC, realm dédié | ✅ | Rôle Ansible |
| MFA utilisateur | Keycloak | ❌ | nulle part |
| Quotas par utilisateur | APISIX 3.15, limit-count adossé à Redis |
✅ | Manifestes APISIX |
| Restriction des routes d'orchestration | APISIX ip-restriction |
✅ | Manifestes APISIX |
| Admin API de la gateway | ClusterIP restreinte au CIDR des pods | ✅ | Manifestes APISIX |
| Vérification JWT à la gateway | APISIX | 🔶 | Désactivée, validation déportée aux backends |
| Autorisation applicative | Spring Security, @PreAuthorize dans 84 fichiers |
✅ | Code des services |
| Filtre d'appartenance | Spring, mode audit par défaut | 🔶 | Code des services |
Le rate limiting est la partie dont je suis le plus satisfait. La clé de comptage est calculée depuis le JWT, avec repli sur l'adresse source quand le jeton est absent, ce qui évite qu'un utilisateur légitime derrière un NAT partagé se fasse pénaliser par son voisin. Neuf budgets distincts, de 20 à 300 requêtes par minute selon la classe de route : les routes d'authentification sont beaucoup plus serrées que les routes de lecture.
En revanche, la vérification du JWT au niveau de la gateway est désactivée depuis juillet 2026. Mes jetons viennent d'un UAA maison qui n'expose pas de JWKS, et APISIX ne pouvait donc pas valider les signatures. Ce sont les backends qui valident, chacun de leur côté. La compensation fonctionne, mais elle déplace le contrôle au lieu de le remplacer, et j'y reviens dans la section suivante.
Dernier détail sur cette couche : le filtre d'appartenance, qui vérifie qu'un utilisateur ne manipule que ses propres ressources, est configuré en mode audit par défaut. Il journalise les violations, il ne les bloque pas. Le principe est traité plus largement dans Sécurité des API et OWASP Top 10.
L6 Données
Une base PostgreSQL 17 partagée sur vm.db.lab, une quinzaine de bases pour tout le lab. C'est mon principal point de défaillance unique, et je le sais depuis le jour où je l'ai créée. Son schéma est intégralement versionné par Liquibase, ce qui est la bonne nouvelle. Son pg_hba est beaucoup trop permissif, ce qui est la mauvaise.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| Schéma et migrations versionnés | PostgreSQL 17 sur vm.db.lab, Liquibase |
✅ | Changelogs Liquibase |
| Contrôle d'accès réseau à la base | pg_hba |
🔶 | Rôle Ansible, trop permissif |
| Chiffrement au repos (TDE) | aucun | ❌ | Présent au document d'architecture |
| TLS en transit | Let's Encrypt en bordure, mTLS dans le mesh | 🔶 | Pas de sslmode côté PostgreSQL |
| Sécurité au niveau des lignes | aucun | ❌ | Isolation faite en Java |
| Masquage des données personnelles | Annotation dédiée côté audit, test de non-régression | ✅ | Code du service d'audit |
| Cycle de vie RGPD | Workflow Temporal : suppression douce, 30 jours de grâce, purge et anonymisation | ✅ | Code du workflow |
| Sauvegardes | Dump complet à 02:30, rétention 7 jours et 4 semaines vers le NAS | 🔶 | Tâche planifiée |
| Journal d'audit immuable | Kafka avuru.audit.*, outbox transactionnel, checksum SHA-256 chaîné |
✅ | Migrations Liquibase et code |
Le chiffrement au repos figure dans mon document d'architecture depuis le premier jour. Il n'a jamais atteint le moteur. Même chose pour la sécurité au niveau des lignes : l'isolation entre utilisateurs existe, mais elle est réalisée en Java, par des gardes d'appartenance. Si quelqu'un ouvre un psql avec le compte applicatif, il voit tout.
Le TLS en transit est un cas d'école de faux sentiment de sécurité. Let's Encrypt protège la bordure, le mesh chiffre le trafic entre pods, et pourtant les connexions à PostgreSQL n'ont pas de sslmode. Trois quarts du chemin sont chiffrés.
Sur les sauvegardes, la restauration a été testée en juillet 2026 et elle fonctionne. Mais il n'y a ni copie hors site, ni restauration à un instant donné. La règle 3-2-1 n'est donc pas tenue, et il faut le dire ainsi plutôt que d'écrire « sauvegardes en place ».
Le contrôle dont je suis le plus fier est le journal d'audit. Les événements partent en Kafka sur les topics avuru.audit.* via un outbox transactionnel, sont consommés par un service dédié, et chaque enregistrement porte un checksum SHA-256 qui intègre celui de l'enregistrement précédent. Toute modification a posteriori casse la chaîne. Un trigger BEFORE UPDATE OR DELETE refuse les modifications, un REVOKE UPDATE, DELETE retire le droit au compte applicatif, et un partitionnement annuel est prévu pour tenir plus de sept ans de rétention.
Et pour les données personnelles, la règle est simple : elles n'entrent pas dans les événements d'audit. Une annotation dédiée assure le masquage, et un test de non-régression vérifie la règle à chaque build.
Une couche « données » se juge sur ce qui se passe quand toutes les autres ont échoué. Chez moi, l'audit tiendrait. Le reste serait lisible.
Le chemin d'un attaquant
La carte statique dit ce qui existe. Elle ne dit pas ce qui résiste. Le schéma suivant suit un attaquant depuis Internet, couche par couche, avec à chaque étape la question qui compte.
Sur le papier, la chaîne tient. En pratique, deux maillons sont plus faibles que le schéma ne le laisse croire.
Le premier, c'est le mode effectif de ModSecurity. La directive SecRuleEngine On est bien dans le rôle, le CRS v4.0.0 est chargé, les journaux partent vers Graylog. Mais je n'ai pas rejoué de test de blocage depuis l'incident de mise à jour, et entre un WAF qui bloque et un WAF qui se contente de regarder passer, il y a toute la différence entre une défense et un témoin. Tant que je n'ai pas envoyé une requête volontairement malveillante pour voir si elle repart en 403, ce maillon reste une hypothèse.
Le second, c'est la vérification du JWT à la gateway. Chaque backend valide le jeton qu'il reçoit, et les routes d'orchestration internes sont restreintes par ip-restriction. Ces deux mesures compensent. Mais un contrôle centralisé et un contrôle distribué n'ont pas le même profil de risque : il suffit qu'un service oublie sa configuration de validation pour que le trou existe, et rien à la gateway ne le rattrapera. Une compensation n'est pas une équivalence.
Le reste de la chaîne est plus solide. Le mTLS STRICT du mesh signifie qu'un pod compromis dans staging ne peut pas parler à la production, y compris s'il connaît le nom du service : la règle deny-staging-to-prod est explicite. Et une image portant une CVE critique n'arrive pas jusqu'au cluster, parce que Harbor la refuse avant.
La question utile n'est pas « combien de couches j'ai empilées » mais « laquelle produit une alerte quand la précédente tombe ».
Zoom : la couche applicative et le pipeline
Une bonne partie de mes contrôles applicatifs ne s'exécutent pas en production. Ils s'exécutent avant, dans le pipeline. C'est là que se joue la différence entre un lab qui se protège et un lab qui compile.
En reprenant la grille de familles d'outils du panorama SAST, DAST, SCA et fuzzing, voilà ce que le lab couvre réellement.
| Famille | Outil au lab | Statut | Bloquant ? |
|---|---|---|---|
| SAST code | Semgrep, règles OWASP Top 10, dans GitLab CI | 🔶 | Non |
| SAST code | SonarQube, Jenkinsfile dédié dans environ 18 dépôts | 🔶 | Oui dans son pipeline, absent du flux standard |
| SAST architecture | ArchUnit, 30 règles | ✅ | Oui |
| SAST IaC | Checkov | 🔶 | Non |
| SAST secrets | Gitleaks | 🔶 | Non, et aucun hook pre-commit |
| DAST | ZAP baseline, conditionné à une variable | 🔶 | Non |
| DAST et API | Karate, 41 fichiers de scénarios | ✅ | Oui |
| SCA | Dependency-Check | 🔶 | Non |
| SCA | Syft et Grype, runner nocturne | 🔶 | Non |
| SCA et conteneurs | Harbor et Trivy | ✅ | Oui, au seuil critique |
| Fuzzing | aucun harness | ❌ | Non |
ArchUnit est la surprise de cet exercice. Je l'avais mis en place pour des raisons d'architecture, pas de sécurité, et il s'avère être un de mes contrôles les plus efficaces. Sur ses 30 règles, un jeu est explicitement orienté sécurité : interdiction d'accéder à la requête HTTP depuis une classe de service, validation obligatoire sur les contrôleurs, et obligation d'exposer un identifiant externe plutôt que l'identifiant technique dans les DTO. Cette dernière règle élimine une classe entière de fuites par énumération, à la compilation, sans qu'un humain ait à y penser.
Karate est l'autre bonne surprise. Les 41 fichiers de scénarios ne testent pas que le fonctionnel : l'authentification (401 sur jeton invalide, absence d'énumération d'utilisateurs), les portées (403 et non 500 quand un scope manque, nuance qui trahit beaucoup d'implémentations), un garde anti-IDOR sur la suppression RGPD, et les quotas.
Le reste alerte. Semgrep, Gitleaks, Checkov, ZAP, Dependency-Check, Syft, Grype : tous tournent, aucun n'interrompt un build. Et le fuzzing est simplement absent. J'ai bien k6 qui envoie de la charge et qui valide au passage que le rate limiting renvoie ses 429, mais faire du volume sur des entrées valides n'est pas du fuzzing.
Aujourd'hui, la ligne qui bloque vraiment tient en trois noms : ArchUnit, Karate et Harbor. Tout le reste produit du signal.
Et l'IA ?
Le lab expose un endpoint MCP et fait tourner un service d'IA, donc la question se pose déjà.
Ce qui existe : un budget de rate limiting dédié aux routes MCP, des événements d'audit spécifiques pour la création de clé MCP, sa révocation, son rejet et les connexions, et une route de recette qui fait passer les corps JSON-RPC par nginx et ModSecurity. Ce dernier point vient avec un risque documenté de faux positifs du CRS sur les charges JSON imbriquées, que j'ai noté avant même de l'observer.
Ce qui manque : aucun red-teaming de LLM, aucun scan des serveurs MCP installés. Un serveur MCP est du code tiers avec des droits d'exécution, et je le traite aujourd'hui avec moins de rigueur qu'une image Docker. La section « Ouverture » du panorama d'outils liste les pistes de cette famille émergente.
Un outil déployé n'est pas un outil qui protège. Sur les onze outils du tableau, trois bloquent réellement une livraison.
Le SOC : télémétrie, corrélation, réponse
Un SOC, c'est trois choses : voir, comprendre, réagir. Je vois beaucoup, je comprends moyennement, je réagis rarement.
La collecte est la partie la plus mûre. Treize hôtes expédient l'intégralité de leurs journaux vers Graylog, et le WAF y ajoute son propre flux avec un tag dédié qui permet de le filtrer.
## Ce que porte chaque hôte : tout part vers Graylog en UDP
*.* @graylog.monitor.lab:10514
| Source | Transport | Destination | Statut |
|---|---|---|---|
rsyslog *.* sur 13 hôtes |
UDP 10514 | graylog.monitor.lab |
✅ |
| Journal d'audit ModSecurity, tag dédié | UDP 10514 | graylog.monitor.lab |
✅ |
| Événements d'audit applicatifs | GELF UDP 12201 | graylog.monitor.lab |
🔶 |
| Agents Wazuh 4.12 : intégrité, rootcheck, ports ouverts | Agent | Manager Wazuh | 🔶 |
| Traces, logs, métriques, profils | OTLP 4317 et 4318, capteur eBPF | avuru obs, ClickHouse | ✅ |
| Télémétrie du mesh | Istio | Kiali | ✅ |
| Métriques | Scrape | Prometheus, limité au mesh | 🔶 |
| Supervision infra et matériel | Agents Zabbix 7.0 | Zabbix | 🔶 |
| Collecte ECK 9.2.1 | Beats | Elasticsearch, non exploité | 🔶 |
Le GELF n'est activé que sur un seul service, en recette. Wazuh tourne, mais je ne saurais pas dire de tête sur combien d'hôtes exactement, donc le périmètre reste à confirmer. Et ECK collecte consciencieusement des données que personne ne regarde depuis des mois, ce qui est la définition même d'un badge orange.
avuru obs, la brique que j'ai écrite
Au milieu de cette collection d'outils, il y a une brique que je n'ai pas installée mais développée. avuru obs est la plateforme d'observabilité que j'ai substituée à Jaeger dans ce lab, publiée en open source sous licence AGPL sur avuruobs.io.
Elle réunit sur un seul moteur de stockage, ClickHouse, ce que j'avais réparti sur une constellation d'outils : les traces (capteur eBPF sans modification de code, plus l'ingestion OTLP classique), les logs corrélés par identifiant de trace, les métriques RED, le profiling continu, le suivi des erreurs, la santé des services et l'alerting.
Elle embarque aussi un module « green » qui attribue une consommation en Wh et une empreinte en gCO2e par service, calculées à partir de Kepler et des compteurs RAPL du processeur, avec des budgets carbone par service et un export exploitable pour un reporting CSRD. Les suites d'observabilité que j'ai croisées ne mesurent pas cet axe, alors que la directive le réclame chez les entreprises qui y sont soumises.
Concrètement, dans ce lab, la télémétrie du mesh Istio et celle de la gateway APISIX pointent toutes les deux vers avuruobs-gateway.avuru-obs.svc.cluster.local. Le Jaeger d'origine reste un chemin de repli documenté dans le rôle Ansible, au cas où.
La corrélation et la réponse
C'est là que le SOC s'arrête net.
| Contrôle | Outil | Statut | Où c'est défini |
|---|---|---|---|
| Canal d'alerte et chaîne d'automatisation | Discord et n8n | ✅ | Configuration versionnée |
| Streams T0/T1, T2/T3, échecs et définitions d'alerte | Graylog | ⚪ | À créer dans l'interface |
| Remédiation « ModSecurity muet » | Graylog vers n8n | ✅ | Workflow n8n |
Le canal Discord existe et la chaîne vers n8n fonctionne. Mais les streams qui devraient trier les journaux par criticité, et les définitions d'alerte qui devraient les déclencher, restent à créer. J'ai donc un tuyau parfaitement fonctionnel, branché sur presque rien.
Le seul workflow de remédiation qui existe aujourd'hui est aussi le plus révélateur de mes priorités : Graylog détecte que ModSecurity a cessé d'émettre des journaux, déclenche n8n, qui relance la collecte. Autrement dit, la seule chose que mon SOC sait réparer tout seul, c'est son propre aveuglement.
Ce n'est pas ridicule. C'est même le bon premier automatisme, parce qu'un capteur muet est le pire des états : celui où l'absence d'alerte est interprétée comme une absence de problème.
Une alerte qui n'a pas de destinataire n'est pas une alerte. C'est une ligne de journal de plus.
Bilan chiffré
Les tableaux de couches de cet article recensent 57 contrôles. Le décompte par badge est le suivant.
| Élément | Valeur |
|---|---|
| Couches cartographiées | 7 |
| Contrôles recensés | 57 |
| ✅ en place et versionné | 24 |
| 🔶 partiel ou non bloquant | 13 |
| ⚪ cliqué dans une interface | 12 |
| ❌ absent | 8 |
| Microservices | 15 |
| Nœuds Kubernetes sous Istio Ambient 1.30.3 | 6 |
| Hôtes qui expédient leurs journaux | 13 |
| Règles ArchUnit | 30 |
| Fichiers de scénarios Karate | 41 |
| Fichiers portant une annotation d'autorisation | 84 |
| Budgets de rate limiting | 9, de 20 à 300 requêtes par minute |
| Bases sauvegardées chaque nuit | Une quinzaine, à 02:30 |
| Rétention des sauvegardes | 7 jours et 4 semaines, vers le NAS |
| Registre d'images | Harbor v2.14.0, Trivy au seuil critique |
| WAF | ModSecurity v3.0.8, OWASP CRS v4.0.0 |
| Signaux unifiés dans avuru obs | 5 (traces, logs, métriques, profils, erreurs), un seul moteur de stockage |
Vingt-quatre contrôles sur cinquante-sept survivraient à une réinstallation complète. Douze disparaîtraient avec le run-book.
Le ratio qui compte n'est pas 49 contrôles présents sur 57. C'est 24 contrôles reproductibles sur 57.
Ce qui manque, par priorité
Je range cette liste en feuille de route plutôt qu'en confession. Elle est ordonnée par rapport entre le gain de sécurité et l'effort.
- MFA sur Keycloak. Un seul realm à configurer, et toute la surface d'authentification du lab remonte d'un cran.
- Resserrer
pg_hbaet activer TLS sur PostgreSQL. Deux fichiers de configuration pour mon point de défaillance unique. - Faire passer SonarQube, ZAP, Semgrep et Gitleaks de « alerte » à « bloque ». Les outils sont déjà là. Il ne manque qu'un seuil et le courage de casser mes propres builds.
- Sauvegarde hors site chiffrée, pour tenir enfin la règle 3-2-1.
- Décommenter et durcir le
securityContextdes conteneurs, chart par chart. - Créer les streams et les définitions d'alerte Graylog, pour que le tuyau vers Discord serve à autre chose qu'aux tests.
- NetworkPolicies sur les namespaces applicatifs, pour ne plus dépendre uniquement du mesh.
- SBOM signé. Syft produit déjà les inventaires, la signature manque.
- Sortir les mots de passe par défaut des rôles Ansible vers Ansible Vault.
- Scanner les serveurs MCP et introduire du red-teaming LLM, la famille la plus jeune et la moins outillée.
Les quatre premières lignes coûtent moins d'une journée chacune. C'est précisément pour ça qu'elles traînent depuis des mois.
Leçons
1. Un contrôle non versionné n'existe qu'aussi longtemps que la mémoire
Douze de mes contrôles vivent dans des interfaces web. Les règles de l'UDM Pro, le seuil CVE de Harbor, la politique inter-VLAN. Ils fonctionnent parfaitement, aujourd'hui.
Le jour où je remonte cette infrastructure, ils n'existent plus, et rien ne me le dira. Aucun test ne va échouer parce qu'une règle de pare-feu manque. Le service répondra, plus vite même, et je mettrai des semaines à comprendre pourquoi.
C'est la raison pour laquelle j'ai créé un badge distinct plutôt que de compter ces contrôles comme acquis.
2. « Déployé » et « bloquant » sont deux états différents
C'est la leçon qui m'a le plus surpris. J'aurais juré que ma chaîne de sécurité applicative bloquait. En réalité, sur les onze outils recensés dans le tableau du pipeline, trois interrompent une livraison.
Le reste envoie du signal dans un tableau de bord, et un signal que personne ne lit a exactement la même valeur qu'un outil désinstallé, avec en prime le coût CPU et l'illusion de couverture.
Le pire cas n'est pas l'outil absent. C'est l'outil présent dont on croit qu'il protège.
3. L'audit immuable est le contrôle qui m'a coûté le plus, et je ne le regrette pas
L'outbox transactionnel, la chaîne de checksums SHA-256, le trigger qui refuse les modifications, le REVOKE sur le compte applicatif, le partitionnement annuel : c'est de loin ce qui m'a demandé le plus de code.
Et c'est le seul contrôle du lab qui garde sa valeur après une compromission. Tous les autres protègent le présent. Celui-là protège le récit de ce qui s'est passé, y compris contre celui qui a pris la main.
4. Cartographier fait plus pour la sécurité qu'ajouter un outil
Je n'ai rien installé pendant cet exercice. J'ai lu des dépôts, ouvert des interfaces, compté des fichiers et attribué des badges.
Résultat : j'ai découvert que mon securityContext était commenté partout, que le mode effectif de mon WAF était une supposition, et que mes NetworkPolicies n'existaient que dans un chart. Trois trous qu'aucun scanner ne m'aurait signalés, parce qu'aucun scanner ne connaît mon intention.
L'écart entre le schéma et le code est un gisement de vulnérabilités. Il ne se lit qu'en les mettant côte à côte.
Points clés
- Sept couches, 57 contrôles. 24 versionnés, 13 partiels, 12 cliqués à la main, 8 absents. Le décompte est plus instructif que le schéma.
- Trois contrôles bloquent réellement une livraison : ArchUnit, Karate et Harbor. Les huit autres outils du pipeline produisent du signal.
- Le mesh porte l'isolation du cluster, via le mTLS STRICT d'Istio Ambient et une règle explicite entre staging et production, faute de NetworkPolicies sur les namespaces applicatifs.
- L'audit immuable est le contrôle le plus solide : outbox transactionnel, checksum SHA-256 chaîné, trigger de refus et
REVOKEsur le compte applicatif. - La collecte est mûre, la réponse ne l'est pas. Treize hôtes expédient leurs journaux, un seul workflow de remédiation existe, et il répare le SOC lui-même.
Pour aller plus loin
- SOC, SIEM et SOAR : la théorie derrière la couche 7 de cette carte.
- Fondamentaux : DICP, risque et défense en profondeur : pourquoi on empile des couches plutôt que d'en renforcer une seule.
- Sécurité des API et OWASP Top 10 : les classes de failles visées par la couche applicative.
- Panorama d'outils SAST, DAST, SCA et fuzzing : la grille de familles reprise dans le tableau du pipeline.
- Migration urgence vers RAID 1 SSD : comment la couche physique en est arrivée là.
- avuru obs : la plateforme d'observabilité open source utilisée dans ce lab.
Cet article a été écrit en confrontant le schéma au code, dépôt par dépôt, badge par badge. Plusieurs contrôles que je croyais verts sont ressortis oranges, et deux que je croyais oranges étaient rouges. Ils sont restés tels quels. C'est l'esprit du Forgé au lab : la carte vaut par ce qu'elle admet, pas par ce qu'elle affiche.