#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
{
"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
| Variable | Exemple | Description |
|---|---|---|
ELASTICSEARCH_PASSWORD | changeme-es-pass | Mot de passe utilisateur elastic |
KIBANA_PASSWORD | changeme-kibana-pass | Mot de passe utilisateur kibana_system |
LOGSTASH_BEATS_PORT | 5044 | Port d'écoute Filebeat |
LOGSTASH_SYSLOG_PORT | 5000 | Port d'écoute syslog UDP |
ES_JAVA_OPTS | -Xms512m -Xmx512m | Mémoire JVM Elasticsearch |
LS_JAVA_OPTS | -Xms256m -Xmx256m | Mémoire JVM Logstash |
ELASTICSEARCH_URL | http://rgz-elasticsearch:9200 | URL interne Elasticsearch |
Pipeline Logstash — API logs
# 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
# 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
# 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-kibanaSécurité
| Règle | Implémentation |
|---|---|
| SEC-07 | ELASTICSEARCH_PASSWORD et KIBANA_PASSWORD via env vars, jamais en clair |
| SEC-08 | TLS entre Logstash et Elasticsearch en production (ssl_certificate_verification: true) |
| LL#27 | CRITIQUE : Logs Docker max-size: 10m max-file: 3 — Elasticsearch peut écrire des centaines de MB/jour en logs JVM |
| LL#33 | restart: unless-stopped pour les 3 conteneurs |
| LL#43 | Healthcheck ES : curl -u elastic:$PASS http://127.0.0.1:9200/_cluster/health |
| ARCEP | Rétention 12 mois minimum garantie par la politique ILM delete à 365j |
| APDP | Logs contenant des MSISDN/MACs soumis au droit à l'oubli (#78 data-deletion) |
# 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-logstashdansdocker-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-idspour 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