Skip to content

#40 — elk-stack

EN COURS DE DÉPLOIEMENT

Priorité: 🔴 CRITIQUE · Type: TYPE F · Conteneurs: rgz-elasticsearch + rgz-kibana + rgz-logstash · Code: config/logstash/pipelines/

Dépendances: aucune (déployer en parallèle de #38)


Description

elk-stack constitue la couche de centralisation et d'analyse des logs de toute la plateforme RGZ. La stack ELK 8.x (Elasticsearch, Logstash, Kibana) ingère les logs de tous les services — API, RADIUS, gateway nftables, IDS Suricata, et système — les indexe dans Elasticsearch et les rend interrogeables via Kibana. La rétention est fixée à 12 mois, conformément aux obligations légales ARCEP de traçabilité des accès internet.

La politique ILM (Index Lifecycle Management) gère automatiquement le cycle de vie des index : hot (7 jours, SSD rapide) → warm (30 jours, stockage standard) → cold (90 jours, peu d'accès) → delete (365 jours, suppression). Cela évite une accumulation indéfinie de données et garantit les performances de recherche sur les données récentes.

Logstash est le composant d'ingestion. Il écoute sur plusieurs ports selon la source : port 5044 pour les agents Filebeat (JSON structuré de rgz-api), port 5000 UDP pour syslog (RADIUS, système). Les pipelines Logstash enrichissent les logs avec des métadonnées (parsage GeoIP des IPs, extraction du NAS-ID depuis les logs RADIUS, normalisation des timestamps) avant indexation dans Elasticsearch.

L'ELK stack est critique pour la conformité ARCEP (#47) et les logs immutables (#45). Sans elle, les logs de connexion abonnés ne sont pas centralisés et aucun audit de traçabilité n'est possible. Elle alimente également la détection d'anomalies (#44) via les logs Suricata EVE JSON.

LL#27 critique : Les trois conteneurs ELK génèrent des volumes importants de logs Docker. Sans les limites max-size: 10m max-file: 3, le disque du serveur peut être rempli en quelques jours.

Architecture Interne

Sources de logs

    ├── rgz-api (JSON via Filebeat :5044)
    │     Filebeat → Logstash pipeline: api_logs
    │     Format: {"timestamp","level","message","request_id","user_id",...}

    ├── rgz-radius (syslog UDP :5000)
    │     Logstash pipeline: radius_logs
    │     Format: Access-Accept/Reject + Accounting Start/Stop

    ├── rgz-gateway (nftables logs via rsyslog :5000)
    │     Logstash pipeline: firewall_logs
    │     Format: IN=eth0 OUT=eth1 SRC=x.x.x.x DST=y.y.y.y

    └── rgz-ids (Suricata EVE JSON via Filebeat :5044)
          Logstash pipeline: ids_logs
          Format: {"event_type":"alert","alert":{"signature":...},...}


rgz-logstash:5044/5000

    ├── Pipeline parsing + enrichissement
    │     (GeoIP, user-agent, normalisation champs)


rgz-elasticsearch:9200

    ├── Index: rgz-api-YYYY.MM.DD
    ├── Index: rgz-radius-YYYY.MM.DD
    ├── Index: rgz-ids-YYYY.MM.DD
    ├── Index: rgz-firewall-YYYY.MM.DD
    └── ILM Policy: hot(7j)→warm(30j)→cold(90j)→delete(365j)


rgz-kibana:5601
    ├── Discover (recherche full-text)
    ├── Dashboards (sessions RADIUS, alertes IDS)
    └── Security (SIEM intégré ELK 8.x)

Politique ILM

json
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": { "max_size": "5GB", "max_age": "7d" },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },
          "set_priority": { "priority": 50 }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "freeze": {},
          "set_priority": { "priority": 0 }
        }
      },
      "delete": {
        "min_age": "365d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Configuration

Variables d'environnement

VariableExempleDescription
ELASTICSEARCH_PASSWORDchangeme-es-passMot de passe utilisateur elastic
KIBANA_PASSWORDchangeme-kibana-passMot de passe utilisateur kibana_system
LOGSTASH_BEATS_PORT5044Port d'écoute Filebeat
LOGSTASH_SYSLOG_PORT5000Port d'écoute syslog UDP
ES_JAVA_OPTS-Xms512m -Xmx512mMémoire JVM Elasticsearch
LS_JAVA_OPTS-Xms256m -Xmx256mMémoire JVM Logstash
ELASTICSEARCH_URLhttp://rgz-elasticsearch:9200URL interne Elasticsearch

Pipeline Logstash — API logs

ruby
# config/logstash/pipelines/api_logs.conf
input {
  beats {
    port => 5044
    tags => ["api"]
  }
}

filter {
  if "api" in [tags] {
    json { source => "message" }
    date { match => ["timestamp", "ISO8601"] target => "@timestamp" }
    mutate {
      add_field => { "service" => "rgz-api" }
    }
  }
}

output {
  if "api" in [tags] {
    elasticsearch {
      hosts => ["rgz-elasticsearch:9200"]
      user => "elastic"
      password => "${ELASTICSEARCH_PASSWORD}"
      index => "rgz-api-%{+YYYY.MM.dd}"
      ilm_enabled => true
      ilm_rollover_alias => "rgz-api"
      ilm_policy => "rgz-logs-policy"
    }
  }
}

Pipeline Logstash — RADIUS logs

ruby
# config/logstash/pipelines/radius_logs.conf
input {
  udp {
    port => 5000
    tags => ["radius"]
  }
}

filter {
  if "radius" in [tags] {
    grok {
      match => {
        "message" => "%{WORD:radius_packet_type} %{WORD:nas_id} %{GREEDYDATA:radius_data}"
      }
    }
    mutate { add_field => { "service" => "rgz-radius" } }
  }
}

output {
  if "radius" in [tags] {
    elasticsearch {
      hosts => ["rgz-elasticsearch:9200"]
      user => "elastic"
      password => "${ELASTICSEARCH_PASSWORD}"
      index => "rgz-radius-%{+YYYY.MM.dd}"
    }
  }
}

Commandes Utiles

bash
# Vérifier la santé du cluster Elasticsearch
curl -u elastic:$ELASTICSEARCH_PASSWORD \
  http://127.0.0.1:9200/_cluster/health | jq .
# status doit être "green" ou "yellow" (jamais "red")

# Lister les index existants avec leur taille
curl -u elastic:$ELASTICSEARCH_PASSWORD \
  http://127.0.0.1:9200/_cat/indices?v | grep rgz

# Vérifier la politique ILM appliquée
curl -u elastic:$ELASTICSEARCH_PASSWORD \
  http://127.0.0.1:9200/_ilm/policy/rgz-logs-policy | jq .

# Rechercher dans les logs API (dernière heure)
curl -u elastic:$ELASTICSEARCH_PASSWORD \
  -H "Content-Type: application/json" \
  http://127.0.0.1:9200/rgz-api-*/_search -d '{
    "query": {
      "range": { "@timestamp": { "gte": "now-1h" } }
    },
    "size": 10,
    "sort": [{ "@timestamp": { "order": "desc" } }]
  }' | jq '.hits.hits[]._source | {timestamp: .["@timestamp"], level, message}'

