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)
/opt/data/email-config.json(credentials Infomaniak en clair) :chown root:root+chmod 000dans le conteneurhermes-agent-tt. Testé : lecture refusée pour l'utilisateurhermes./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é.hermes-mail-browser/app/main.py, endpointPOST /draft-reply: raiseHTTPException(403, ...)inconditionnel en tête de fonction, avant toute autre logique. Testé en direct :HTTP 403confirmé même avecconfirmed_by_user: true. Lecture (GET /inbox, /search, etc.) non touchée.SOUL.mdde 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.jsonou 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-20260821et.bak-v1-sanction-20260821/app/main.py.bak-sanction-20260821(conteneur hermes-mail-browser)