diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d3042..c901a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # CHANGELOG — Set-OPS +## 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 +échoue sur huit points contre le code précédent). Simulé puis **déployé chez les deux locataires**. + +La mesure des écoutes l'a montré chez les deux locataires : Postfix (`edge-mta-01`) faisait +vérifier chaque mot de passe de soumission (587, 465) par le Dovecot de `infra-mail-01`, sur +`:12345`. Le client SASL Dovecot de Postfix ne chiffre pas : **chaque mot de passe traversait le +réseau en clair**, et le registre déclarait ce flux `tls`. + +On ne chiffre pas ce flux, **on le supprime** : + +- **Un Dovecot d'authentification seule à côté de Postfix** (`serveur_dovecot/tasks/auth_locale.yml`), + posé par `serveur_postfix` là où la soumission existe. Cœur + pilote LDAP, aucun paquet de + protocole : ni IMAP, ni LMTP, **aucun port**. Postfix lui parle par `private/auth`. +- **Il vérifie sur l'annuaire en LDAPS**, sous le compte de Dovecot, par le chemin que Postfix + emprunte déjà sur cette machine. Aucun flux nouveau : le mot de passe ne voyage plus que dans TLS. +- **Le port 12345 disparaît** : écoute de `infra-mail-01`, flux des deux rôles, règle nftables. + `serveur_dovecot_sasl_reseau` est retiré ; une instance qui le pose encore est arrêtée par une + garde qui dit pourquoi. Retiré des group_vars de Chezlepro, Technolibre et du lab. + +Éprouvé sur un banc Debian 13 jetable avant d'écrire le rôle : seuls `dovecot`, `auth`, `anvil`, +`log`, `config` tournent ; Postfix rend 235 pour le bon mot de passe et 535 pour un faux ; sans +TLS, il n'offre pas l'authentification. + +Mesuré chez Technolibre après déploiement : Dovecot démarre « without any protocols », n'écoute +aucun port TCP, et son processus `auth` tient une connexion LDAPS établie vers `idm-01` ; le 587 +rend 535 pour un compte inexistant (« auth failed », et non une panne de l'annuaire) ; `infra-mail-01` +n'écoute plus le 12345, sa règle est partie, et `edge-mta-01` s'y fait refuser. + +**Prouvé par un envoi réel chez chacun** (Technolibre 11:19, Chezlepro 11:24) : depuis le poste, +soumission sur 465 en TLS 1.3, certificats vérifiés contre l'autorité du locataire ; +`sasl_username=sysadmin@.ca` au journal de Postfix ; remise LMTP `status=sent` ; message +relu par IMAP dans la boîte. + +La sonde « boites » n'est **pas** une preuve : elle disait « daniel » résolu chez Technolibre, où ce +compte n'existe pas. `static_allow_all_users` fait répondre Dovecot pour n'importe quel nom. À corriger. + +Chez Chezlepro, mêmes mesures après déploiement (Dovecot sans protocole ni port, LDAPS établi vers +`idm-01`, 535 pour un compte inexistant, 12345 refusé, LMTP 24 joignable, file vide). Le pare-feu y a +aussi reçu les commentaires nftables du 2026-10-09 (9a01b17), jamais déployés là ; une fois les +commentaires ignorés, la seule règle qui a changé est le 12345. +Les tenants recevront aussi les réglages TLS de 139 (`main.cf`) : niveaux inchangés (`may`), plus +la journalisation TLS et le fichier d'autorités. + ## 2026-10-12 (139) — Le courriel de la forge du site est chiffré de bout en bout, et le relais vérifie qui il joint **95 preuves.** `make verifier` passe ; `test_relais_du_site.py` vérifie aussi les réglages TLS. diff --git a/Makefile b/Makefile index ab79897..5f2b90c 100644 --- a/Makefile +++ b/Makefile @@ -338,6 +338,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_libelles_parefeu.py 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_sonde_tcp.py python3 scripts/tests/test_restauration.py python3 scripts/tests/test_repertoires_partages.py diff --git a/docs/flux-conception.md b/docs/flux-conception.md index 792afb8..19ed128 100644 --- a/docs/flux-conception.md +++ b/docs/flux-conception.md @@ -144,7 +144,7 @@ Le registre confondait deux situations : | Situation | Exemple | `partage` | |---|---|---| -| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 12345 (SASL réseau) | absent | +| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 24 (LMTP réseau) | absent | | le rôle **décrit** celle d'un autre | `serveur_backup` emprunte le sshd de `serveur_debian` | `true` | Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur le diff --git a/docs/registre-flux.md b/docs/registre-flux.md index ebe8bf8..721c69d 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -43,7 +43,6 @@ | `serveur_dns_public` | egress | 5300 | udp | primaires_dns_locataires | clair | SOA demande au primaire avant chaque transfert : c'est lui qui dit si la zone a change. Sans lui, le secondaire garde sa premiere copie pour toujours. | | `serveur_dovecot` | ingress | 24 | tcp | serveur_postfix | tls-requis | Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify). | | `serveur_dovecot` | ingress | 993 | tcp | externe | tls-requis | Accès courriel des utilisateurs (IMAPS). Frontière publique gérée à l'OPNsense. | -| `serveur_dovecot` | ingress | 12345 | tcp | serveur_postfix | tls | Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot. | | `serveur_dovecot` | egress | 636 | tcp | serveur_openldap | tls-requis | userdb/passdb : Dovecot résout et authentifie les comptes sur l'annuaire (LDAPS). | | `serveur_forge_site` | ingress | 22 | tcp | runners_locataires | ssh | Chaque runner de locataire lit son propre depot du genome, prive, avec sa cle de deploiement en lecture seule. Il ne lit ni le depot d'un autre locataire, ni celui du site. | | `serveur_forge_site` | ingress | 443 | tcp | voisins_site | tls-requis | Servir le génome aux écosystèmes de ce site : c'est de cette forge qu'ils clonent leur moteur, leurs modèles et la carte de la fabric (D-81). Sans ce flux, un écosystème neuf ne peut pas se reproduire. | @@ -98,9 +97,8 @@ | `serveur_postfix` | ingress | 587 | tcp | flotte, externe | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. | | `serveur_postfix` | egress | 24 | tcp | serveur_dovecot | tls-requis | Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié. | | `serveur_postfix` | egress | 25 | tcp | externe | starttls | Relais sortant vers les MX distants (STARTTLS opportuniste). | -| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS). | +| `serveur_postfix` | egress | 636 | tcp | serveur_openldap | tls-requis | Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire, et mots de passe de soumission vérifiés par le Dovecot local (LDAPS). | | `serveur_postfix` | egress | 11332 | tcp | localhost | clair | Filtre milter rspamd co-localisé (antispam + signature DKIM). | -| `serveur_postfix` | egress | 12345 | tcp | serveur_dovecot | tls | Validation SASL des identifiants de soumission contre Dovecot. | | `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). | | `serveur_postgresql` | ingress | 9187 | tcp | serveur_prometheus | clair | Metriques PostgreSQL lues par l'observatoire. Series, pas verdicts : ce qui derive lentement — cache qui decroche, connexions qui montent, bases qui grossissent — n'a que le graphe pour se faire voir. En clair pour l'instant, contrairement a `client_metrique` : dette inscrite, a fermer par `--web.config.file` + `client_pki`. | | `serveur_powerdns` | ingress | 5300 | tcp | dns_public_site | clair | AXFR signe TSIG par le serveur DNS public du site, vers l'instance publique uniquement. | @@ -136,6 +134,6 @@ - **n-a** : 8 flux - **ssh** : 9 flux - **starttls** : 6 flux -- **tls** : 10 flux +- **tls** : 8 flux - **tls-cible** : 2 flux - **tls-requis** : 36 flux diff --git a/roles/client_journal/defaults/main.yml b/roles/client_journal/defaults/main.yml index 276f19e..424e63b 100644 --- a/roles/client_journal/defaults/main.yml +++ b/roles/client_journal/defaults/main.yml @@ -20,8 +20,9 @@ client_journal_loki_tls: false client_journal_loki_ca: "/etc/step/certs/root_ca.crt" # Port de l'interface HTTP locale d'Alloy (diagnostic). DECLARE ici parce que son defaut -# amont — 12345 — entre en collision avec `serveur_dovecot_sasl_port`, qui vaut 12345 lui -# aussi et qui est un choix DELIBERE de Set-OPS (SASL reseau, pour la soumission :587). +# amont — 12345 — entrait en collision avec `serveur_dovecot_sasl_port`, qui valait 12345 +# lui aussi (SASL reseau, pour la soumission :587 ; retire le 2026-10-12 — il passait en +# clair). Le port reste declare : le defaut amont, lui, n'a pas change. # # Sur `infra-mail-01`, les deux cohabitent : le premier demarre gagne, l'autre boucle sur # « bind: address already in use ». Constate le 2026-08-08 lors de la premiere diff --git a/roles/serveur_dovecot/README.md b/roles/serveur_dovecot/README.md index ea38ea7..c3b636d 100644 --- a/roles/serveur_dovecot/README.md +++ b/roles/serveur_dovecot/README.md @@ -34,16 +34,15 @@ bout-en-bout en mono-domaine. | `serveur_dovecot_ldap_filter` | `(&(objectClass=inetOrgPerson)(mail=%{user}))` | Login = adresse courriel | | `serveur_dovecot_tls_actif` | `true` | Pont de certificat step_ca | | `serveur_dovecot_lmtp_reseau` | `true` | LMTP :24 pour un MTA **distant** (edge-mta) | -| `serveur_dovecot_sasl_reseau` | `false` | Auth SASL réseau :12345 pour la soumission :587 | ## Flux (`meta/flux.yml`) `993/tcp` IMAPS (utilisateurs) · `24/tcp` LMTP ← Postfix (TLS vérifié) · -`12345/tcp` SASL ← Postfix · `636/tcp` → OpenLDAP (userdb/passdb). +`636/tcp` → OpenLDAP (userdb/passdb). ## Notes / limites - **Mono-domaine** : le chemin ne conserve pas le domaine. Le multi-domaine demandera de le préserver des deux côtés (raffinement Étape B). -- LMTP et SASL **réseau** doivent être restreints au nœud MTA par pare-feu — c'est ce que +- LMTP **réseau** doit être restreint au nœud MTA par pare-feu — c'est ce que fait `nftables_baseline` à partir de `meta/flux.yml` (`pair: serveur_postfix`). - Si Postfix est **co-localisé**, le rôle bascule sur les sockets sous le chroot (`/var/spool/postfix/private`) plutôt que sur le réseau (détection automatique). @@ -52,3 +51,8 @@ bout-en-bout en mono-domaine. ## Prérequis - `serveur_dovecot requiert serveur_openldap actif` (`docs/dependances-groupes.yml`). - `client_pki` appliqué sur le nœud (couche `pki_client`, avant la couche `services`). +- **Authentification de la soumission : locale à Postfix** (2026-10-12). Le port SASL + réseau `12345` est retiré : le client SASL Dovecot de Postfix ne chiffre pas, et les mots + 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. diff --git a/roles/serveur_dovecot/defaults/main.yml b/roles/serveur_dovecot/defaults/main.yml index 186c3ba..791ab6f 100644 --- a/roles/serveur_dovecot/defaults/main.yml +++ b/roles/serveur_dovecot/defaults/main.yml @@ -40,11 +40,14 @@ serveur_dovecot_postfix_socket_dir: "/var/spool/postfix/private" serveur_dovecot_lmtp_reseau: true serveur_dovecot_lmtp_port: 24 -# --- Auth SASL réseau : pour la soumission :587 servie par un Postfix DISTANT (edge-mta). -# Dovecot vérifie le mot de passe (passdb LDAP prouvé) pour Postfix. À restreindre au MTA -# par pare-feu ; le trafic d'auth devrait être isolé/chiffré en prod. -serveur_dovecot_sasl_reseau: false -serveur_dovecot_sasl_port: 12345 +# --- Authentification de la soumission : LOCALE A POSTFIX, plus jamais par le reseau --- +# Jusqu'au 2026-10-12, Postfix (edge-mta) interrogeait ce Dovecot sur :12345 ; le client +# SASL Dovecot de Postfix ne chiffre pas, et les mots de passe passaient en clair. Ce port +# n'existe plus : `serveur_postfix` pose a cote de lui un Dovecot d'authentification seule +# (`tasks/auth_locale.yml`), qui verifie sur l'annuaire en LDAPS. +serveur_dovecot_auth_locale_paquets: + - dovecot-core + - dovecot-ldap # TLS sur le port LMTP. Doit rester ACCORDE avec `serveur_postfix_lmtp_tls_verify` du # cote emetteur : l'un sans l'autre, la livraison interne s'arrete — soit elle part en diff --git a/roles/serveur_dovecot/meta/flux.yml b/roles/serveur_dovecot/meta/flux.yml index ee59b84..84cca81 100644 --- a/roles/serveur_dovecot/meta/flux.yml +++ b/roles/serveur_dovecot/meta/flux.yml @@ -7,12 +7,6 @@ flux: pair: serveur_postfix chiffrement: tls-requis raison: "Remise LMTP depuis Postfix (edge-mta -> mailstore), en TLS vérifié (lmtp_tls_security_level=verify)." - - sens: ingress - port: 12345 - protocole: tcp - pair: serveur_postfix - chiffrement: tls - raison: "Authentification SASL déléguée : Postfix valide les identifiants de soumission contre Dovecot." - sens: ingress port: 993 protocole: tcp diff --git a/roles/serveur_dovecot/tasks/auth_locale.yml b/roles/serveur_dovecot/tasks/auth_locale.yml new file mode 100644 index 0000000..2b2b4e4 --- /dev/null +++ b/roles/serveur_dovecot/tasks/auth_locale.yml @@ -0,0 +1,77 @@ +--- +# AUTHENTIFICATION LOCALE POUR POSTFIX (2026-10-12) — voir templates/auth-locale.conf.j2. +# +# Appelee par `serveur_postfix` la ou la soumission existe, APRES que Postfix a demarre : +# le socket vit dans `/var/spool/postfix/private`, que Postfix cree a son premier demarrage. +# Ni IMAP, ni LMTP, ni aucun port : la machine n'entre pas dans le groupe `serveur_dovecot`, +# et aucun de ses flux ne s'ouvre ici. +- name: Authentification locale — resoudre l'annuaire (role partage) + ansible.builtin.include_role: + name: resoudre_annuaire + vars: + # Le compte de Dovecot, comme sur la machine des boites : meme verification, meme droit. + resoudre_annuaire_bind_cn: "dovecot" + resoudre_annuaire_secret: "vault_ldap_bind_dovecot" + +- name: Authentification locale — adopter la connexion annuaire + ansible.builtin.set_fact: + serveur_dovecot_ldap_uris: "{{ resoudre_annuaire_uri }}:{{ resoudre_annuaire_port }}" + serveur_dovecot_ldap_base: "{{ resoudre_annuaire_base_dn }}" + serveur_dovecot_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}" + serveur_dovecot_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}" + no_log: true + +- name: Authentification locale — exiger le mot de passe de liaison (Vault) + ansible.builtin.assert: + that: + - serveur_dovecot_ldap_bind_password | length > 0 + fail_msg: "vault_ldap_bind_dovecot requis : l'authentification locale se lie a l'annuaire sous le compte de Dovecot." + +# Le coeur et le pilote LDAP, RIEN d'autre : sans `dovecot-imapd` ni `dovecot-lmtpd`, aucun +# protocole n'est declare et Dovecot ne lance aucun service de connexion. +- name: Authentification locale — installer Dovecot (coeur + LDAP) + ansible.builtin.apt: + name: "{{ serveur_dovecot_auth_locale_paquets }}" + state: present + update_cache: true + register: serveur_dovecot_auth_locale_apt + +- name: Authentification locale — assurer le repertoire du socket (sous le chroot Postfix) + ansible.builtin.file: + path: "{{ serveur_dovecot_postfix_socket_dir }}" + state: directory + owner: postfix + group: root + mode: "0700" + +- name: Authentification locale — deployer la configuration (drop-in Dovecot 2.4) + ansible.builtin.template: + src: auth-locale.conf.j2 + dest: /etc/dovecot/conf.d/99-setops.conf + owner: root + group: root + mode: "0640" + no_log: true # contient le mot de passe de liaison LDAP + notify: Redémarrer dovecot + +- name: Authentification locale — desactiver l'auth systeme (LDAP est l'unique autorite) + ansible.builtin.replace: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^!include auth-system\.conf\.ext' + replace: '#!include auth-system.conf.ext' + notify: Redémarrer dovecot + # En simulation, le paquet qu'on vient d'« installer » n'a pose aucun fichier. + when: not (ansible_check_mode and serveur_dovecot_auth_locale_apt is changed) + +- name: Authentification locale — valider la configuration (doveconf) + ansible.builtin.command: doveconf -n + changed_when: false + when: not ansible_check_mode + +- name: Authentification locale — activer et demarrer Dovecot + when: not ansible_check_mode + ansible.builtin.systemd: + name: "{{ serveur_dovecot_service }}" + enabled: true + state: started + daemon_reload: true diff --git a/roles/serveur_dovecot/tasks/main.yml b/roles/serveur_dovecot/tasks/main.yml index ddcd6c6..28b4877 100644 --- a/roles/serveur_dovecot/tasks/main.yml +++ b/roles/serveur_dovecot/tasks/main.yml @@ -15,6 +15,18 @@ serveur_dovecot_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}" no_log: true +# UNE INSTANCE QUI DEMANDE ENCORE L'ANCIEN PORT (2026-10-12). `serveur_dovecot_sasl_reseau` +# ouvrait :12345, ou Postfix envoyait les mots de passe de soumission EN CLAIR. Le gabarit +# ne le lit plus ; une instance qui le pose encore croirait l'avoir — on le dit. +- name: Refuser l'authentification reseau retiree (serveur_dovecot_sasl_reseau) + ansible.builtin.assert: + that: + - not (serveur_dovecot_sasl_reseau | default(false) | bool) + fail_msg: >- + `serveur_dovecot_sasl_reseau` est retire (2026-10-12) : il faisait passer les mots de + passe de soumission en clair. Postfix verifie desormais par un Dovecot local + (serveur_dovecot/tasks/auth_locale.yml). Retire la ligne des group_vars de l'instance. + - name: Exiger le mot de passe de bind LDAP (Vault) ansible.builtin.assert: that: diff --git a/roles/serveur_dovecot/templates/99-setops.conf.j2 b/roles/serveur_dovecot/templates/99-setops.conf.j2 index d982964..9059d78 100644 --- a/roles/serveur_dovecot/templates/99-setops.conf.j2 +++ b/roles/serveur_dovecot/templates/99-setops.conf.j2 @@ -69,16 +69,6 @@ service lmtp { } {% endif %} -{% if serveur_dovecot_sasl_reseau %} -# --- Auth SASL réseau (soumission :587 depuis le MTA distant edge-mta) --- -# À restreindre au nœud MTA par pare-feu. -service auth { - inet_listener sasl { - port = {{ serveur_dovecot_sasl_port }} - } -} -{% endif %} - # --- Sockets pour Postfix (remise LMTP + auth SASL) --- # Seulement si Postfix est co-localisé (l'utilisateur postfix existe). Sinon Dovecot # tourne en autonome (IMAP) ; les sockets seront ajoutés au déploiement de serveur_postfix. diff --git a/roles/serveur_dovecot/templates/auth-locale.conf.j2 b/roles/serveur_dovecot/templates/auth-locale.conf.j2 new file mode 100644 index 0000000..a2e96e4 --- /dev/null +++ b/roles/serveur_dovecot/templates/auth-locale.conf.j2 @@ -0,0 +1,43 @@ +# Géré par Set-OPS (rôle serveur_dovecot, authentification locale). Ne pas éditer à la main. +# Syntaxe Dovecot 2.4. +{# + AUTHENTIFICATION SEULE, A COTE DE POSTFIX (2026-10-12). + + Postfix confiait la verification des mots de passe de soumission a la machine des + boites, par `smtpd_sasl_path = inet::12345`. Le client SASL Dovecot de + Postfix ne sait pas chiffrer : chaque mot de passe traversait le reseau en clair, et le + registre le disait `tls`. Directive du 2026-10-12 : tout transit chiffre. + + On ne chiffre pas ce flux, on le supprime. Ce Dovecot-ci ne sert ni IMAP ni LMTP (aucun + paquet de protocole n'est installe) et n'ecoute AUCUN port : Postfix lui parle par un + socket local, et il verifie le mot de passe sur l'annuaire en LDAPS — le chemin que + Postfix emprunte deja sur cette machine. Le mot de passe ne voyage plus que dans TLS. + + Eprouve sur banc Debian 13 avant d'ecrire le role : processus `dovecot`, `auth`, + `anvil`, `log`, `config` seulement ; 235 avec le bon mot de passe, 535 avec un faux, + AUTH non offert hors TLS. +#} +dovecot_config_version = 2.4.0 + +# Garder l'adresse complète (user@domaine) comme identité — domaines virtuels. +auth_username_format = %{user} +auth_mechanisms = plain login +ldap_uris = {{ serveur_dovecot_ldap_uris }} +ldap_base = {{ serveur_dovecot_ldap_base }} +ldap_auth_dn = {{ serveur_dovecot_ldap_bind_dn }} +ldap_auth_dn_password = {{ serveur_dovecot_ldap_bind_password }} + +{# Le meme passdb que la machine des boites : une identite, un mot de passe, une regle. + Pas de userdb : une verification SASL n'en consulte aucun. #} +passdb ldap { + ldap_filter = {{ serveur_dovecot_ldap_filter }} + ldap_bind = yes +} + +service auth { + unix_listener {{ serveur_dovecot_postfix_socket_dir }}/auth { + user = postfix + group = postfix + mode = 0660 + } +} diff --git a/roles/serveur_postfix/defaults/main.yml b/roles/serveur_postfix/defaults/main.yml index 6a12327..3bf593b 100644 --- a/roles/serveur_postfix/defaults/main.yml +++ b/roles/serveur_postfix/defaults/main.yml @@ -38,12 +38,13 @@ serveur_postfix_mailstore_hote: "" serveur_postfix_lmtp_port: 24 # --- Soumission :587 (clients authentifiés) --- -# SASL délégué à Dovecot (passdb LDAP prouvé) sur le nœud mail-store, via son auth réseau. +# SASL délégué à un Dovecot d'AUTHENTIFICATION SEULE, sur cette machine, par socket local +# (`serveur_dovecot/tasks/auth_locale.yml`) ; il vérifie sur l'annuaire en LDAPS. Jusqu'au +# 2026-10-12, Postfix interrogeait le Dovecot du mailstore sur :12345 — en clair, faute de +# TLS dans ce protocole. # 587 (STARTTLS requis) et 465 (TLS direct) ; seuls les authentifiés relaient. Publics depuis le # 2026-09-29 : les clients de courriel y arrivent par l'adresse publique du locataire. serveur_postfix_submission_actif: false -serveur_postfix_sasl_hote: "{{ serveur_postfix_mailstore_hote }}" -serveur_postfix_sasl_port: 12345 # --- Annuaire LDAP (validation domaines/boîtes/alias) --- # Connexion à l'annuaire (serveur, base, bind_dn, bind_password) : fournie par le rôle partagé diff --git a/roles/serveur_postfix/meta/flux.yml b/roles/serveur_postfix/meta/flux.yml index f208d84..31b5154 100644 --- a/roles/serveur_postfix/meta/flux.yml +++ b/roles/serveur_postfix/meta/flux.yml @@ -36,12 +36,6 @@ flux: pair: serveur_dovecot chiffrement: tls-requis raison: "Remise finale par LMTP au mailstore (Dovecot), en TLS vérifié." - - sens: egress - port: 12345 - protocole: tcp - pair: serveur_dovecot - chiffrement: tls - raison: "Validation SASL des identifiants de soumission contre Dovecot." - sens: egress port: 11332 protocole: tcp @@ -53,7 +47,7 @@ flux: protocole: tcp pair: serveur_openldap chiffrement: tls-requis - raison: "Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire (LDAPS)." + raison: "Tables virtuelles (domaines/alias/boîtes) résolues sur l'annuaire, et mots de passe de soumission vérifiés par le Dovecot local (LDAPS)." - sens: egress port: 25 protocole: tcp diff --git a/roles/serveur_postfix/tasks/main.yml b/roles/serveur_postfix/tasks/main.yml index 2fd91fa..61f502f 100644 --- a/roles/serveur_postfix/tasks/main.yml +++ b/roles/serveur_postfix/tasks/main.yml @@ -192,7 +192,7 @@ # 465 (`submissions`, TLS DIRECT, 2026-09-29) A COTE DU 587 (STARTTLS). La RFC 8314 recommande # le TLS direct pour la soumission : aucun echange en clair avant le chiffrement. Memes # restrictions que le 587 : authentifie, sinon refuse. -- name: Activer la soumission :587 et :465 (clients authentifiés, SASL Dovecot distant) +- name: Activer la soumission :587 et :465 (clients authentifiés, SASL Dovecot local) ansible.builtin.blockinfile: path: /etc/postfix/master.cf marker: "# {mark} SETOPS SUBMISSION 587" @@ -202,7 +202,7 @@ -o smtpd_tls_security_level=encrypt -o smtpd_sasl_auth_enable=yes -o smtpd_sasl_type=dovecot - -o smtpd_sasl_path=inet:{{ serveur_postfix_sasl_hote }}:{{ serveur_postfix_sasl_port }} + -o smtpd_sasl_path=private/auth -o smtpd_sasl_security_options=noanonymous -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_relay_restrictions=permit_sasl_authenticated,reject @@ -211,7 +211,7 @@ -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o smtpd_sasl_type=dovecot - -o smtpd_sasl_path=inet:{{ serveur_postfix_sasl_hote }}:{{ serveur_postfix_sasl_port }} + -o smtpd_sasl_path=private/auth -o smtpd_sasl_security_options=noanonymous -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_relay_restrictions=permit_sasl_authenticated,reject @@ -231,6 +231,17 @@ state: started daemon_reload: true +# LES MOTS DE PASSE NE QUITTENT PLUS LA MACHINE (2026-10-12). `smtpd_sasl_path` visait +# `inet::12345`, que le client SASL Dovecot de Postfix ne sait pas chiffrer : +# chaque soumission y envoyait son mot de passe en clair. Le socket `private/auth` est +# servi par un Dovecot d'authentification seule, pose ici, qui verifie en LDAPS. Apres le +# demarrage de Postfix : c'est lui qui cree `private/` a sa premiere mise en route. +- name: Authentification de la soumission — Dovecot local (aucun port) + ansible.builtin.include_role: + name: serveur_dovecot + tasks_from: auth_locale.yml + when: serveur_postfix_submission_actif | bool + # --- Sonde de supervision (docs/supervision-conception.md) -------------------------- # Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la # fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. diff --git a/scripts/tests/test_sasl_local.py b/scripts/tests/test_sasl_local.py new file mode 100644 index 0000000..b5468e1 --- /dev/null +++ b/scripts/tests/test_sasl_local.py @@ -0,0 +1,106 @@ +#!/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())