Aller au contenu principal

Module 14 — Sécurité, utilisateurs, sauvegardes et exploitation au quotidien

Le moteur Veille est en place, indexé et interrogé ; Inès veut maintenant deux garanties avant d'exposer quoi que ce soit à un client. D'abord, que l'API de Karim ne puisse pas modifier un article par accident. Ensuite, qu'un incident matériel n'efface pas six mois d'agrégations enregistrées dans Kibana. Ce module met en place les rôles, les clés d'API, les snapshots et la routine de surveillance qui font qu'un cluster tient dans la durée.

Ce que le kit sécurise déjà

Ouvrez docker-compose.yml du kit et lisez le bloc elasticsearch : la sécurité est activée par défaut dans le laboratoire, ce que beaucoup de tutoriels antérieurs oublient. Quatre lignes suffisent à comprendre l'état du cluster.

- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}

Cela signifie trois choses. Chaque requête est authentifiée : sans identifiants, Elasticsearch répond 401 security_exception. Le mot de passe superutilisateur est celui du fichier .env (veille2026 par défaut), défini au premier démarrage et gravé dans le volume es-data. HTTP circule en clair : les échanges entre veille-kibana, veille-python et veille-es ne sont pas chiffrés, ce qui est acceptable dans un réseau Docker isolé mais inacceptable dès qu'on expose le port 9200 à l'extérieur.

Le service veille-setup complète le tableau : il attend qu'Elasticsearch soit healthy, puis appelle l'API /_security/user/kibana_system/_password pour aligner le mot de passe interne de l'utilisateur système sur KIBANA_PASSWORD. Vous n'avez donc jamais à copier un token à la main entre les deux services.

HTTP sans TLS = atelier seulement

