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ôles data, 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.roles sur les nœuds conservés, pas par reconstruction. Les nœuds data_warm/ml et le 3ᵉ Logstash ont été détruits.
  • xpack.security actif (TLS + authentification) sur tout le cluster.
  • Accès Kibana : reverse proxy nginx local (443, TLS) sur INDSOC017. Le README.md du 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ée kibana dans /root/haproxy/ansible/group_vars/haproxy.yml, ni de règle haproxy-to-soc-kibana dans /root/nsx/dfw_soc.tf) — seul le nginx local sert l'accès aujourd'hui, via la règle DFW mgmt-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.elk en for_each sur var.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, et elastic/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 (rollover max_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_patterns couvre les 6 flux réels — les 5 topics Kafka historiques (logs-cloud, logs-firewall, logs-identity, logs-infra, logs-windows) et logs-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), pas logs-* (couvrirait aussi les indices Fleet/agents déjà en place). Mot de passe généré aléatoirement, vaulté dans terraform.tfvars (gitignored). Ce compte est aujourd'hui réellement utilisé par le pipeline audit-kafka de 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 topic logs-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.yml orchestre, 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 sur elasticsearch_master[0]) → Logstash → Kibana + kibana_proxy.
  • fleet-server.yml : playbook séparé, rôle fleet_server sur le nœud Kibana (installation elastic-agent, enrôlement via elastic-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 (.gitignore sur *.key).
  • Comptes applicatifs elastic/kibana_system et 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 via elasticsearch-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 Squid 10.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.yml déclare deux pipelines actifs :
    • placeholder (heartbeatnull) : Logstash refuse de démarrer avec un pipelines.yml sans 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é dans pipelines.yml — ses hosts/identifiants ne sont pas valides tels quels, à adapter avant activation.
  • /tmp monté noexec (durcissement CIS du template) : le provider FFI de JRuby y échoue au mmap PROT_EXEC (Could not load FFI Provider, fatal pour Logstash — contrairement au warning bénin équivalent côté Elasticsearch/jansi) — contourné en redirigeant java.io.tmpdir vers /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 internes kafka-node0N (enregistrements DNS A créés sur INDIDEN008 à cet effet, voir Kafka).
  • Filtre : deux passes grok successives (en-tête RFC5424, puis en-tête type=X msg=audit(epoch:serial):), puis un kv unique sur les paires clé=valeur de l'événement auditd (éventuellement combinées si un sous-message msg='...' imbriqué existe, typique des événements PAM). Champ cmd= (hex-encodé par auditd) décodé en [audit][cmd_decoded] via un filtre ruby dédié quand présent.
  • Sortie : elasticsearch { action => "create", index => "logs-audit", manage_template => false }, authentifié avec le compte logstash_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ême target REMPLACE 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_field avec interpolation) puis un seul appel kv.
  • data_stream => true rejette un index nommé 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 exige create, pas index, 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ôle logstash_writer volontairement 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 => false est 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) :

  1. systemctl unmask elastic-agent sur INDSOC017 avant toute chose.
  2. Vérifier via GET /api/fleet/package_policies qu'aucune intégration « System » n'est attachée à fleet-server-policy (celle-ci serait issue d'un appel POST .../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.
  3. Lancer l'enrôlement avec une borne de temps explicite et non contournable côté hôte — pas seulement un timeout côté commande (insuffisant lors du 2ᵉ incident : les composants déjà démarrés par elastic-agent install continuent de tourner en tâche de fond après l'expiration du timeout de la commande elle-même). Un async/poll Ansible avec une politique d'arrêt franche des sous-process est préférable à un simple wrapper timeout.
  4. 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).
  5. 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èse sys_monitoring comme 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 :

cd /root/elk
terraform init
terraform apply

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.1 par 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.
  • /tmp en noexec (durcissement du template) : voir « Rôle logstash — 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ôle fleet_server.
  • Pont Kafka pour les 5 topics historiques toujours absent : seul logs-audit dispose d'un pipeline Logstash réel. Les 5 topics logs-cloud/firewall/identity/infra/windows n'ont aucun consommateur — la destination Elasticsearch est prête (index template soc-logs les couvre déjà) mais aucun pipeline input { 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.