From 78c35055776935492820f7b0cf97bcf5063fa2e2 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 12 Oct 2026 11:36:41 -0400 Subject: [PATCH] courriel : la sonde boites cherche un vrai compte, et l'IMAP n'ecoute plus en clair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La sonde demandait `doveadm user`, que le userdb static (static_allow_all_users, requis par la remise LMTP) satisfait pour n'importe quel nom : elle disait « daniel » resolu chez deux locataires ou ce compte n'existe pas. Elle interroge desormais le passdb (`doveadm auth lookup`), sans mot de passe, et cherche le compte d'amorcage (amorcage_acces_courriel) plutot qu'un nom ecrit. Le 143 (IMAP en clair) ecoutait par defaut a cote du 993 : eteint par `inet_listener imap { port = 0 }`, eprouve sur banc Debian 13. Deploye chez les deux locataires : 24 et 993 seulement ; sonde verte sur sysadmin, rouge sur un compte inexistant. test_dovecot_sonde_et_imap.py garde les deux ; 95 preuves. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 22 +++++++ Makefile | 1 + roles/serveur_dovecot/README.md | 5 ++ roles/serveur_dovecot/defaults/main.yml | 14 ++-- .../templates/99-setops.conf.j2 | 10 +++ .../templates/sonde-boites.sh.j2 | 29 ++++++--- scripts/tests/test_dovecot_sonde_et_imap.py | 65 +++++++++++++++++++ 7 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 scripts/tests/test_dovecot_sonde_et_imap.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c901a85..be4fd30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # CHANGELOG — Set-OPS +## 2026-10-12 (141) — La sonde « boites » cherche un vrai compte, et l'IMAP n'écoute plus en clair + +**95 preuves.** `make verifier` passe ; `test_dovecot_sonde_et_imap.py` garde les deux corrections +(témoin : il échoue sur cinq points contre le code précédent). Déployé chez les deux locataires +après simulation. + +- **La sonde « boites » était verte quoi qu'il arrive.** Elle demandait `doveadm user`, que le + userdb `static` (`static_allow_all_users`, requis par la remise LMTP) satisfait pour n'importe + quel nom : elle disait « daniel » résolu chez deux locataires où ce compte n'existe pas. Elle + interroge désormais le **passdb** (`doveadm auth lookup`) : Dovecot cherche le compte dans + l'annuaire, sous son compte de liaison, en LDAPS, sans tentative de mot de passe (rien ne compte + pour le verrouillage). Mesuré : 0 s'il existe, 67 s'il n'existe pas. +- **Le compte cherché est le compte d'amorçage** (`amorcage_acces_courriel`, son identifiant de + connexion), le seul que Set-OPS sait exister — plus un nom écrit dans le rôle. +- **Le 143 ne s'ouvre plus.** L'IMAP en clair écoutait par défaut à côté du 993 ; le pare-feu le + bloquait et `ssl = required` refusait toute connexion avant STARTTLS, mais le port répondait. + `inet_listener imap { port = 0 }`, éprouvé sur banc Debian 13 avant d'écrire le gabarit. + +Mesuré sur `infra-mail-01`, chez Chezlepro et chez Technolibre : n'écoutent plus que le 24 (LMTP, +TLS) et le 993 ; la sonde est verte sur `sysadmin@.ca` et **rouge** (rc 2) sur un +compte inexistant ; l'IMAPS répond au poste en TLS 1.3, certificat vérifié. + ## 2026-10-12 (140) — Les mots de passe de soumission ne quittent plus la machine de Postfix **95 preuves.** `make verifier` passe ; `test_sasl_local.py` garde le changement (témoin : il diff --git a/Makefile b/Makefile index 5f2b90c..322251b 100644 --- a/Makefile +++ b/Makefile @@ -339,6 +339,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_conditions_site.py python3 scripts/tests/test_relais_du_site.py python3 scripts/tests/test_sasl_local.py + python3 scripts/tests/test_dovecot_sonde_et_imap.py python3 scripts/tests/test_sonde_tcp.py python3 scripts/tests/test_restauration.py python3 scripts/tests/test_repertoires_partages.py diff --git a/roles/serveur_dovecot/README.md b/roles/serveur_dovecot/README.md index c3b636d..97896ef 100644 --- a/roles/serveur_dovecot/README.md +++ b/roles/serveur_dovecot/README.md @@ -56,3 +56,8 @@ bout-en-bout en mono-domaine. de passe y passaient en clair. `serveur_postfix` appelle `tasks/auth_locale.yml`, qui pose sur sa machine un Dovecot d'**authentification seule** (cœur + LDAP, aucun port), joint par un socket local et vérifiant sur l'annuaire en LDAPS. +- **IMAP en TLS direct seulement** (2026-10-12) : l'écoute en clair 143, ouverte par défaut, + est éteinte (`inet_listener imap { port = 0 }`) ; seul le 993 répond. +- **Sonde « boites »** : `doveadm auth lookup` du compte d'amorçage (`amorcage_acces_courriel`), + c'est-à-dire une recherche dans le **passdb** LDAP, sans mot de passe. `doveadm user` ne + prouvait rien : le userdb `static` (`static_allow_all_users`) répond pour n'importe quel nom. diff --git a/roles/serveur_dovecot/defaults/main.yml b/roles/serveur_dovecot/defaults/main.yml index 791ab6f..c162a34 100644 --- a/roles/serveur_dovecot/defaults/main.yml +++ b/roles/serveur_dovecot/defaults/main.yml @@ -55,7 +55,13 @@ serveur_dovecot_auth_locale_paquets: serveur_dovecot_lmtp_tls: true # --- Sonde de supervision ----------------------------------------------------------- -# UN COMPTE QUI DOIT EXISTER. Le resoudre prouve la chaine entiere : demon, configuration, -# bind LDAP, mappage userdb. Variable, donc mise en defaut avec un nom inexistant sans -# toucher a l'annuaire. -serveur_dovecot_sonde_utilisateur: "daniel" +# UN COMPTE QUI DOIT EXISTER, sous son IDENTIFIANT DE CONNEXION (l'attribut `mail` : c'est +# ce que cherche le filtre de connexion). Le trouver prouve la chaine : demon, configuration, +# liaison LDAPS, filtre. Variable, donc mise en defaut avec un nom inexistant sans toucher a +# l'annuaire. +# +# Le compte d'amorcage, pas un nom ecrit (2026-10-12) : ce defaut valait `daniel`, qui +# n'existe chez aucun locataire — et la sonde, qui interrogeait le userdb statique, le disait +# trouve. Le compte d'amorcage est le seul que Set-OPS sait exister ; son adresse est un +# intrant obligatoire de toute instance. +serveur_dovecot_sonde_utilisateur: "{{ amorcage_acces_courriel | default('') }}" diff --git a/roles/serveur_dovecot/templates/99-setops.conf.j2 b/roles/serveur_dovecot/templates/99-setops.conf.j2 index 9059d78..8d9180b 100644 --- a/roles/serveur_dovecot/templates/99-setops.conf.j2 +++ b/roles/serveur_dovecot/templates/99-setops.conf.j2 @@ -23,6 +23,16 @@ ssl_server_cert_file = {{ serveur_dovecot_tls_dir }}/cert.pem ssl_server_key_file = {{ serveur_dovecot_tls_dir }}/key.pem ssl_min_protocol = TLSv1.2 +{# LE 143 NE S'OUVRE PLUS (2026-10-12). L'IMAP en clair ecoutait par defaut a cote du 993 : + le pare-feu le bloquait et `ssl = required` refusait toute connexion avant STARTTLS, mais + le port repondait. Directive : tout transit chiffre — on n'offre que le TLS direct (993, + RFC 8314). `port = 0` eteint l'ecoute ; eprouve sur banc Debian 13 (le 993 reste). #} +service imap-login { + inet_listener imap { + port = 0 + } +} + # --- Auth / annuaire LDAP (serveur_openldap) --- auth_mechanisms = plain login ldap_uris = {{ serveur_dovecot_ldap_uris }} diff --git a/roles/serveur_dovecot/templates/sonde-boites.sh.j2 b/roles/serveur_dovecot/templates/sonde-boites.sh.j2 index 88e2012..ec85395 100644 --- a/roles/serveur_dovecot/templates/sonde-boites.sh.j2 +++ b/roles/serveur_dovecot/templates/sonde-boites.sh.j2 @@ -1,22 +1,33 @@ #!/bin/bash # GENERE par Set-OPS (role serveur_dovecot). Ne pas editer a la main. # -# SONDE « boites » — Dovecot repond et resout un utilisateur dans l'annuaire +# SONDE « boites » — Dovecot repond et trouve un compte dans l'annuaire # # Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. # Mise en defaut PAR PARAMETRE : `serveur_dovecot_sonde_utilisateur` (compte inexistant). set -uo pipefail -UTILISATEUR={{ serveur_dovecot_sonde_utilisateur }} +UTILISATEUR={{ serveur_dovecot_sonde_utilisateur | quote }} +[[ -n "${UTILISATEUR}" ]] || { echo "Aucun compte a chercher (serveur_dovecot_sonde_utilisateur vide)."; exit 3; } systemctl is-active --quiet dovecot || { echo "Dovecot n'est pas actif."; exit 2; } -# ON RESOUT UN UTILISATEUR REEL, PAS UN PING DE SERVICE. C'est le bind LDAP qui tombe le -# plus souvent, et il ne se voit que quand on demande un compte : le demon reste debout, -# le port reste ouvert, et toute authentification echoue. -sortie=$(doveadm user "${UTILISATEUR}" 2>&1) -if ! printf '%s' "${sortie}" | grep -qE '^uid[[:space:]]'; then - echo "Dovecot ne resout pas « ${UTILISATEUR} » — l'annuaire ne repond plus : ${sortie:0:70}" +# LE PASSDB, PAS LE USERDB (2026-10-12). La sonde demandait `doveadm user`, et le userdb est +# `static` avec `static_allow_all_users` (requis par la remise LMTP) : il repond pour +# N'IMPORTE QUEL nom. Elle disait « daniel » resolu chez deux locataires ou ce compte +# n'existe pas — verte quoi qu'il arrive. +# +# `doveadm auth lookup` interroge le passdb : Dovecot cherche le compte dans l'annuaire, +# sous SON compte de liaison, en LDAPS, avec le filtre de connexion. Aucune tentative de +# mot de passe — rien ne compte pour le verrouillage du compte. Mesure : 0 s'il existe, +# 67 s'il n'existe pas, une erreur si l'annuaire ne repond plus. +sortie=$(doveadm auth lookup "${UTILISATEUR}" 2>&1) +rc=$? +if [[ ${rc} -eq 67 ]]; then + echo "Dovecot ne trouve pas « ${UTILISATEUR} » dans l'annuaire : plus personne de ce nom ne peut se connecter." + exit 2 +elif [[ ${rc} -ne 0 ]] || ! printf '%s' "${sortie}" | grep -q '^passdb: '; then + echo "Dovecot n'interroge plus l'annuaire (rc=${rc}) : ${sortie:0:70}" exit 2 fi -echo "Dovecot : « ${UTILISATEUR} » resolu dans l'annuaire, service actif." +echo "Dovecot : « ${UTILISATEUR} » trouve dans l'annuaire (passdb LDAP), service actif." diff --git a/scripts/tests/test_dovecot_sonde_et_imap.py b/scripts/tests/test_dovecot_sonde_et_imap.py new file mode 100644 index 0000000..b1b27f1 --- /dev/null +++ b/scripts/tests/test_dovecot_sonde_et_imap.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""La sonde « boites » cherche un vrai compte, et Dovecot n'ouvre plus l'IMAP en clair. + +POURQUOI (2026-10-12). + 1. La sonde demandait `doveadm user`, que le userdb `static` (`static_allow_all_users`, + requis par la remise LMTP) satisfait pour N'IMPORTE QUEL nom. Elle disait « daniel » + resolu chez deux locataires ou ce compte n'existe pas. + 2. Le 143 (IMAP en clair) ecoutait par defaut a cote du 993. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import jinja2 +import yaml + +RACINE = Path(__file__).resolve().parents[2] +DOVECOT = RACINE / "roles/serveur_dovecot" + + +def main() -> int: + echecs: list[str] = [] + + # 1. La sonde interroge le passdb, pas le userdb statique. + sonde = (DOVECOT / "templates/sonde-boites.sh.j2").read_text(encoding="utf-8") + code = "\n".join(l for l in sonde.splitlines() if not l.lstrip().startswith("#")) + if "doveadm auth lookup" not in code: + echecs.append("la sonde « boites » n'interroge pas le passdb (`doveadm auth lookup`)") + if re.search(r"doveadm\s+user\b", code): + echecs.append("la sonde « boites » interroge encore le userdb statique (`doveadm user`)") + if "67" not in code: + echecs.append("la sonde « boites » ne distingue pas le compte absent (rc 67)") + + # 1b. Le compte cherche se derive du compte d'amorcage, il n'est pas ecrit. + defauts = yaml.safe_load((DOVECOT / "defaults/main.yml").read_text(encoding="utf-8")) + cible = str(defauts.get("serveur_dovecot_sonde_utilisateur", "")) + if "amorcage_acces_courriel" not in cible: + echecs.append(f"serveur_dovecot_sonde_utilisateur = {cible!r} : un nom ecrit, pas le compte d'amorcage") + + # 2. Le 99-setops.conf rendu eteint l'ecoute IMAP en clair (port 0). + rendu = jinja2.Environment().from_string( + (DOVECOT / "templates/99-setops.conf.j2").read_text(encoding="utf-8")).render( + serveur_dovecot_mail_path="/var/vmail/%{user}", serveur_dovecot_vmail_uid=5000, + serveur_dovecot_vmail_gid=5000, serveur_dovecot_tls_dir="/etc/dovecot/tls", + serveur_dovecot_ldap_uris="ldaps://idm-01.exemple.internal:636", + serveur_dovecot_ldap_base="dc=exemple", serveur_dovecot_ldap_bind_dn="cn=dovecot", + serveur_dovecot_ldap_bind_password="banc", serveur_dovecot_ldap_filter="(mail=%{user})", + serveur_dovecot_vmail_utilisateur="vmail", serveur_dovecot_lmtp_reseau=True, + serveur_dovecot_lmtp_port=24, serveur_dovecot_lmtp_tls=True, + serveur_dovecot_postfix_present=False, serveur_dovecot_postfix_socket_dir="/x") + bloc = re.search(r"service imap-login \{\s*inet_listener imap \{\s*port = (\d+)", rendu) + if not bloc or bloc.group(1) != "0": + echecs.append("99-setops.conf n'eteint pas l'ecoute IMAP en clair (`inet_listener imap { port = 0 }`)") + + for e in echecs: + print(f"ECHEC : {e}") + if not echecs: + print("OK : la sonde « boites » cherche le compte d'amorcage dans le passdb ; l'IMAP n'ecoute qu'en TLS (993).") + return 1 if echecs else 0 + + +if __name__ == "__main__": + raise SystemExit(main())