Le kit privilégie la lisibilité : un curl ou un ./lab.sh es fonctionne sans certificat. En production, il faut basculer sur HTTPS. Elastic fournit un utilitaire elasticsearch-certutil (dans l'image, bin/elasticsearch-certutil ca puis cert) qui génère l'autorité et les certificats du nœud, puis on active xpack.security.http.ssl.enabled=true et on monte les fichiers dans le conteneur. Ce cours reste sur HTTP ; la démarche de bascule est documentée sur elastic.co et se met en place en une heure.

Créer un rôle et un utilisateur en lecture seule pour Karim

L'API de Karim n'a besoin que de lire l'index news : jamais d'écrire, jamais de supprimer, jamais de toucher aux mappings. On lui construit un rôle dédié, puis un utilisateur qui porte ce rôle. Ouvrez Kibana Dev Tools.

POST _security/role/veille_lecture
{
"cluster": ["monitor"],
"indices": [
{
"names": ["news"],
"privileges": ["read", "view_index_metadata"]
}
]
}

read autorise _search, _count, _msearch, _mget ; view_index_metadata permet de récupérer le mapping et les paramètres, indispensable pour un client qui veut connaître le nom du champ temporel. monitor au niveau cluster autorise _cluster/health : la sonde de vie de l'application n'aura pas à se réauthentifier avec elastic.

POST _security/user/api_karim
{
"password": "karim2026",
"roles": ["veille_lecture"],
"full_name": "API Veille (Karim)",
"email": "karim@veille.example"
}

Elasticsearch répond {"created": true}. Vérifiez immédiatement avec deux appels depuis un terminal, dans le conteneur veille-es qui embarque curl :

docker exec veille-es curl -s -u api_karim:karim2026 \
http://localhost:9200/news/_count

Sortie attendue :

{"count":200853,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}

La lecture passe. Testez maintenant qu'une écriture est refusée :

docker exec veille-es curl -s -u api_karim:karim2026 \
-H 'Content-Type: application/json' \
-X PUT http://localhost:9200/news/_doc/999999 \
-d '{"headline":"tentative","date":"2026-09-09"}'

Sortie attendue :

{"error":{"root_cause":[{"type":"security_exception","reason":"action [indices:data/write/index] is unauthorized for user [api_karim] with effective roles [veille_lecture] on indices [news]"}],"status":403}}

403 security_exception : le rôle fait exactement ce qu'on lui a demandé. C'est le premier réflexe de test dès qu'on crée un utilisateur — vérifier ce qui passe et ce qui doit être refusé.

Lister rôles et utilisateurs

GET _security/role/veille_lecture et GET _security/user/api_karim renvoient la définition JSON complète. GET _security/_query/user liste tous les utilisateurs paginés. Sur Kibana, Management → Stack Management → Security → Users / Roles offre la même chose à la souris.

Les clés d'API pour l'authentification machine

Un mot de passe fonctionne, mais chaque service qui l'utilise doit connaître le mot de passe complet, et le révoquer proprement demande de tout redéployer. Les clés d'API règlent le problème : chaque service reçoit une clé qui lui est propre, avec un rôle attaché et une date d'expiration. On révoque une clé sans toucher au reste.

POST _security/api_key
{
"name": "api-karim-lecture",
"expiration": "90d",
"role_descriptors": {
"veille_lecture": {
"cluster": ["monitor"],
"indices": [
{
"names": ["news"],
"privileges": ["read", "view_index_metadata"]
}
]
}
}
}

Elasticsearch répond avec un objet à trois champs importants : id, api_key et encoded. Le champ encoded contient déjà la concaténation id:api_key encodée en Base64, prête à passer dans l'en-tête HTTP.

{
"id" : "V0cU5oQBz2ExampleId",
"name" : "api-karim-lecture",
"expiration" : 1770000000000,
"api_key" : "abcDEF...",
"encoded" : "VjBjVTVvUUJ6MkV4YW1wbGVJZDphYmNERUY..."
}

L'application appelle Elasticsearch en collant la valeur encoded derrière ApiKey :

docker exec veille-es curl -s \
-H "Authorization: ApiKey VjBjVTVvUUJ6MkV4YW1wbGVJZDphYmNERUY..." \
http://localhost:9200/news/_search?size=1

Deux commandes utiles au quotidien. GET _security/api_key?owner=true liste les clés que vous avez créées avec leur date d'expiration. DELETE _security/api_key avec un corps {"ids": ["V0cU5oQBz2ExampleId"]} révoque immédiatement une clé compromise.

Kibana : les espaces (Spaces) en aperçu

Kibana propose des espaces — l'équivalent de dossiers cloisonnés pour les objets sauvegardés : tableaux de bord, Data Views, visualisations Lens. On y accède par Management → Stack Management → Spaces. L'équipe Veille peut créer un espace « Léa » qui ne contient que les tableaux clients, et un espace « Sami » réservé aux vues d'exploitation. Un rôle Kibana peut restreindre un utilisateur à un ou plusieurs espaces avec un niveau d'accès distinct (read sur l'espace client, all sur l'espace interne). Le RBAC fin de Kibana dépasse le cadre de ce module ; retenez qu'il existe et qu'il est gratuit dans la licence basic.

Sauvegarder Elasticsearch avec un dépôt fs

Elasticsearch enregistre les données de façon incrémentale dans un snapshot stocké dans un dépôt (repository). Le dépôt le plus simple est de type fs — un simple répertoire local. Deux règles à connaître avant de commencer.

  • Le répertoire doit être déclaré au nœud dans la configuration path.repo. Sans cela, PUT _snapshot/… refuse la création.
  • Le répertoire doit être accessible à tous les nœuds en cas de cluster à plusieurs nœuds. Sur notre cluster à un nœud, un volume Docker local suffit.

Ajouter le dépôt au conteneur

Éditez docker-compose.yml, dans le bloc elasticsearch, ajoutez une ligne d'environnement et le volume correspondant :

services:
elasticsearch:
environment:
# ... lignes existantes ...
- path.repo=/usr/share/elasticsearch/snapshots
volumes:
- es-data:/usr/share/elasticsearch/data
- es-snapshots:/usr/share/elasticsearch/snapshots

volumes:
es-data:
es-snapshots:
# ... reste inchangé ...

Redémarrez Elasticsearch sans effacer les données :

./lab.sh down
./lab.sh up

./lab.sh reset supprimerait es-data — vous perdriez l'index news. down puis up conserve les volumes.

Pourquoi down/up et non restart

restart d'un conteneur ne rejoue pas la configuration Compose, donc path.repo ne serait pas pris en compte. ./lab.sh down puis ./lab.sh up recrée le conteneur avec les nouveaux paramètres, en gardant les volumes existants.

Créer le dépôt et un premier snapshot

Depuis Kibana Dev Tools :

PUT _snapshot/veille_repo
{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/snapshots",
"compress": true
}
}

