ELK (Elasticsearch, Logstash, Kibana)¶
Rôle¶
Stockage, indexation et visualisation des logs de sécurité du SOC — le volet
« à chaud » du pipeline (voir Vue d'ensemble SOC), en
parallèle de l'archivage à froid assuré par syslog-ng.
Héberge également Fleet Server, point d'enrôlement centralisé pour les
Elastic Agent (dont les futurs postes Windows du domaine infra.indio,
déployés par GPO) — bien que ce volet reste actuellement bloqué, voir
Points d'attention.
Architecture¶
9 VM Rocky 9.6 (tmpl-rocky96-hardened), segment seg-soc (10.100.20.128/25) :
| Nœud | IP | Rôle |
|---|---|---|
| INDSOC005 | 10.100.20.134 | Elasticsearch (master dédié) |
| INDSOC006 | 10.100.20.135 | Elasticsearch (master dédié) |
| INDSOC007 | 10.100.20.136 | Elasticsearch (master dédié) |
| INDSOC008 | 10.100.20.137 | Elasticsearch (data) |
| INDSOC009 | 10.100.20.138 | Elasticsearch (data) |
| INDSOC010 | 10.100.20.139 | Elasticsearch (data) |
| INDSOC014 | 10.100.20.143 | Logstash |
| INDSOC015 | 10.100.20.144 | Logstash |
| INDSOC017 | 10.100.20.146 | Kibana + Fleet Server |
- Cluster Elasticsearch nommé
soc-indio: 3 masters dédiés (quorum 2/3, pas de rôle data) + 3 nœuds data génériques (rôlesdata,data_content,ingest). - Simplifié le 2026-07-14 : l'ancien cluster (9 ES en 4 tiers — 3 master
- 3 data_hot + 2 data_warm + 1 ml — + 3 Logstash) a été réduit par
ajustement chirurgical de
node.rolessur les nœuds conservés, pas par reconstruction. Les nœudsdata_warm/mlet le 3ᵉ Logstash ont été détruits. xpack.securityactif (TLS + authentification) sur tout le cluster.- Accès Kibana : reverse proxy nginx local (443, TLS) sur INDSOC017. Le
README.mddu projet mentionne encore une exposition via la VIP HAProxy partagée (10.100.2.130:5601) comme méthode de vérification — obsolète (retirée le 2026-07-15, vérifié : plus d'entréekibanadans/root/haproxy/ansible/group_vars/haproxy.yml, ni de règlehaproxy-to-soc-kibanadans/root/nsx/dfw_soc.tf) — seul le nginx local sert l'accès aujourd'hui, via la règle DFWmgmt-to-soc-kibana.
Schéma — pipeline logs-audit (seul pont Kafka fonctionnel à ce jour)¶
flowchart LR
Fleet["Flotte Indio<br/>audisp-syslog + omkafka"] -->|"topic logs-audit"| Kafka[("Kafka<br/>INDSOC002-004")]
Kafka -->|"consumer group<br/>logstash-audit"| LS14["Logstash<br/>INDSOC014"]
Kafka -->|"consumer group<br/>logstash-audit"| LS15["Logstash<br/>INDSOC015"]
LS14 -->|"action=create<br/>index=logs-audit"| ES[("Elasticsearch soc-indio<br/>data stream logs-audit<br/>ILM soc-logs, 90j")]
LS15 -->|"action=create<br/>index=logs-audit"| ES
ES --> KIB["Kibana<br/>INDSOC017<br/>Data View 'SOC - Audit'"]
Les 2 nœuds Logstash partagent le même group_id (logstash-audit) : Kafka
répartit automatiquement les 3 partitions du topic entre eux, sans doublon.
Voir Kafka — topic logs-audit pour la chaîne
complète en amont (auditd → audisp-syslog → Kafka) et le piège SELinux
rencontré côté producteurs.
Provisioning Terraform¶
Fichiers : main.tf, variables.tf, versions.tf,
elasticsearch_config.tf, kibana_config.tf, terraform.tfvars.example.
vsphere_virtual_machine.elkenfor_eachsurvar.nodes, dimensionnement par nœud (RAM, disque OS, disque data dédié en option) : masters 16 Go RAM/60 Go disque, data 32 Go RAM/disque dédié 600 ou 400 Go, Logstash/Kibana sur les valeurs par défaut (4 Go RAM).- Réseau
seg-soc, réservation IP via data source NetBox (netbox_ip_addresses, filtrée par nom DNS). - Providers
hashicorp/vsphere(>= 2.6.0),e-breuninger/netbox(>= 5.0, < 6.0) pour le provisioning VM, etelastic/elasticstack(>= 0.11.0, < 1.0.0, ajouté le 2026-07-17) pour la configuration logique du cluster déjà en service. output vms: map nom → IP.
elasticsearch_config.tf / kibana_config.tf — configuration logique via elastic/elasticstack¶
Ajoutés le 2026-07-17 : jusque-là, /root/elk ne portait que le
provisioning VM — aucun rôle/index template/ILM n'était géré en Terraform,
tout était absent ou posé à la main via l'API. Le provider s'authentifie en
superuser elastic (mot de passe déjà vaulté, voir Configuration Ansible)
avec ca_file pointant sur le CA Vault PKI déjà déployé — pas
ca_certs, qui n'existe que pour les blocs kibana{}/fleet{} du
provider (elasticsearch{} utilise ca_file/ca_data, vérifié via
terraform providers schema -json plutôt que deviné).
Ressources créées :
elasticstack_elasticsearch_index_lifecycle.soc_logs(« soc-logs ») : hot-only (rollovermax_age=1d/max_primary_shard_size=20gb) puis suppression à90d— pas de warm/cold, cohérent avec la fin du tiering du 2026-07-14. Valeurs de rétention par défaut, non demandées explicitement par l'exploitant à ce stade.elasticstack_elasticsearch_component_template.soc_logs_settings: 1 shard/1 réplica +index.lifecycle.name=soc-logs.elasticstack_elasticsearch_index_template.soc_logs(« soc-logs »,data_stream{}, priority 200) :index_patternscouvre les 6 flux réels — les 5 topics Kafka historiques (logs-cloud,logs-firewall,logs-identity,logs-infra,logs-windows) etlogs-audit. Un seul data stream par nom, priority 200 pour ne pas entrer en collision avec le template générique intégrélogs([logs-*-*], priority 100, déjà en place pour Fleet/APM/otel).elasticstack_elasticsearch_security_role.logstash_writer+elasticstack_elasticsearch_security_user.logstash_writer: rôle référencé dans le pipeline d'exemple du rôle Logstash depuis le tout premier déploiement mais jamais créé jusqu'à cette date — c'était un TODO bloquant explicite. Scope volontairement restreint aux 6 data streams SOC (auto_configure/create_doc/view_index_metadata), paslogs-*(couvrirait aussi les indices Fleet/agents déjà en place). Mot de passe généré aléatoirement, vaulté dansterraform.tfvars(gitignored). Ce compte est aujourd'hui réellement utilisé par le pipelineaudit-kafkade Logstash (voir Configuration Ansible ci-dessous) — vérifié par une écriture de test réussie au moment de sa création, puis confirmé en conditions réelles par l'ingestion effective du topiclogs-audit.elasticstack_kibana_data_view.logs_audit(kibana_config.tf) : Data View Kibana « SOC - Audit (auditd) »,title=logs-audit,time_field_name=@timestamp— permet de parcourir les logs d'audit dans Discover sans passer par l'API ES brute.
DFW ouverte pour ce provider
Le poste de contrôle (là où tourne Terraform) doit joindre l'API ES en
direct : règle mgmt-to-soc-elasticsearch ajoutée dans
/root/nsx/dfw_soc.tf (source grp-mgmt-nat, destination grp-elasticsearch
= les 6 IP INDSOC005-010, port 9200). Le port 9200 est donc désormais
joignable depuis le réseau management — contrairement à une contrainte
plus ancienne (port fermé, health-check obligatoirement relayé par
Ansible/SSH) qui ne s'applique plus depuis l'ouverture de cette règle
dédiée, scopée à la même source que mgmt-to-soc-ssh/mgmt-to-soc-kibana.
Configuration Ansible¶
site.ymlorchestre, dans l'ordre : dépôt YUM Elastic (elastic_repo) sur les 9 nœuds → cluster Elasticsearch masters (bootstrap) → data (rejoignent les masters) → finalisation des mots de passe applicatifs (tâche ciblée surelasticsearch_master[0]) → Logstash → Kibana +kibana_proxy.fleet-server.yml: playbook séparé, rôlefleet_serversur le nœud Kibana (installationelastic-agent, enrôlement viaelastic-agent install --fleet-server-*, port 8220) — voir Points d'attention pour son état réel actuel (bloqué).- Rôles :
elastic_repo,elasticsearch,logstash,kibana,kibana_proxy,fleet_server. - TLS : certificats émis par la PKI interne Vault (
pki_int/issue/infra-indio, TTL 8760h/1 an — voir Vault & PKI), un certificat par nœud Elasticsearch (CN=FQDN, réutilisé transport+HTTP), CA commune dupliquée dans chaque rôle (files/tls/), certificat dédié pour Kibana. Clés privées exclues du dépôt (.gitignoresur*.key). - Comptes applicatifs
elastic/kibana_systemet clés de chiffrement Kibana (xpack.security.encryptionKey,encryptedSavedObjects,reporting) — ces dernières indispensables pour ne pas invalider les sessions/l'alerting à chaque redémarrage — chiffrés ansible-vault (déchiffrement via/root/.elk_vault_pass), appliqués viaelasticsearch-reset-password, idempotent via un fichier marqueur (/etc/elasticsearch/.elk_*_password_set). Vérifié en conditions réelles : le cluster tourne en production depuis le 2026-07-08, ce mécanisme a donc bien été exécuté avec succès (pas seulement écrit puis jamais rejoué). Le service token Fleet Server suit la même logique de chiffrement ansible-vault. - Dépôt YUM Elastic consommé directement depuis
artifacts.elastic.co(un miroir Nexus dédié a été évalué puis abandonné, absent du Nexus réel) via le proxy Squid10.100.10.4:3128. - Disque data dédié : partitionné/monté au premier passage du rôle
(idempotent, migration automatique des données si le nœud a déjà tourné
sur le LV
/var).
Rôle logstash — pipelines réels¶
pipelines.ymldéclare deux pipelines actifs :placeholder(heartbeat→null) : Logstash refuse de démarrer avec unpipelines.ymlsans au moins un pipeline déclaré (ERROR: Pipelines YAML file is empty) — ce placeholder, coût nul, reste en place même une fois un vrai pipeline actif.audit-kafka(audit-kafka.conf.j2) — voir section dédiée ci-dessous.
example.conf.disabled: gabarit de référence pour un futur pipeline (input { beats {...} }vers Elasticsearch), déployé mais non déclaré danspipelines.yml— ses hosts/identifiants ne sont pas valides tels quels, à adapter avant activation./tmpmonté noexec (durcissement CIS du template) : le provider FFI de JRuby y échoue aummap PROT_EXEC(Could not load FFI Provider, fatal pour Logstash — contrairement au warning bénin équivalent côté Elasticsearch/jansi) — contourné en redirigeantjava.io.tmpdirvers/var/lib/logstash/tmp.
Pipeline audit.log (Kafka -> Logstash -> Elasticsearch)¶
Seul pont Kafka → ELK fonctionnel de bout en bout à ce jour (construit
le 2026-07-17). Fichier : roles/logstash/templates/audit-kafka.conf.j2,
déployé sur INDSOC014/INDSOC015.
- Entrée :
kafka { bootstrap_servers => "kafka-node01.infra.indio:9092,...", topics => ["logs-audit"], group_id => "logstash-audit" }— nécessite que Logstash résolve les 3 noms interneskafka-node0N(enregistrements DNS A créés surINDIDEN008à cet effet, voir Kafka). - Filtre : deux passes
groksuccessives (en-tête RFC5424, puis en-têtetype=X msg=audit(epoch:serial):), puis unkvunique sur les paires clé=valeur de l'événementauditd(éventuellement combinées si un sous-messagemsg='...'imbriqué existe, typique des événements PAM). Champcmd=(hex-encodé parauditd) décodé en[audit][cmd_decoded]via un filtrerubydédié quand présent. - Sortie :
elasticsearch { action => "create", index => "logs-audit", manage_template => false }, authentifié avec le comptelogstash_writer(voir Provisioning Terraform ci-dessus).
Deux pièges de parsing réels rencontrés (2026-07-17)
kv { target => "audit" }appelé plusieurs fois sur le mêmetargetREMPLACE le contenu à chaque appel au lieu de fusionner — seul le dernier appel survivait (3 appels séparés outer/inner/trailer ne laissaient que les champs du dernier). Fix : combiner les 3 sources en une seule chaîne (mutate add_fieldavec interpolation) puis un seul appelkv.data_stream => truerejette unindexnommé explicitement (Invalid data stream configuration) — ce mode ne supporte que la construction<type>-<dataset>-<namespace>. Pour cibler un data stream déjà créé sous un nom exact (comme ici),action => "create"+index => "<nom exact>"suffit (un data stream exigecreate, pasindex, pour respecter sa contrainte append-only).- Sans
manage_template => false, le plugin de sortie tente d'installer son propre template ECS par défaut au démarrage (PUT _index_template/ecs-logstash) — 403 avec le rôlelogstash_writervolontairement restreint, et le pipeline entier refuse de démarrer sur cette erreur (pas un simple warning). Le template étant déjà géré par Terraform,manage_template => falseest le fix correct plutôt que d'élargir les privilèges du rôle.
Le piège SELinux qui a bloqué silencieusement le trafic producteur
(syslogd_t → port 9092) est documenté côté Kafka
— il concerne les VM productrices (toute la flotte), pas Logstash lui-même.
Vérifié bout en bout : un événement sudo réel déclenché sur une VM de test
est retrouvé, avec tous les champs attendus (pid, uid, auid, exe,
acct, etc.), dans le data stream Elasticsearch logs-audit — confirmé sur
503 événements USER_ACCT déjà indexés après rattrapage du retard
accumulé pendant le blocage SELinux (~460 000 messages, voir
Kafka).
Procédure manuelle¶
Reprise du chantier Fleet Server (actuellement bloqué)¶
Le rôle fleet_server est écrit et déployé, mais son exécution a provoqué
deux incidents de disponibilité de Kibana le 2026-07-15 (VM saturée
jusqu'au reset dur via govc vm.power -reset). L'exploitant a repris ce
chantier en direct (« fleet server c moi qui fait ») ; elastic-agent est
actuellement masqué (systemctl mask) sur INDSOC017, dans un état
arrêté propre. Les 2 policies Kibana déjà créées (IND-Fleet-Server sans
intégration system, IND-Windows-Workstations avec Elastic Defend) restent
valides et réutilisables.
Ne pas relancer ansible-playbook fleet-server.yml ni
elastic-agent install --fleet-server-* sans demande explicite de
l'exploitant. Si une reprise est demandée, procédure recommandée (tirée
des 3 tentatives déjà menées) :
systemctl unmask elastic-agentsurINDSOC017avant toute chose.- Vérifier via
GET /api/fleet/package_policiesqu'aucune intégration « System » n'est attachée àfleet-server-policy(celle-ci serait issue d'un appelPOST .../agent_policies?sys_monitoring=true— supprimer cette package policy si présente,DELETE /api/fleet/package_policies/<id>, avant tout nouvel essai). Ce paramètre n'était pas la cause unique du blocage d'enrôlement lors des tentatives précédentes, mais aggrave la charge CPU/mémoire pendant le bootstrap. - Lancer l'enrôlement avec une borne de temps explicite et non
contournable côté hôte — pas seulement un
timeoutcôté commande (insuffisant lors du 2ᵉ incident : les composants déjà démarrés parelastic-agent installcontinuent de tourner en tâche de fond après l'expiration dutimeoutde la commande elle-même). Unasync/pollAnsible avec une politique d'arrêt franche des sous-process est préférable à un simple wrappertimeout. - Si possible, tester d'abord sur une VM jetable dédiée plutôt que sur
INDSOC017(colocalisé avec Kibana, déjà tombé deux fois). - Avant toute nouvelle tentative, lire intégralement
/var/log/elastic-agent/d'un run précédent mené jusqu'au bout (sans le tuer) — la cause exacte du blocage d'enrôlement (le port 8220 ne s'ouvre jamais, l'agent reste « Waiting For Enroll... ») n'a jamais été confirmée à 100 % malgré trois tentatives et l'élimination de l'hypothèsesys_monitoringcomme cause unique.
Procédure de déploiement¶
cd /root/elk/ansible
ansible-galaxy collection install community.general ansible.posix
ansible-playbook --syntax-check site.yml
ansible-playbook site.yml -e @/root/.hap_secrets.yml
# Fleet Server (playbook séparé) : NE PAS lancer sans accord explicite de
# l'exploitant, voir Procédure manuelle ci-dessus.
# ansible-playbook fleet-server.yml -e @/root/.hap_secrets.yml
Configuration logique du cluster (rôles/index template/ILM) — une fois le cluster Ansible en place et sain :
Un MANUEL_INSTALLATION.md existe à la racine du projet mais est explicitement
marqué périmé (topologie 2026-07-08 à 6 nœuds sur seg-supervision,
jamais celle réellement en service) — conservé à titre historique
uniquement, à ne pas suivre pour une installation réelle.
Contrôle de santé / Vérification¶
# Depuis le poste de contrôle (règle DFW mgmt-to-soc-elasticsearch) :
curl -sk -u elastic:<mot_de_passe> https://10.100.20.134:9200/_cluster/health?pretty
# attendu : "number_of_nodes": 6, "status": "green" (ou "yellow" sans index)
# Data stream logs-audit (pipeline audit.log) :
curl -sk -u elastic:<mot_de_passe> https://10.100.20.134:9200/_data_stream/logs-audit?pretty
curl -sk -u logstash_writer:<mot_de_passe> \
https://10.100.20.134:9200/logs-audit/_count?pretty
Kibana : https://<hostname>.infra.indio/ via le nginx local du nœud
INDSOC017. Fleet Server : https://10.100.20.146:8220/api/status — actuellement
sans objet, service masqué (voir Procédure manuelle).
Points d'attention¶
Aucun secret reproduit ici
Mots de passe elastic/kibana_system/logstash_writer, clés de
chiffrement Kibana et service token Fleet Server sont chiffrés en
ansible-vault (group_vars/) ou dans un terraform.tfvars gitignored.
Aucune valeur déchiffrée n'a été lue ni reproduite pour cette page —
valeurs gérées séparément (déchiffrement via /root/.elk_vault_pass) —
voir Gestion des secrets.
- Historique de sécurité : un incident de clés privées TLS committées par erreur s'est produit sur ce projet précis (corrigé le 2026-07-05, clés purgées et régénérées) — à l'origine du renforcement des règles de vérification de secrets sur l'ensemble de l'IaC Indio (voir Gestion des secrets).
- Piège bind localhost vs network.host : l'API monitoring HTTP de
Logstash (port 9600) n'écoute que sur
127.0.0.1par défaut, contrairement à Elasticsearch qui écoute sur l'IP réelle du nœud (network.host) — à garder en tête pour tout health-check ou intégration de supervision. /tmpennoexec(durcissement du template) : voir « Rôlelogstash— pipelines réels » ci-dessus.- Fleet Server bloqué : voir « Procédure manuelle » ci-dessus — ne pas
supposer ce composant opérationnel malgré sa présence dans
site.yml/le rôlefleet_server. - Pont Kafka pour les 5 topics historiques toujours absent : seul
logs-auditdispose d'un pipeline Logstash réel. Les 5 topicslogs-cloud/firewall/identity/infra/windowsn'ont aucun consommateur — la destination Elasticsearch est prête (index templatesoc-logsles couvre déjà) mais aucun pipelineinput { kafka {...} }ne les lit. Voir Vue d'ensemble SOC pour le schéma complet du gap. - Kibana nécessite ses propres réglages de proxy sortant (drop-in systemd
HTTP_PROXY+xpack.fleet.registryProxyUrl) : le plugin Fleet ne respecte pas les variables d'environnement standard du service.