Installez Postfix depuis votre distribution et acceptez les défauts, et vous avez un serveur de messagerie qui envoie votre courrier en clair à tout serveur qui n’offre pas de TLS, et qui accepte du courrier de quiconque prétend être qui il veut. Le main.cf livré par Debian met les deux niveaux TLS à may. Rien ne vérifie SPF. Rien ne vérifie DKIM. Un domaine expéditeur qui n’existe pas est accepté.
Ce n’est pas un bug. C’est ce que les standards demandaient il y a vingt ans, et c’est ainsi que tournent encore aujourd’hui quantité de serveurs de messagerie sur internet.
Ce billet construit l’autre genre. Il reprend les règles maison du billet d’ouverture de cette série et met chacune d’elles dans un Postfix qui fonctionne, avec la réponse que chacune produit lorsqu’elle refuse quelque chose. L’argumentaire en faveur des règles est dans ce billet-là et je ne le referai pas deux fois. Celui-ci, c’est la construction.
| Règle | Le Postfix de Debian tel que livré | Cette installation | Ce qu’une compromission obtient |
|---|---|---|---|
| Pas de clair en entrée | TLS proposé, jamais exigé | encrypt sur chaque écouteur | 530 5.7.0 Must issue a STARTTLS command first |
| Pas de clair en sortie | TLS si proposé, clair sinon | encrypt, DANE ou MTA-STS là où c’est publié | différé, dsn=4.7.4 ou dsn=4.7.5 |
| IPv6 sur tout | inet_protocols = all | pareil, plus un AAAA sur chaque MX | chaque test ci-dessous a tourné sur les deux familles |
| Relais seulement avec un login | relaie pour mynetworks | le port 25 ne relaie jamais, 587 et 465 exigent un login | 554 5.7.1 … Relay access denied |
| Un login envoie en son propre nom | non vérifié | enveloppe et From: tous deux liés au login | 553 5.7.1 … not owned by user |
| Le domaine expéditeur doit exister | non vérifié | reject_unknown_sender_domain, code 550 | 550 5.1.8 … Domain not found |
| Échec SPF, échec DKIM | non vérifié | Rspamd, avec rejets forcés | 554 5.7.1 SPF hard fail / DKIM signature does not verify |
Chaque réponse de ce billet a été capturée depuis un serveur en marche, en IPv4 et IPv6. Plusieurs réglages se sont révélés ne rien faire tant qu’un autre n’était pas posé lui aussi. J’ai trouvé chacun d’eux en le regardant ne rien faire. Ils sont signalés plus bas, parce que ce sont eux qui vous mordront.
Le labo
| Composant | Version | Rôle |
|---|---|---|
| Postfix | 3.10.13, Debian trixie | le serveur MX et de soumission pour alpha.test |
| Dovecot | 2.4.1 | les mots de passe des logins, tous les protocoles mail désactivés |
| Rspamd, Redis | 3.12.1, 8.0.2 | SPF, DKIM, DMARC, signature DKIM |
| postfix-tlspol | 1.14.4 | la politique sortante depuis TLSA et MTA-STS |
| BIND, unbound | 9.20.30, 1.26.1 | une zone test. privée et signée, et un résolveur qui la valide |
| Domaine | À quoi il sert |
|---|---|
alpha.test | le serveur que construit ce billet |
bravo.test | Stalwart, qui aura son propre billet |
plain.test | le mauvais voisin : zone non signée, pas de STARTTLS |
sender.test | SPF -all, DKIM, DMARC p=reject : pour forger, altérer et réexpédier |
Chaque écouteur s’attache aux deux familles, et chaque MX a un enregistrement A et un AAAA. IPv6 n’est pas une option ici. Un serveur de messagerie sans elle est un défaut, et prouver que celui-ci l’a est simple : lancez chaque vérification deux fois.
Étape par étape
Une session SMTP est une séquence fixe, et chaque règle de cette installation y a une place naturelle. L’ordre importe pour une raison : refuser aussi tôt que la preuve le permet. Un domaine expéditeur qui n’existe pas est connu dès MAIL FROM, donc il n’y a aucune raison de prendre le corps avant de le refuser. Un corps altéré n’est connu qu’après DATA, et c’est donc là qu’il est refusé.
Le reste du billet parcourt ces étapes dans l’ordre : TLS en entrée, TLS en sortie, logins et relais, adresses expéditrices, puis ce que Rspamd fait du corps.
Pas de clair en entrée
Une ligne :
smtpd_tls_security_level = encrypt
Un client qui tente de sauter STARTTLS n’obtient que le refus :
<- 220 mail.alpha.test ESMTP Postfix
-> MAIL FROM:<carol@sender.test>
<** 530 5.7.0 Must issue a STARTTLS command first
Lisez maintenant ce que le propre manuel de Postfix dit de ce réglage :
According to RFC 2487 this MUST NOT be applied in case of a publicly-referenced SMTP server. Instead, this should be used on dedicated servers, for example submission (port 587).1
RFC 2487 est l’ancien numéro de RFC 3207, et cette règle est la raison pour laquelle la plupart des hôtes MX sur internet acceptent encore le courrier en clair. Je l’applique quand même à un MX référencé publiquement. Volontairement. Le billet d’ouverture explique pourquoi je pense que la règle est mauvaise, et le coût est réel : un expéditeur dont le serveur ne sait pas faire de TLS ne peut pas vous joindre. En 2026, c’est un serveur dont vous ne voulez pas de courrier.
Pas de clair en sortie
C’est la moitié que la plupart des guides ratent, parce que le réglage évident ne fait pas ce que son nom laisse croire.
Postfix a un niveau de sécurité dane. Posez-le et vous vous attendriez à ce que le courrier parte authentifié. Ce n’est pas le cas. Voici ce que postconf(5) dit qu’il fait quand l’autre bout ne publie rien :
When DNSSEC-validated TLSA records are not found the effective tls security level is “may”.2
may, c’est du TLS opportuniste, ce que le TLS_README décrit sans détour : « Otherwise, messages are sent in the clear. »3 La majeure partie d’internet ne publie aucun enregistrement TLSA. Donc smtp_tls_security_level = dane tout seul, c’est DANE pour la poignée de domaines qui le font, et du texte en clair pour tous les autres dont le serveur n’offre pas STARTTLS. De l’opportuniste, avec un meilleur nom.
Le correctif consiste à faire de encrypt le plancher et à hisser chaque destination au-dessus quand elle publie quelque chose de mieux. C’est postfix-tlspol qui hisse. C’est un petit serveur de politique, sous licence MIT, à qui Postfix demande pour chaque domaine destinataire via une socketmap. Il cherche d’abord des enregistrements TLSA utilisables dans une zone signée, puis MTA-STS, et répond avec la politique que Postfix doit utiliser :
smtp_dns_support_level = dnssec
smtp_tls_security_level = encrypt
smtp_tls_mandatory_protocols = >=TLSv1.2
smtp_tls_policy_maps = socketmap:inet:[fd89:50::25]:8642:QUERYwithTLSRPT
Son README est précis sur ce qu’il renvoie : il va « Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned. »4 Interrogé sur les trois domaines du labo, il a dit exactement cela :
level=INFO msg="Evaluated policy" origin=network domain=alpha.test policy=dane-only ttl=214
level=INFO msg="Evaluated policy" origin=network domain=bravo.test policy=dane-only ttl=198
level=INFO msg="No policy found" origin=network domain=plain.test ttl=1800
Pas de politique pour plain.test signifie que Postfix retombe sur le plancher encrypt, et plain.test n’offre pas STARTTLS. Le courrier attend :
postfix/smtp[194]: 78598130DA54: to=<someone@plain.test>, relay=mail.plain.test[fd89:50::22]:25,
delay=0.02, delays=0.01/0.01/0.01/0, dsn=4.7.4,
status=deferred (TLS is required, but was not offered by host mail.plain.test[fd89:50::22])
Et le cas qui prouve que DANE fait quelque chose. Au début de la construction, Stalwart sur bravo.test servait encore son propre certificat d’amorçage alors que la zone publiait l’enregistrement TLSA du vrai. Un serveur avec un certificat parfaitement valable, mais pas celui dont le propriétaire du domaine s’est porté garant :
postfix/smtp[196]: server certificate verification failed for mail.bravo.test[fd89:50::21]:25:
num=65:no matching DANE TLSA records
postfix/smtp[196]: 79160130DA56: to=<someone@bravo.test>, relay=mail.bravo.test[10.89.50.21]:25,
delay=5.9, delays=0/0.01/5.9/0, dsn=4.7.5, status=deferred (Server certificate not verified)
Différer est la bonne réponse dans les deux cas. Un message différé est réessayé pendant des jours, ce qui laisse à l’autre côté le temps de corriger chez lui, et rien n’est perdu. Un message envoyé en clair ne peut pas être dé-envoyé. De ce fait, attendre est toujours l’échec le moins coûteux.
Le résolveur fait partie de la chaîne de confiance
smtp_dns_support_level = dnssec fait que Postfix demande à son résolveur des réponses validées, puis croit le bit AD du résolveur, le drapeau qui dit « j’ai vérifié les signatures ». Postfix ne valide rien lui-même. Il fait confiance. Quel que soit le résolveur nommé dans /etc/resolv.conf, c’est lui qui décide désormais si votre courrier est authentifié.
Vous trouverez des conseils affirmant que glibc jette ce bit à moins que resolv.conf ne porte options trust-ad. Je l’ai testé, et sur ce Postfix cela ne change rien : DANE a correspondu avec l’option comme sans. La source explique pourquoi. Postfix pose le drapeau lui-même dès qu’il demande du DNSSEC :
if (DNS_WANT_DNSSEC_VALIDATION(flags))
flags |= (RES_USE_EDNS0 | RES_TRUSTAD);
Cela vient de src/dns/dns_lookup.c.5 Donc ce qui compte, ce n’est pas une option de résolveur. C’est le résolveur que vous visez. Il doit valider, et le chemin jusqu’à lui doit être un chemin de confiance, ce qui en pratique veut dire qu’il tourne sur le serveur de messagerie lui-même ou sur un réseau que personne d’autre ne peut atteindre. Un résolveur public à l’autre bout d’internet posera volontiers le bit AD pour vous. Et quiconque se trouve entre vous et lui aussi.
DANE : publier la clé, pas le certificat
Un enregistrement TLSA épingle ce qu’un serveur expéditeur devrait voir en se connectant. Celui à utiliser est 3 1 1, et chacun des trois nombres est un choix :
| Champ | Valeur | Signification | Pourquoi celui-ci |
|---|---|---|---|
| Usage | 3, DANE-EE | correspond au certificat du serveur lui-même | aucune AC dans la chaîne à valider ou à voir échouer |
| Sélecteur | 1, SPKI | ne hache que la clé publique | survit à chaque renouvellement sur la même clé |
| Correspondance | 1, SHA-256 | une empreinte, pas la clé entière | tout client DANE doit la gérer |
RFC 7672 donne la raison de cette combinaison en une ligne :
this record type works for all clients and need not change across certificate renewals with the same key.6
Le hachage tient en un seul pipeline, et le voici face au certificat du labo et à l’enregistrement que sert la zone signée :
$ openssl x509 -in mail.alpha.test.crt -noout -pubkey \
| openssl pkey -pubin -outform DER | sha256sum
093a80cac907a52eb41d17df20163c95f2e15d6702c76f65174d7fc32460c69f -
$ dig +dnssec _25._tcp.mail.alpha.test TLSA
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
_25._tcp.mail.alpha.test. 165 IN TLSA 3 1 1 093A80CAC907A52EB41D17DF20163C95F2E15D6702C76F65174D7FC3 2460C69F
Le labo a prouvé le « need not change » par accident. À mi-chemin, les contrôles de certificat plus stricts de Python 3.13 ont refusé les certificats du labo parce qu’il leur manquait un authority key identifier, donc chacun d’eux a été réémis. Mêmes clés privées, nouveaux certificats. Pas un seul enregistrement TLSA n’a changé, et pas un seul expéditeur DANE ne l’a remarqué.
Maintenant le piège. La plupart des gens renouvellent leurs certificats avec certbot, automatiquement, toutes les quelques semaines. D’après la propre documentation de certbot :
Not reusing private keys is the default behavior of Certbot.7
Donc le renouvellement par défaut fabrique une nouvelle clé à chaque fois. L’enregistrement TLSA cesse de correspondre, et chaque expéditeur qui vérifie DANE se met à différer votre courrier le jour du renouvellement. Rien ne casse de votre côté. Rien ne vous prévient. Lancez certbot avec --reuse-key et l’enregistrement survit à chaque certificat que vous mettez derrière lui. Quand la clé doit vraiment changer, il y a un ordre à respecter :
L’étape deux est celle que les gens sautent. Le résultat, c’est du courrier différé jusqu’à ce que l’ancien enregistrement sorte de tous les caches, et il vient précisément des expéditeurs qui font la sécurité correctement.
Personne ne relaie sans login
Relayer, c’est faire suivre du courrier vers un domaine dont vous n’êtes pas le destinataire. Un relais ouvert, c’est la façon dont le spam se blanchit à travers la réputation de quelqu’un d’autre. L’ancienne façon d’en fermer un était de faire confiance à une plage d’adresses, si bien que tout ce qui venait du réseau du bureau pouvait relayer. C’est de la confiance par emplacement. Sur un réseau avec du WiFi invité, des utilisateurs VPN et le portable compromis de temps en temps, l’emplacement ne vaut pas grand-chose.
Donc aucune adresse n’est de confiance, sauf la machine elle-même :
mynetworks = 127.0.0.0/8 [::1]/128
smtpd_relay_restrictions = reject_unauth_destination
Le port 25 est pour le courrier qui arrive pour ce domaine et rien d’autre. Il ne relaie jamais, pour personne :
<** 554 5.7.1 <someone@plain.test>: Relay access denied
Les utilisateurs envoient par soumission : le port 587 avec STARTTLS, ou le 465 avec TLS dès le premier octet. Les deux exigent un login avant de faire quoi que ce soit. Dovecot vérifie les mots de passe, avec tous les protocoles mail coupés pour qu’il ne fasse rien d’autre, et Postfix l’interroge par une socket. L’écouteur de soumission dans master.cf :
submission inet n - y - - smtpd
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_auth_only=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_sender_restrictions=reject_sender_login_mismatch
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
smtpd_tls_auth_only signifie que le serveur ne propose même pas AUTH tant que TLS n’est pas en place, si bien qu’un mot de passe ne traverse jamais le fil en clair. Lignes AUTH dans la réponse à un EHLO en clair : zéro. Un client qui se connecte au 587 et tente d’envoyer sans se connecter n’arrive nulle part :
<** 554 5.7.1 <unknown[fd89:50::50]>: Client host rejected: Access denied
Un login possède ses adresses
Un login prouve qui vous êtes. Il ne dit rien des adresses sous lesquelles vous pouvez envoyer. Sans ce lien, n’importe quel utilisateur du serveur peut envoyer au nom du directeur financier, et le courrier sera signé et remis comme s’il était authentique. Postfix lie les deux avec une table d’adresse vers propriétaire :
alice@alpha.test alice@alpha.test
bob@alpha.test bob@alpha.test
postmaster@alpha.test alice@alpha.test
reject_sender_login_mismatch sur les ports de soumission l’impose contre l’expéditeur d’enveloppe. Connecté en tant qu’alice, en essayant d’envoyer en tant que bob :
<** 553 5.7.1 <bob@alpha.test>: Sender address rejected: not owned by user alice@alpha.test
Cela couvre l’enveloppe. Cela ne couvre pas l’en-tête From:, qui est l’adresse que le destinataire voit réellement, et postconf(5) le dit dans ses notes sur cette restriction précise :
to enforce that the From: header address matches the envelope sender (MAIL FROM) address, use an external filter such as a Milter8
Rspamd est déjà le milter ici, donc la vérification de l’en-tête est une courte règle Lua qui compare l’utilisateur connecté à l’adresse From:, plus un rejet forcé sur le symbole qu’elle lève :
rspamd_config.AUTH_FROM_NOT_LOGIN = {
callback = function(task)
local user = task:get_user()
if not user then return false end
local from = task:get_from('mime')
if not from or not from[1] or not from[1].addr then return true, 'no From address' end
if string.lower(from[1].addr) ~= string.lower(user) then
return true, from[1].addr .. ' is not ' .. user
end
return false
end,
score = 0.0,
description = 'Authenticated user sent with a From: address that is not their own',
group = 'headers',
}
Alice envoyant avec MAIL FROM:<alice@alpha.test> et From: bob@alpha.test :
<** 554 5.7.1 You may only send as your own address
Un nom d’affichage passe, et un changement de casse aussi. Alice <ALICE@alpha.test> passe, parce que la comparaison porte sur l’adresse et ignore la casse.
La vérification qui se désactive toute seule
Voici le premier des réglages qui ne font silencieusement rien. La façon évidente d’empêcher des tiers de forger votre propre domaine sur le port 25 est d’y mettre aussi les vérifications login-mismatch. Postfix accepte la configuration, démarre, et journalise ceci :
warning: restriction `reject_authenticated_sender_login_mismatch' ignored: no SASL support
warning: restriction `reject_unauthenticated_sender_login_mismatch' ignored: no SASL support
Le port 25 a SASL coupé, à juste titre, parce que personne ne s’y connecte. La vérification est donc ignorée. Le courrier est accepté. La seule preuve est un avertissement dans un log que personne ne lit, et une fois la vérification partie, n’importe qui sur internet pourrait envoyer MAIL FROM:<alice@alpha.test> à ce serveur et le faire remettre aux collègues d’alice comme si elle l’avait écrit.
Le correctif est de refuser nos propres domaines nommément sur le port 25, parce que du courrier venant de nous n’y arrive jamais légitimement :
smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain,
check_sender_access texthash:/etc/postfix/own_domains
avec own_domains contenant une ligne, alpha.test REJECT Our own domain: send through submission with a login. Testé depuis l’extérieur :
<** 554 5.7.1 <alice@alpha.test>: Sender address rejected: Our own domain: send through submission with a login
Les domaines expéditeurs qui n’existent pas
Si le domaine dans MAIL FROM n’existe pas, rien ne pourra jamais lui être renvoyé, et rien de légitime n’en est jamais venu. reject_unknown_sender_domain refuse le courrier quand ce domaine a « no DNS MX and no DNS A record ».9 Le code de réponse par défaut de Postfix pour cela est 450, un échec temporaire, qui invite le faussaire à réessayer plus tard. Je l’ai changé en 550.
postconf(5) est direct sur ce réglage : « Do not change this unless you have a complete understanding of RFC 5321. »10 Soit. Voici le raisonnement. Un 4xx dit à l’expéditeur de mettre en file et de réessayer, ce qui est juste quand la réponse peut changer. Un domaine sans enregistrement aujourd’hui n’en fera pas pousser dans les cinq prochaines minutes. Postfix sépare cela du cas où la réponse n’est pas encore connue : un échec DNS est géré par unknown_address_tempfail_action, qui diffère encore. Changer le code ne change que le cas qui a une réponse.
Le labo a testé les trois, plus celui qui intéresse la règle IPv6. La formulation de postconf dit « A record », et un domaine dont la seule adresse est un enregistrement AAAA ne devrait pas être traité comme manquant :
| Domaine expéditeur | Ce que dit le DNS | Réponse |
|---|---|---|
nosuch-lab.co.uk | NXDOMAIN | 550 5.1.8 <someone@nosuch-lab.co.uk>: Sender address rejected: Domain not found |
v6only-lab.co.uk | AAAA seul, pas de A, pas de MX | accepté |
dnsfail-lab.co.uk | SERVFAIL | 450 4.1.8 … Sender address rejected: Domain not found, réessayé plus tard |
sender.test | MX, A, AAAA | accepté |
En pratique, « A record » couvre AAAA, et un domaine IPv6 seul est en sécurité. Le refus arrive à MAIL FROM, avant que l’expéditeur ait envoyé un seul octet du corps. Postfix retient par défaut un rejet de MAIL FROM jusqu’au premier RCPT TO, si bien que la réponse arrive une étape plus tard qu’on pourrait s’y attendre, et c’est pourquoi le diagramme des étapes la montre là.
SPF, DKIM et DMARC, avec des dents
Ces trois-là sont traités comme il faut dans le billet d’ouverture. En une ligne chacun : SPF dit quels serveurs peuvent envoyer pour un domaine, DKIM signe le message pour que les modifications se voient, et DMARC dit quoi faire quand ils échouent et s’ils correspondent au domaine From: visible. La règle maison est de rejeter sur un échec dur SPF et sur une signature DKIM présente qui ne se vérifie pas. Le courrier non signé n’est pas un échec DKIM. C’est DMARC p=reject sur le domaine de l’expéditeur qui gère cela.
Rspamd fait les trois en un seul milter, plus la signature DKIM pour le courrier sortant. Postfix lui remet chaque message, et si Rspamd ne répond pas, le courrier attend plutôt que de passer sans contrôle :
smtpd_milters = inet:127.0.0.1:11332
non_smtpd_milters = $smtpd_milters
milter_default_action = tempfail
Rspamd travaille sur des scores, et c’est la première chose à comprendre à son sujet. Par défaut, rien n’est rejeté parce que SPF a échoué. Rien. Chaque résultat ajoute ou retranche quelques points, et le message est rejeté quand le total dépasse 15. C’est le module force_actions qui transforme un résultat isolé en décision :
rules {
SPF_FAIL_REJECT {
action = "reject";
expression = "R_SPF_FAIL";
message = "SPF hard fail: this host is not allowed to send for that domain";
}
DKIM_FAIL_REJECT {
action = "reject";
expression = "R_DKIM_REJECT";
message = "DKIM signature does not verify: the message was changed or forged";
}
DMARC_REJECT {
action = "reject";
expression = "DMARC_POLICY_REJECT";
message = "DMARC policy of the sender's domain says reject";
}
}
Cinq messages depuis sender.test, envoyés depuis le client du labo. L’enregistrement SPF autorise ip6:fd89:50::50 et rien d’autre, si bien que le même client qui envoie en IPv4 est un serveur non autorisé pour ce domaine. C’est aussi exactement à cela que ressemble un message réexpédié :
| Cas | Envoyé comme | SPF | DKIM | DMARC | Score Rspamd | Réponse |
|---|---|---|---|---|---|---|
| A | signé, depuis l’adresse IPv6 listée | pass | pass | pass | 3,59 sur 15 | accepté |
| B | signé, puis les coordonnées bancaires modifiées | pass | fail | pass | 2,30 sur 15 | 554 5.7.1 DKIM signature does not verify |
| C | non signé, depuis IPv4 | fail | none | reject | 5,00 sur 15 | 554 5.7.1 SPF hard fail |
| D | signé et intact, depuis IPv4 | fail | pass | pass | 2,30 sur 15 | 554 5.7.1 SPF hard fail |
| E | non signé, depuis l’adresse IPv6 listée | pass | none | pass | 3,80 sur 15 | accepté |
Regardez le cas B. C’est une facture dont le code guichet et le numéro de compte ont été modifiés après la signature. De la fraude à la facture, autrement dit. SPF a passé, parce que le message venait du bon serveur. DMARC a passé, parce que SPF a passé et s’alignait sur le domaine From:. Sur ses scores, Rspamd lui a donné 2,30 sur 15 et l’aurait remis. La seule chose qui l’a arrêté, c’est le rejet forcé sur la signature cassée. DMARC tout seul laisse passer un message altéré tant que l’une des deux vérifications survit, et cela vaut la peine d’être su avant de déclarer un domaine protégé parce que son DMARC dit reject.
Le cas D est le coût, et il faut le dire franchement. Un message correctement signé et intact est refusé parce qu’il venait d’un serveur absent de l’enregistrement SPF. C’est ce que fait la réexpédition : le réexpéditeur fait suivre votre message depuis sa propre adresse. Rejeter sur le seul SPF veut dire que le courrier réexpédié depuis des domaines en -all se fait renvoyer. SRS sur le réexpéditeur corrige cela, et faire confiance à DKIM plutôt qu’à SPF aussi, mais la règle maison est la règle maison, et le coût est mis par écrit.
Le défaut qui fait confiance à tout réseau privé
Le deuxième réglage qui ne fait silencieusement rien. Rspamd saute SPF et quelques autres vérifications pour les adresses qu’il considère comme locales, et la liste livrée dans options.inc est celle-ci :
local_addrs = [192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, fd00::/8, 169.254.0.0/16, fe80::/10];
Toutes les plages privées, les deux familles. Si votre MX est derrière un répartiteur de charge, un NAT ou un proxy sur une adresse privée, SPF est discrètement sauté pour tout cela. Dans le labo, qui est entièrement en adressage privé, SPF n’a jamais échoué jusqu’à ce que ceci soit changé.
Le correctif évident ne marche pas. Rspamd a deux répertoires pour les réglages locaux, et sa FAQ explicite la différence : local.d « Merges with defaults. Collections and lists are combined », tandis que override.d « Replaces defaults. »11 Une liste plus étroite dans local.d/options.inc est combinée avec celle livrée, ce qui veut dire qu’elle ne restreint rien. Elle doit aller dans override.d/options.inc :
local_addrs = [127.0.0.0/8, ::1];
et rspamadm configdump options est la façon de vérifier ce avec quoi Rspamd a réellement fini, plutôt que ce que vous croyez avoir écrit.
Le greylisting est aussi coupé dans cette installation. Il envoie un rejet temporaire aux expéditeurs inconnus et les accepte au réessai, ce qui arrête effectivement une partie du spam, mais le greylisting de Rspamd répond avant que SPF et DMARC aient eu leur mot à dire, si bien qu’en test vous voyez un report là où vous attendiez un verdict.12 Les tests ci-dessus n’ont de sens qu’avec lui coupé.
Vos propres bounces, refusés par votre propre DMARC
Le dernier piège est apparu quand Stalwart dans le labo a refusé un bounce de ce serveur. Postfix envoie les bounces avec un expéditeur d’enveloppe vide et From: MAILER-DAEMON@mail.alpha.test. Sans expéditeur d’enveloppe, la seule vérification SPF qui reste porte sur le nom que le serveur a donné dans HELO, mail.alpha.test, qui n’avait pas d’enregistrement SPF. Le bounce n’était pas signé non plus. DMARC est donc retombé sur la politique de alpha.test, a trouvé p=reject, et le serveur récepteur a fait exactement ce que le domaine lui disait de faire :
smtp.dmarc-fail … domain = "alpha.test", policy = "reject", result = DMARC check failed
(dmarc.fail) { causedBy = Policy not aligned }
La propre politique du domaine jetait ses propres échecs de remise. Chaque nom d’hôte MX reçoit son propre enregistrement SPF, ce qui corrige cela :
mail IN TXT "v=spf1 a -all"
Six réglages qui ont l’air faits
Les pièges ci-dessus ont une forme commune. Chacun d’eux est un réglage qui a l’air fait, ne produit aucune erreur, et ne fait rien du tout.
| A l’air fait | Ce qui se passe vraiment | Trouvé par |
|---|---|---|
smtp_tls_security_level = dane | du texte en clair partout où aucun TLSA n’est publié | en lisant postconf(5) |
| vérifications login-mismatch sur le port 25 | ignorées, un avertissement dans le log | en envoyant une usurpation |
local_addrs restreint dans local.d | fusionné avec le défaut, rien de restreint | rspamadm configdump |
DMARC p=reject sur l’expéditeur | le courrier altéré passe si SPF passe | cas B |
| renouvellements certbot | nouvelle clé, TLSA ne correspond plus | en lisant la doc de certbot |
DMARC p=reject sur votre propre domaine | vos propres bounces refusés | un bounce refusé |
Deux d’entre eux sont dans la documentation, à condition de lire le bon paragraphe de la bonne page. Les quatre autres ne sont apparus qu’en envoyant le message que la règle était censée arrêter et en regardant ce qui revenait. Relire la configuration et l’approuver n’en a trouvé aucun. Si vous ne gardez qu’une habitude de ce billet, gardez celle-là, parce que chaque ligne ici peut être copiée dans un serveur et ne toujours pas fonctionner.
Les défauts sont la décision de quelqu’un d’autre
Chaque refus de ce billet est un réglage que Postfix a depuis des années. Même DANE, la chose la plus récente ici, y est depuis la 2.11.13 Rien de tout cela n’est nouveau, rien n’est cher, et le serveur en dessous est le même logiciel libre qu’il a toujours été. L’écart entre un serveur de messagerie qui laisse passer une facture altérée et un qui la refuse à DATA, c’est une page de configuration et un après-midi de tests.
Alors pourquoi si peu d’internet tourne-t-il comme ça ? Parce que ce sont les défauts qui décident, et les défauts ont été réglés pour s’assurer que le courrier passe sur un internet où TLS était l’exception. C’était un choix raisonnable à l’époque. Laissez-les tels quels aujourd’hui et vous avez confié vos décisions de sécurité à un choix fait par quelqu’un en 2002, pour un réseau qui n’existe plus, à propos d’un courrier qui n’était pas le vôtre.
Faire tourner un serveur de messagerie, c’est décider d’être responsable de ce qui y transite. Ces réglages sont cette décision mise par écrit. Les défauts sont ce que vous obtenez quand personne ne l’a prise.
postconf(5) : smtpd_tls_security_level — le niveau
encrypt: « According to RFC 2487 this MUST NOT be applied in case of a publicly-referenced SMTP server. » ↩︎postconf(5) : smtp_tls_security_level — le niveau
dane: « When DNSSEC-validated TLSA records are not found the effective tls security level is « may ». » ↩︎Postfix TLS_README : Opportunistic TLS — « The SMTP transaction is encrypted if the STARTTLS ESMTP feature is supported by the server. Otherwise, messages are sent in the clear. » ↩︎
postfix-tlspol sur GitHub — serveur de politique MTA-STS et DANE pour Postfix, sous licence MIT : « Verify TLSA records for correctness and supported parameters, only then the dane-only policy (Mandatory DANE) will be returned. » Version 1.14.4 utilisée ici. ↩︎
Source Postfix, src/dns/dns_lookup.c — « Automatically turns on the RES_TRUSTAD flag on systems that support this flag », et le code qui le fait lorsque la validation DNSSEC est demandée. ↩︎
RFC 7672 §3.1.1 — SMTP security via opportunistic DANE TLS. Les enregistrements TLSA pour les serveurs SMTP « should, in most cases, be « DANE-EE(3) SPKI(1) SHA2-256(1) » records ». ↩︎
Guide utilisateur Certbot, options en ligne de commande —
--reuse-key: « When renewing, use the same private key as the existing certificate. (default: False) », et sous--no-reuse-key: « Not reusing private keys is the default behavior of Certbot. » ↩︎postconf(5) : reject_known_sender_login_mismatch — « to enforce that the From: header address matches the envelope sender (MAIL FROM) address, use an external filter such as a Milter ». ↩︎
postconf(5) : reject_unknown_sender_domain — rejette quand le domaine du MAIL FROM a « no DNS MX and no DNS A record », ou un MX mal formé. ↩︎
postconf(5) : unknown_address_reject_code — défaut 450 : « Do not change this unless you have a complete understanding of RFC 5321. » ↩︎
FAQ Rspamd : la différence entre local.d et override.d — local.d « Merges with defaults. Collections and lists are combined » ; override.d « Replaces defaults. Entire sections are overwritten. » ↩︎
Module de greylisting Rspamd — « The greylisting module triggers a soft reject action, which is intended to be interpreted by the MTA as a temporary rejection ». ↩︎
Postfix TLS_README : DANE — « Postfix ≥ 2.11 supports the new dane and dane-only security levels ». ↩︎