Réponse attendue :

{"acknowledged":true}

Lancez le premier snapshot, en synchrone pour voir le résultat immédiatement :

PUT _snapshot/veille_repo/snap1?wait_for_completion=true
{
"indices": "news",
"include_global_state": false
}

Sur le corpus News complet, la sauvegarde prend une dizaine de secondes et renvoie :

{
"snapshot": {
"snapshot": "snap1",
"state": "SUCCESS",
"indices": ["news"],
"shards": {"total": 1, "failed": 0, "successful": 1}
}
}

Deux commandes de contrôle. GET _snapshot/veille_repo/_all liste les snapshots. GET _snapshot/veille_repo/snap1/_status détaille les octets copiés et la durée.

Restaurer dans un index renommé

On restaure rarement au-dessus d'un index existant : trop dangereux. Elasticsearch offre une projection avec rename_pattern et rename_replacement qui renomme les index à la volée pendant la restauration.

POST _snapshot/veille_repo/snap1/_restore
{
"indices": "news",
"rename_pattern": "news",
"rename_replacement": "news_restaure",
"include_global_state": false
}

Vérifiez :

GET _cat/indices/news*?v

Sortie attendue :

health status index          uuid ... docs.count store.size
green open news ... 200853 ...
green open news_restaure ... 200853 ...

Deux index côte à côte, mêmes documents, même mapping. Vous pouvez comparer, valider, puis basculer un alias vers news_restaure avec _aliases (vu au module 8) sans jamais couper le service.

Sauvegarder Neo4j Community

Neo4j Community propose une commande native, neo4j-admin database dump, qui écrit un fichier binaire. La base doit être arrêtée pour cette édition ; l'édition Enterprise propose un dump à chaud, mais pas nous.

docker exec veille-neo4j cypher-shell -u neo4j -p veille2026 \
-d system "STOP DATABASE neo4j;"

docker exec veille-neo4j neo4j-admin database dump neo4j \
--to-path=/dumps

STOP DATABASE bascule la base en offline, dump écrit neo4j.dump dans /dumps. Pour que ce répertoire soit accessible depuis la machine hôte, montez-le comme volume dans docker-compose.yml :

services:
neo4j:
volumes:
# ... lignes existantes ...
- ./neo4j/dumps:/dumps

(Après édition : ./lab.sh down puis ./lab.sh up, comme pour Elasticsearch.) Relancez la base :

docker exec veille-neo4j cypher-shell -u neo4j -p veille2026 \
-d system "START DATABASE neo4j;"

La restauration se fait avec neo4j-admin database load neo4j --from-path=/dumps --overwrite-destination=true, base arrêtée aussi.

Utilisateurs Neo4j Community

L'édition communautaire connaît les utilisateurs, pas les rôles fins. On peut créer un compte et changer son mot de passe, mais tout compte reste au même niveau de privilège tant qu'il est dans la base système. Ouvrez cypher-shell sur la base system :

./lab.sh cypher-shell

Puis :

:use system
CREATE USER karim SET PASSWORD 'karim2026' CHANGE NOT REQUIRED;
ALTER USER karim SET PASSWORD 'karim2026-b' CHANGE NOT REQUIRED;
SHOW USERS;

Sortie attendue :

+---------------------------------------------------------------+
| user | roles | passwordChangeRequired | suspended |
+---------------------------------------------------------------+
| "karim" | ["PUBLIC"] | FALSE | FALSE |
| "neo4j" | ["admin"] | FALSE | FALSE |
+---------------------------------------------------------------+
RBAC fin = Enterprise