# Vérifier Logstash (pipelines actives)
curl http://127.0.0.1:9600/_node/pipelines | jq .

# Accéder à Kibana (depuis l'hôte, si port exposé)
# http://127.0.0.1:5601  (user: elastic, pass: $ELASTICSEARCH_PASSWORD)

# Logs Elasticsearch
docker logs rgz-elasticsearch -f --tail=50

# Logs Logstash
docker logs rgz-logstash -f --tail=50

# Restart ELK (ordre important: ES d'abord, puis Logstash, puis Kibana)
docker compose -f /home/claude-dev/RGZ/docker-compose.monitoring.yml \
  restart rgz-elasticsearch && sleep 10 && \
  docker compose -f /home/claude-dev/RGZ/docker-compose.monitoring.yml \
  restart rgz-logstash rgz-kibana

Sécurité

RègleImplémentation
SEC-07ELASTICSEARCH_PASSWORD et KIBANA_PASSWORD via env vars, jamais en clair
SEC-08TLS entre Logstash et Elasticsearch en production (ssl_certificate_verification: true)
LL#27CRITIQUE : Logs Docker max-size: 10m max-file: 3 — Elasticsearch peut écrire des centaines de MB/jour en logs JVM
LL#33restart: unless-stopped pour les 3 conteneurs
LL#43Healthcheck ES : curl -u elastic:$PASS http://127.0.0.1:9200/_cluster/health
ARCEPRétention 12 mois minimum garantie par la politique ILM delete à 365j
APDPLogs contenant des MSISDN/MACs soumis au droit à l'oubli (#78 data-deletion)
yaml
# Fragment docker-compose.monitoring.yml — Elasticsearch
rgz-elasticsearch:
  image: elasticsearch:8.x.x
  restart: unless-stopped
  environment:
    discovery.type: single-node
    ELASTIC_PASSWORD: ${ELASTICSEARCH_PASSWORD}
    ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    xpack.security.enabled: "true"
  volumes:
    - elasticsearch_data:/usr/share/elasticsearch/data
  healthcheck:
    test: ["CMD-SHELL", "curl -s -u elastic:${ELASTICSEARCH_PASSWORD} http://127.0.0.1:9200/_cluster/health | grep -v '\"status\":\"red\"'"]
    interval: 30s
    timeout: 10s
    retries: 5
  logging:
    driver: "json-file"
    options:
      max-size: "10m"
      max-file: "3"

Implémentation TODO

  • [x] Services rgz-elasticsearch, rgz-kibana, rgz-logstash dans docker-compose.monitoring.yml
  • [ ] config/logstash/pipelines/api_logs.conf — pipeline API JSON
  • [ ] config/logstash/pipelines/radius_logs.conf — pipeline RADIUS syslog
  • [ ] config/logstash/pipelines/firewall_logs.conf — pipeline nftables
  • [ ] config/logstash/pipelines/ids_logs.conf — pipeline Suricata EVE JSON
  • [ ] config/logstash/ilm-policy.json — politique ILM 4 phases
  • [ ] Appliquer la politique ILM au démarrage (script init ES)
  • [ ] Configurer Filebeat sur rgz-api (sidecar ou via volume log)
  • [ ] Configurer Filebeat sur rgz-ids pour EVE JSON
  • [ ] Créer les index templates avec mapping champs (msisdn, nas_id, session_id)
  • [ ] Configurer Kibana index patterns rgz-*
  • [ ] Créer les dashboards Kibana (sessions RADIUS, alertes IDS, logs API)
  • [ ] Tester la rétention : créer index > 365j et vérifier suppression automatique
  • [ ] Tester le healthcheck en mode "yellow" (single-node normal)

Dernière mise à jour: 2026-02-21

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone