Postfix (edge-mta) faisait verifier chaque mot de passe de soumission par le Dovecot du mailstore, sur :12345. Le client SASL Dovecot de Postfix ne chiffre pas : chaque mot de passe traversait le reseau en clair, et le registre disait ce flux tls. Le flux est supprime, pas chiffre. serveur_postfix pose a cote de lui un Dovecot d'authentification seule (serveur_dovecot/tasks/auth_locale.yml) : coeur + LDAP, aucun protocole, aucun port, joint par private/auth, qui verifie sur l'annuaire en LDAPS. Le 12345 disparait des deux roles et du pare-feu ; serveur_dovecot_sasl_reseau est retire, et une garde arrete l'instance qui le pose encore. Deploye chez les deux locataires ; prouve par un envoi reel chez chacun (sasl_username au journal, remise LMTP, relu par IMAP). test_sasl_local.py garde le changement ; 95 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
5.2 KiB
Python
106 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Les mots de passe de soumission ne quittent plus la machine de Postfix.
|
|
|
|
POURQUOI (2026-10-12). Directive : tout transit chiffre, a l'interne comme a l'externe.
|
|
La mesure des ecoutes l'a montre sur les deux locataires : Postfix (edge-mta) faisait
|
|
verifier chaque mot de passe de soumission par le Dovecot du mailstore, sur :12345, et le
|
|
client SASL Dovecot de Postfix ne sait pas chiffrer. Le registre declarait ce flux `tls`.
|
|
|
|
Le flux n'est pas chiffre, il est SUPPRIME : un Dovecot d'authentification seule vit a
|
|
cote de Postfix, joint par socket local, et verifie sur l'annuaire en LDAPS.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import jinja2
|
|
import yaml
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
from resoudre_flux import charger_flux # noqa: E402
|
|
|
|
DOVECOT = RACINE / "roles/serveur_dovecot"
|
|
POSTFIX = RACINE / "roles/serveur_postfix"
|
|
|
|
|
|
def main() -> int:
|
|
echecs: list[str] = []
|
|
|
|
# 1. Plus aucun role ne declare le port SASL reseau, d'un cote ou de l'autre.
|
|
for role, flux in charger_flux().items():
|
|
for f in flux:
|
|
if str(f.get("port")) == "12345":
|
|
echecs.append(f"{role} declare encore le flux {f.get('sens')} 12345")
|
|
|
|
# 2. Postfix lit le socket local, jamais un `inet:`, pour ses deux ports de soumission.
|
|
taches = (POSTFIX / "tasks/main.yml").read_text(encoding="utf-8")
|
|
chemins = re.findall(r"smtpd_sasl_path=(\S+)", taches)
|
|
if len(chemins) != 2 or set(chemins) != {"private/auth"}:
|
|
echecs.append(f"smtpd_sasl_path = {chemins}, attendu private/auth sur 587 et 465")
|
|
|
|
# 3. Le Dovecot local est pose APRES le demarrage de Postfix (qui cree `private/`),
|
|
# et seulement la ou la soumission existe.
|
|
liste = yaml.safe_load(taches)
|
|
noms = [t.get("name", "") for t in liste]
|
|
i_auth = next((i for i, t in enumerate(liste)
|
|
if (t.get("ansible.builtin.include_role") or {}).get("tasks_from") == "auth_locale.yml"), None)
|
|
i_demarrer = next((i for i, n in enumerate(noms) if n == "Activer et démarrer Postfix"), None)
|
|
if i_auth is None:
|
|
echecs.append("serveur_postfix n'inclut pas serveur_dovecot/auth_locale.yml")
|
|
else:
|
|
if i_demarrer is None or i_auth < i_demarrer:
|
|
echecs.append("le Dovecot local est pose avant le demarrage de Postfix")
|
|
if "serveur_postfix_submission_actif" not in str(liste[i_auth].get("when")):
|
|
echecs.append("le Dovecot local n'est pas conditionne a la soumission")
|
|
|
|
# 4. Authentification SEULE : aucun paquet de protocole, aucune ecoute reseau.
|
|
defauts = yaml.safe_load((DOVECOT / "defaults/main.yml").read_text(encoding="utf-8"))
|
|
paquets = defauts.get("serveur_dovecot_auth_locale_paquets") or []
|
|
if set(paquets) != {"dovecot-core", "dovecot-ldap"}:
|
|
echecs.append(f"auth locale installe {paquets}, attendu dovecot-core + dovecot-ldap")
|
|
if "serveur_dovecot_sasl_reseau" in defauts or "serveur_dovecot_sasl_port" in defauts:
|
|
echecs.append("serveur_dovecot porte encore ses variables SASL reseau")
|
|
|
|
fichier = DOVECOT / "templates/auth-locale.conf.j2"
|
|
if not fichier.exists():
|
|
echecs.append("serveur_dovecot n'a pas de gabarit d'authentification seule (auth-locale.conf.j2)")
|
|
else:
|
|
rendu = jinja2.Environment(undefined=jinja2.StrictUndefined).from_string(
|
|
fichier.read_text(encoding="utf-8")).render(
|
|
serveur_dovecot_ldap_uris="ldaps://idm-01.exemple.internal:636",
|
|
serveur_dovecot_ldap_base="dc=exemple,dc=ca",
|
|
serveur_dovecot_ldap_bind_dn="cn=dovecot,ou=services,dc=exemple,dc=ca",
|
|
serveur_dovecot_ldap_bind_password="banc",
|
|
serveur_dovecot_ldap_filter=defauts["serveur_dovecot_ldap_filter"],
|
|
serveur_dovecot_postfix_socket_dir=defauts["serveur_dovecot_postfix_socket_dir"],
|
|
)
|
|
for interdit in ("inet_listener", "protocols", "userdb"):
|
|
if interdit in rendu:
|
|
echecs.append(f"auth-locale.conf rend `{interdit}` : ce n'est plus de l'authentification seule")
|
|
for attendu in ("passdb ldap {", "ldap_bind = yes",
|
|
"unix_listener /var/spool/postfix/private/auth {"):
|
|
if attendu not in rendu:
|
|
echecs.append(f"auth-locale.conf ne rend pas `{attendu}`")
|
|
|
|
# 5. Et la machine des boites n'ouvre plus le port.
|
|
if "inet_listener sasl" in (DOVECOT / "templates/99-setops.conf.j2").read_text(encoding="utf-8"):
|
|
echecs.append("99-setops.conf.j2 ouvre encore l'ecoute SASL reseau")
|
|
|
|
# 6. La verification passe par l'annuaire chiffre : le resolveur partage vise LDAPS.
|
|
annuaire = yaml.safe_load((RACINE / "roles/resoudre_annuaire/defaults/main.yml").read_text(encoding="utf-8"))
|
|
if annuaire.get("resoudre_annuaire_encryption") != "ldaps" or int(annuaire.get("resoudre_annuaire_port", 0)) != 636:
|
|
echecs.append("resoudre_annuaire ne vise plus LDAPS:636")
|
|
|
|
for e in echecs:
|
|
print(f"ECHEC : {e}")
|
|
if not echecs:
|
|
print("OK : la soumission verifie ses mots de passe sur place, l'annuaire en LDAPS ; aucun port SASL reseau.")
|
|
return 1 if echecs else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|