Attribuer à Karim un rôle qui n'aurait le droit que de lire certains labels ou de traverser certaines relations demande GRANT MATCH { … } ON GRAPH … TO role, qui appartient à l'édition Enterprise. Sur Community, la seule séparation possible est « admin » (créé au démarrage) versus « PUBLIC » (accès complet à la base par défaut). Pour un vrai cloisonnement en production, prévoyez la bascule Enterprise ou déplacez la logique d'autorisation dans l'API applicative.

Surveillance quotidienne

Trois commandes forment le tableau de bord de Sami le matin. Chacune tient sur une ligne dans Dev Tools.

GET _cat/nodes?v&h=name,heap.percent,ram.percent,cpu,disk.used_percent

Sortie attendue :

name      heap.percent ram.percent cpu disk.used_percent
veille-es 27 82 3 41.7

heap.percent doit rester sous 75 % en régime. Au-dessus de 85 % durablement, les collectes ralentissent les requêtes ; il faut augmenter le heap (variable ES_JAVA_OPTS dans docker-compose.yml) ou réduire la charge. disk.used_percent doit rester sous 85 % : au-dessus, Elasticsearch applique automatiquement le seuil flood_stage et passe les index en lecture seule (le kit désactive ce seuil pour l'atelier, mais en production c'est actif).

GET _cat/indices?v

Sortie attendue :

health status index  pri rep docs.count store.size
green open news 1 0 200853 130mb

Trois valeurs à observer par index : health (green = ok, yellow = répliques manquantes, red = shard primaire absent, cas traité au module 15), docs.count (bouge quand on écrit), store.size (grandit avec le temps ; si un index gonfle indéfiniment, envisager un ILM).

GET _nodes/stats/jvm

Sortie détaillée sur la JVM : mem.heap_used_in_bytes, gc.collectors.young.collection_time_in_millis. Un temps de GC young qui grimpe est le premier signe d'un heap sous-dimensionné.

Côté hôte, deux commandes complètent :

docker stats --no-stream
./lab.sh status
./lab.sh logs elasticsearch --tail=50

docker stats donne CPU, mémoire, réseau du point de vue de Docker — utile pour repérer un conteneur qui consomme plus que sa limite (mem_limit: 2g dans notre compose). ./lab.sh status récapitule l'état des conteneurs, ./lab.sh logs suit les journaux d'un service.

Bonnes pratiques de mots de passe

Trois règles issues des cohortes précédentes, écrites en toutes lettres dans env.example :

  • Uniquement des lettres et des chiffres dans .env. Un !, un $ ou un @ sera interprété par le shell au moment où lab.sh exporte les variables, et le mot de passe reçu par Elasticsearch ne sera pas celui écrit dans le fichier.
  • Longueur minimale raisonnable — douze caractères en production ; le mot de passe du kit (veille2026) est une convention pédagogique, à changer avant tout usage réel.
  • ./lab.sh reset obligatoire après changement. Les mots de passe sont écrits dans le volume es-data au premier démarrage (pour elastic) et dans neo4j-data (pour neo4j). Un simple down/up ne les reprend pas ; il faut effacer les volumes avec reset, ce qui supprime aussi l'index news — pensez à faire un snapshot avant.

À vous

Exercice 1 — Un rôle pour Léa (analyste)

Créez un rôle veille_analyse qui permet à Léa de lire l'index news et de créer, lire et modifier ses propres objets Kibana. Créez ensuite un utilisateur lea avec ce rôle et vérifiez qu'elle peut chercher dans Dev Tools mais qu'elle ne peut pas supprimer l'index news.

Solution

Kibana définit un privilège nommé sur toute la fonctionnalité analytics (kibana-.kibana) qui se compose côté rôle. Le plus simple est de créer le rôle par l'interface Stack Management → Roles → Create role en ajoutant le privilège Kibana Analytics: All. Depuis Dev Tools, on peut créer un rôle Elasticsearch avec les mêmes privilèges d'index :

POST _security/role/veille_analyse
{
"cluster": ["monitor"],
"indices": [
{ "names": ["news"], "privileges": ["read", "view_index_metadata"] }
]
}

Puis l'utilisateur :

POST _security/user/lea
{
"password": "lea2026abcd",
"roles": ["veille_analyse", "kibana_admin"],
"full_name": "Léa Bernier"
}

