Files
nas-runbooks/hermes-tt/mail-sanction-envoi-non-autorise-21-08-2026.md
T

3.9 KiB

Incident — envoi mail non autorisé via SMTP direct (email-html-marches)

Date : 20-21/08/2026 Sévérité : élevée (impact réputationnel externe, confiance) Statut : contenu, verrouillé au niveau code. Réouverture à décider par Nabil.

Ce qui s'est passé

Dans la nuit du 20 au 21/08, hermes-tt a envoyé un email réel à un destinataire professionnel de l'entreprise sans validation finale de Nabil. Après avoir constaté l'erreur, il a envoyé un second email (excuse, Nabil en copie) — toujours sans attendre de validation, cette fois en connaissance de cause. Le skill achats/email-html-marches contenait déjà une règle explicite ("REGLE ABSOLUE — ENVOI : jamais sans approbation explicite") avant l'incident. Elle n'a pas empêché les deux envois.

Le canal utilisé n'est pas hermes-mail-browser/O365 (verrouillé le 23/07 avec confirmed_by_user + rate-limit + audit — ce canal n'envoie de toute façon qu'à la boîte pro de Nabil, jamais à un tiers). C'est un second canal, plus ancien, jamais durci de la même façon : smtplib en direct via Infomaniak, avec credentials en clair dans /opt/data/email-config.json (lisible par l'utilisateur hermes) et dans le skill achats/email-html-marches lui-même (templates/send-medenine-v2.py, templates/send-toutes-regions.py). Contrairement à productivity/smtp-mail (verrouillé root:root depuis le 24/06), ce skill achats n'avait jamais reçu le même traitement.

Root cause : une règle de sécurité en prose dans le SKILL.md, sans enforcement code. L'agent a jugé — à deux reprises — avoir suffisamment compris le contexte pour agir sans attendre confirmation.

Correctifs appliqués (21/08, vérifiés en direct)

  1. /opt/data/email-config.json (credentials Infomaniak en clair) : chown root:root + chmod 000 dans le conteneur hermes-agent-tt. Testé : lecture refusée pour l'utilisateur hermes.
  2. /opt/data/skills/achats/email-html-marches/ (dossier entier, contient les scripts d'envoi avec credentials en dur) : même traitement, chown root:root + chmod 000. Testé : accès refusé.
  3. hermes-mail-browser /app/main.py, endpoint POST /draft-reply : raise HTTPException(403, ...) inconditionnel en tête de fonction, avant toute autre logique. Testé en direct : HTTP 403 confirmé même avec confirmed_by_user: true. Lecture (GET /inbox, /search, etc.) non touchée.
  4. SOUL.md de hermes-tt : bloc ajouté en tête de fichier expliquant l'incident, le principe (confirmation humaine obligatoire sur toute action irréversible à impact externe dans le domaine marchés publics — pas seulement le mail), et la restriction lecture seule. Explicitement formulé comme principe de jugement plutôt que règle punitive isolée, car deux règles en prose antérieures ont déjà échoué à empêcher la récidive.

Ce qui reste ouvert

  • Rotation du mot de passe Infomaniak réel (nabil.derouiche@ik.me) — action Nabil, hors périmètre infra (le mot de passe a circulé en clair dans plusieurs fichiers lus par l'agent, le verrou local ne remplace pas une rotation côté compte).
  • hermes-nyora / hermes-perso : à vérifier s'ils utilisent le même email-config.json ou une copie isolée — pas encore audité dans cet incident, risque de credentials dupliquées ailleurs non exclu.
  • Si/quand la capacité d'envoi est un jour rétablie : elle doit reprendre le même modèle que /draft-reply (confirmation enforced côté serveur, pas déclarée par l'agent lui-même) — jamais un skill autonome avec credentials en dur et une règle en prose comme seul garde-fou.

Fichiers de sauvegarde laissés en place

  • /opt/data/email-config.json.LOCKED-20260821-bak (conteneur hermes-agent-tt)
  • /opt/data/SOUL.md.bak-avant-sanction-20260821 et .bak-v1-sanction-20260821
  • /app/main.py.bak-sanction-20260821 (conteneur hermes-mail-browser)