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

71 lines
3.9 KiB
Markdown

# 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)