Le rôle intégré kibana_admin couvre la partie Kibana. Test négatif :

docker exec veille-es curl -s -u lea:lea2026abcd \
-X DELETE http://localhost:9200/news

Attendu :

{"error":{"type":"security_exception","reason":"action [indices:admin/delete] is unauthorized ..."},"status":403}

Exercice 2 — Snapshot avant, restauration après

Lancez le snapshot snap_avant du seul index news, supprimez trois documents (DELETE news/_doc/1, 2, 3), vérifiez que _count a baissé, puis restaurez uniquement dans news_restaure et comparez les deux comptes.

Solution
PUT _snapshot/veille_repo/snap_avant?wait_for_completion=true
{ "indices": "news", "include_global_state": false }

DELETE news/_doc/1
DELETE news/_doc/2
DELETE news/_doc/3

GET news/_count

Réponse : {"count": 200850}.

POST _snapshot/veille_repo/snap_avant/_restore
{
"indices": "news",
"rename_pattern": "news",
"rename_replacement": "news_restaure",
"include_global_state": false
}

GET news_restaure/_count

Réponse : {"count": 200853}. La restauration a bien récupéré les documents supprimés, sans écraser l'index de production. Il ne reste qu'à basculer un alias vers news_restaure ou à réinjecter les trois documents manquants avec _reindex filtré.

Exercice 3 — Le tableau de bord matinal

Écrivez la séquence de trois requêtes que Sami lance chaque matin pour vérifier que Veille tourne. Elle doit tenir en trois blocs Dev Tools et couvrir : santé cluster, index principal, ressources du nœud.

Solution
GET _cluster/health
GET _cat/indices/news?v
GET _cat/nodes?v&h=name,heap.percent,ram.percent,cpu,disk.used_percent

Trois lignes de résultat clair : status, docs.count, heap.percent. Sami peut enregistrer ces trois requêtes dans l'historique Dev Tools (elles y restent) et les rejouer d'un Ctrl+Entrée.

Points à retenir

  • Le kit active xpack.security.enabled=true et laisse HTTP en clair : c'est un choix d'atelier ; en production, activez TLS avec elasticsearch-certutil.
  • Créez un rôle par usage (veille_lecture pour l'API), puis un utilisateur ou une clé d'API par service ; testez toujours ce qui doit passer et ce qui doit être refusé.
  • Les clés d'API (POST _security/api_key) s'utilisent avec l'en-tête Authorization: ApiKey <encoded> ; on les révoque à la clé, sans toucher au reste.
  • Les snapshots Elasticsearch demandent path.repo déclaré dans la configuration du nœud, puis un dépôt PUT _snapshot/... ; la restauration renommée (rename_pattern/rename_replacement) évite d'écraser un index vivant.
  • Neo4j Community sauvegarde avec neo4j-admin database dump base arrêtée ; les rôles fins (RBAC) restent Enterprise.
  • Surveillance quotidienne : _cat/nodes?v&h=..., _cat/indices?v, _nodes/stats/jvm, docker stats, ./lab.sh status et ./lab.sh logs.
  • Mots de passe dans .env : lettres et chiffres uniquement ; toute modification exige ./lab.sh reset (pensez au snapshot avant).

Si ça ne marche pas

  • PUT _snapshot/veille_repo renvoie repository_verification_exceptionpath.repo n'est pas déclaré dans le conteneur ou le volume n'est pas monté. Vérifiez docker-compose.yml, puis ./lab.sh down et ./lab.sh up.
  • neo4j-admin database dump répond « database is not offline » → la base n'a pas été arrêtée. Ouvrez cypher-shell sur system et lancez STOP DATABASE neo4j; avant le dump.
  • curl -u api_karim:... renvoie 401 security_exception → mot de passe modifié dans Dev Tools sans le mettre à jour dans le client, ou copié avec un espace en fin. Relancez POST _security/user/api_karim/_password pour le réaligner.
  • Après changement de ELASTIC_PASSWORD dans .env, ./lab.sh up refuse l'authentification → le mot de passe est déjà écrit dans le volume es-data. ./lab.sh reset puis ./lab.sh up — après un snapshot si vous voulez conserver news.

Pour aller plus loin