courriel : routage local par identifiant, et la livraison interne remise en marche
Arbitrage rendu : le courrier local est route par uid, plus par l'attribut
mail. Dovecot le faisait DEJA (mail_home = /var/vmail/%{user | username}) ;
les deux moities ne s'accordaient que par coincidence, tant que mail valait
uid@<domaine_interne>. Cout assume : l'adresse interne est derivee et ne se
choisit plus ; en echange mail redevient libre de porter la vraie adresse de
la personne.
Puis la preuve de bout en bout a revele bien pire : toute livraison interne
etait DIFFEREE.
SSL_connect error to infra-mail-01:24: Connection timed out
status=deferred (Cannot start TLS: handshake failure)
Postfix etait durci (lmtp_tls_security_level = verify), le port LMTP de
Dovecot ecoutait en clair — son ssl = required global ne concerne que les
services de connexion. Les deux cotes d'un meme flux avaient ete traites
separement. Corrige et accorde : ssl = yes sur l'inet_listener (TLS implicite)
et lmtp_tls_wrappermode = yes cote client ; l'un sans l'autre ne marche pas.
Livraison prouvee : status=sent (250 ... Saved), message dans
/var/vmail/sysadmin/Maildir/.INBOX/new/.
Le devis disait CONFORME pendant ce temps : il verifiait la resolution et les
dialectes, jamais si le courrier BOUGE. Un devis qui ne regarde que les
reglages ne dit pas si le service rend son service. Il releve desormais la
file d'attente et ses raisons ; test negatif : la panne est nommee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4bfcf4944f
commit
a90dff7ba7
8 changed files with 126 additions and 11 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -1,5 +1,47 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-08 — La livraison interne était en panne, et le devis disait CONFORME
|
||||
|
||||
Suite de l'arbitrage sur l'adressage : **le courrier local est désormais routé par
|
||||
identifiant**, plus par l'attribut `mail`.
|
||||
|
||||
L'argument décisif n'est pas théorique — Dovecot le fait déjà :
|
||||
`mail_home = /var/vmail/%{user | username}`, la partie locale, jamais `mail`. Les deux
|
||||
moitiés n'utilisaient donc pas le même mécanisme et ne s'accordaient que par coïncidence,
|
||||
tant que `mail` valait `uid@<domaine_interne>`. Aligner Postfix ne crée pas un modèle
|
||||
nouveau : ça met fin à une incohérence. Coût assumé : l'adresse interne est dérivée de
|
||||
l'identifiant et ne se choisit plus ; un alias voulu passera par `virtual_alias_maps`, non
|
||||
câblé aujourd'hui. En échange, `mail` redevient libre de porter la vraie adresse de la
|
||||
personne — celle que Keycloak affiche et que « mot de passe oublié » utilise.
|
||||
|
||||
**Puis la preuve de bout en bout a révélé bien pire.** Un vrai courriel envoyé à
|
||||
`sysadmin@chezlepro.internal` n'arrivait pas :
|
||||
|
||||
```
|
||||
SSL_connect error to infra-mail-01[10.27.19.31]:24: Connection timed out
|
||||
status=deferred (Cannot start TLS: handshake failure)
|
||||
```
|
||||
|
||||
Postfix était durci (`lmtp_tls_security_level = verify`), le port LMTP de Dovecot écoutait
|
||||
en clair — son `ssl = required` global ne concerne que les services de connexion. **Les
|
||||
deux côtés d'un même flux avaient été traités séparément**, et toute livraison interne
|
||||
était différée depuis, sans qu'aucun écran ne le montre. Corrigé des deux bouts, et
|
||||
accordé : `ssl = yes` sur l'`inet_listener` (TLS implicite, ce que sait faire un listener)
|
||||
et `lmtp_tls_wrappermode = yes` côté client. L'un sans l'autre ne marche pas.
|
||||
|
||||
Livraison prouvée : `status=sent (250 2.0.0 … Saved)`, message présent dans
|
||||
`/var/vmail/sysadmin/Maildir/.INBOX/new/`.
|
||||
|
||||
**Le devis, lui, annonçait CONFORME.** Il vérifiait la résolution LDAP et les dialectes
|
||||
SMTP/IMAP — tout était correct — et jamais si le courrier *bouge*. C'est la leçon la plus
|
||||
chère de la série : **un devis qui ne regarde que les réglages ne dit pas si le service
|
||||
rend son service.** Il relève désormais la file d'attente et ses raisons de blocage ; le
|
||||
test négatif confirme qu'il aurait nommé cette panne.
|
||||
|
||||
Au passage, deux fois où ma sonde était fausse et non le système : `/dev/tcp` sous `sh`
|
||||
(qui ne le connaît pas), et `Maildir/new/` alors que l'INBOX est `Maildir/.INBOX/new/`.
|
||||
Vérifier l'instrument avant d'accuser le composant, encore.
|
||||
|
||||
## 2026-08-08 — Devis PostgreSQL et courriel : la série est complète
|
||||
|
||||
**PostgreSQL.** Il rend visibles deux défauts déjà vécus ici : un réseau *écrit* dans
|
||||
|
|
|
|||
|
|
@ -111,8 +111,20 @@ vérité : `postmap -q` pour la résolution LDAP de Postfix, `doveadm user` pour
|
|||
Dovecot — précisément le maillon où la livraison avait bloqué — et une vraie conversation
|
||||
SMTP/IMAP pour ce que les serveurs annoncent.
|
||||
|
||||
Il vérifie aussi qu'une adresse **inexistante** ne résout pas : sans ce contrôle, une boîte
|
||||
fourre-tout accepterait n'importe quel nom, et le devis ne mesurerait plus rien.
|
||||
Il vérifie aussi qu'une adresse **inexistante** ne résout pas. Ce n'est pas un luxe :
|
||||
Dovecot a `allow_all_users = yes` et accepte donc n'importe quelle partie locale — le
|
||||
garde-fou est entièrement chez Postfix.
|
||||
|
||||
**Et il regarde la file d'attente.** C'est la leçon la plus chère de la série : sa première
|
||||
version disait `CONFORME` pendant que *toute* livraison interne était différée. Résolution
|
||||
correcte, dialectes corrects, et rien qui arrive — Postfix exigeait TLS
|
||||
(`lmtp_tls_security_level = verify`) vers un port Dovecot qui écoutait en clair. Les deux
|
||||
côtés d'un même flux avaient été durcis séparément. **Un devis qui ne regarde que les
|
||||
réglages ne dit pas si le service rend son service** ; la file est le seul endroit qui
|
||||
révèle que le courrier ne bouge pas.
|
||||
|
||||
Le routage local se fait par **identifiant** (`uid`), pas par l'attribut `mail` — voir le
|
||||
gabarit `ldap-mailboxes.cf.j2` pour la raison et le coût assumé.
|
||||
|
||||
## Un piège de construction, à connaître avant d'écrire le prochain
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@
|
|||
echo "FANTOME=$(postmap -q 'nexistepas-{{ 999 | random }}@{{ domaine_interne }}' \
|
||||
ldap:/etc/postfix/ldap-mailboxes.cf 2>/dev/null)"
|
||||
echo "MYNETWORKS=$(postconf -h mynetworks)"
|
||||
# La file d'attente est le seul endroit qui dit si le courrier BOUGE. Sans ce
|
||||
# releve, le devis a annonce CONFORME pendant que toute livraison interne
|
||||
# etait differee sur un echec TLS — resolution correcte, dialectes corrects,
|
||||
# et rien qui arrive. Constate le 2026-08-08.
|
||||
echo "DIFFERES=$(mailq 2>/dev/null | grep -cE '^[0-9A-F]{8,}' || echo 0)"
|
||||
echo "RAISONS<<"
|
||||
mailq 2>/dev/null | grep -E '^\(' | head -3
|
||||
echo ">>"
|
||||
for P in 25 587; do
|
||||
echo "PORT=$P"
|
||||
exec 3<>/dev/tcp/127.0.0.1/$P || continue
|
||||
|
|
|
|||
|
|
@ -45,3 +45,8 @@ serveur_dovecot_lmtp_port: 24
|
|||
# par pare-feu ; le trafic d'auth devrait être isolé/chiffré en prod.
|
||||
serveur_dovecot_sasl_reseau: false
|
||||
serveur_dovecot_sasl_port: 12345
|
||||
|
||||
# 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
|
||||
# clair, soit elle est differee sans fin.
|
||||
serveur_dovecot_lmtp_tls: true
|
||||
|
|
|
|||
|
|
@ -51,6 +51,20 @@ userdb static {
|
|||
service lmtp {
|
||||
inet_listener lmtp {
|
||||
port = {{ serveur_dovecot_lmtp_port }}
|
||||
{% if serveur_dovecot_lmtp_tls %}
|
||||
# TLS IMPLICITE sur ce port. Le `ssl = required` global ne concerne que les
|
||||
# services de connexion (IMAP/POP) : ce port-ci ecoutait en clair.
|
||||
#
|
||||
# Postfix, lui, etait deja durci (`lmtp_tls_security_level = verify`). Les deux
|
||||
# cotes d'un meme flux avaient donc ete traites separement, et la livraison
|
||||
# interne etait DIFFEREE en silence — « SSL_connect error […] Connection timed
|
||||
# out », 300 s par tentative, sans qu'aucun ecran ne le montre. Constate le
|
||||
# 2026-08-08 par `make courriel-plan`.
|
||||
#
|
||||
# Implicite et non STARTTLS : c'est ce que sait faire un `inet_listener`, et le
|
||||
# client doit donc etre en `lmtp_tls_wrappermode`. Les deux vont ensemble.
|
||||
ssl = yes
|
||||
{% endif %}
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,29 @@
|
|||
# Géré par Set-OPS (rôle serveur_postfix). Ne pas éditer à la main.
|
||||
# Carte LDAP : valide qu'une adresse correspond à une boîte (attribut mail).
|
||||
#
|
||||
# Carte LDAP : valide qu'une adresse locale correspond à un compte. La livraison passe
|
||||
# par LMTP (`virtual_transport`), donc Postfix ne se sert du résultat que comme d'un
|
||||
# CONTRÔLE D'EXISTENCE — la valeur rendue est ignorée.
|
||||
#
|
||||
# ROUTAGE PAR IDENTIFIANT, et non par l'attribut `mail`. Dovecot le fait déjà
|
||||
# (`mail_home = /var/vmail/%{user | username}` : la partie locale, jamais `mail`), et les
|
||||
# deux moitiés ne s'accordaient que par coïncidence — tant que `mail` valait
|
||||
# `uid@{{ '{{' }} domaine_interne {{ '}}' }}`. Le jour où l'adresse de notification d'une
|
||||
# personne est devenue son adresse publique, la livraison interne a cessé sans qu'aucune
|
||||
# erreur n'apparaisse nulle part. Constaté le 2026-08-08 par `make courriel-plan`.
|
||||
#
|
||||
# Conséquence assumée : l'adresse interne est DÉRIVÉE de l'identifiant et ne se choisit
|
||||
# pas. Un alias voulu passerait par `virtual_alias_maps`, qui n'est pas câblé sur
|
||||
# l'annuaire aujourd'hui. En échange, `mail` redevient libre de porter la vraie adresse de
|
||||
# la personne — celle que Keycloak affiche et que « mot de passe oublié » utilise.
|
||||
#
|
||||
# `domain` limite la carte au domaine local : sans lui, Postfix interrogerait l'annuaire
|
||||
# pour n'importe quel destinataire, et un `uid` qui existe suffirait à capter une adresse
|
||||
# d'un autre domaine.
|
||||
server_host = {{ serveur_postfix_ldap_serveur }}
|
||||
search_base = {{ serveur_postfix_ldap_base }}
|
||||
query_filter = (mail=%s)
|
||||
result_attribute = mail
|
||||
domain = {{ serveur_postfix_domaine }}
|
||||
query_filter = (&(objectClass=inetOrgPerson)(uid=%u))
|
||||
result_attribute = uid
|
||||
bind = yes
|
||||
bind_dn = {{ serveur_postfix_ldap_bind_dn }}
|
||||
bind_pw = {{ serveur_postfix_ldap_bind_password }}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ smtpd_tls_protocols = >=TLSv1.2, !SSLv2, !SSLv3
|
|||
smtp_tls_security_level = may
|
||||
{% if serveur_postfix_lmtp_tls_verify | default(false) %}
|
||||
# Zero-confiance : livraison LMTP vers Dovecot chiffree ET verifiee (cert step-ca).
|
||||
# `wrappermode` = TLS des la connexion : c'est ce que sait offrir un `inet_listener`
|
||||
# Dovecot. Sans lui, Postfix tentait STARTTLS sur un port qui ne le propose pas, et
|
||||
# la poignee de main expirait au bout de 300 s — livraison differee en silence.
|
||||
lmtp_tls_wrappermode = yes
|
||||
lmtp_tls_security_level = verify
|
||||
lmtp_tls_CAfile = {{ serveur_postfix_lmtp_tls_cafile }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -46,12 +46,22 @@ def analyser(releves: list[dict]) -> list[str]:
|
|||
if not _valeur(brut, "TROUVE"):
|
||||
ecarts.append(
|
||||
f"{h} : Postfix ne resout PAS « {sonde} » — rien ne peut lui etre livre "
|
||||
f"localement. L'attribut `mail` de l'annuaire porte une autre adresse, et "
|
||||
f"c'est lui qui sert de cle (`query_filter = (mail=%s)`)")
|
||||
f"localement. Le routage local se fait par IDENTIFIANT : verifier qu'un "
|
||||
f"compte porte bien cet `uid` dans l'annuaire, et que la carte est "
|
||||
f"limitee au domaine local (`domain =`)")
|
||||
if _valeur(brut, "FANTOME"):
|
||||
ecarts.append(f"{h} : une adresse inexistante resout quand meme — la boite est "
|
||||
f"un fourre-tout, n'importe quel nom serait accepte")
|
||||
|
||||
# Un courrier qui ne bouge pas est le seul symptome que ni la resolution ni
|
||||
# les dialectes ne revelent.
|
||||
differes = _valeur(brut, "DIFFERES")
|
||||
if differes.isdigit() and int(differes) > 0:
|
||||
raisons = re.search(r"RAISONS<<(.*?)>>", brut, re.S)
|
||||
detail = " ".join((raisons.group(1) if raisons else "").split())[:140]
|
||||
ecarts.append(f"{h} : {differes} message(s) en attente dans la file — "
|
||||
f"le courrier ne part pas. {detail}")
|
||||
|
||||
for port in ("25", "587"):
|
||||
d = _dialecte(brut, port)
|
||||
if not d.strip():
|
||||
|
|
@ -92,10 +102,10 @@ def analyser(releves: list[dict]) -> list[str]:
|
|||
if postfix and dovecot:
|
||||
if not _valeur(postfix["brut"], "TROUVE") and "mail_path" in dovecot["brut"]:
|
||||
ecarts.append(
|
||||
"les deux moities divergent : Dovecot connait la boite, Postfix ne sait "
|
||||
"pas y router. Une identite ne porte qu'une adresse `mail`, et elle a ici "
|
||||
"deux roles incompatibles — la notification (joignable par la personne) et "
|
||||
"la cle de routage local (dans un domaine local). A trancher.")
|
||||
"les deux moities divergent : Dovecot accepte la boite, Postfix ne sait "
|
||||
"pas y router. Dovecot a `allow_all_users = yes` et accepte donc "
|
||||
"N'IMPORTE QUELLE partie locale — le garde-fou est entierement chez "
|
||||
"Postfix, et il vient de sauter.")
|
||||
return ecarts
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue