85.1.1. pepsi-ingress¶
receive incoming e-mail over SMTP and store it for Pepsi
- Section du manuel:
1
85.1.1.1.1. Nom¶
pepsi-ingress - serveur SMTP qui stocke l’e-mail entrant dans PostgreSQL.
85.1.1.1.2. Synopsis¶
pepsi-ingress [GLOBAL-OPTIONS] serve
85.1.1.1.3. Description¶
pepsi-ingress est le composant de réception du courrier de Pepsi. Il exécute un serveur SMTP (un Message Transfer Agent) qui accepte l’e-mail entrant en clair, en TLS implicite et via STARTTLS. Un corps de message peut être envoyé avec DATA ou, pour les clients qui l’annoncent, avec CHUNKING/BDAT (RFC 3030) — annoncé dans la réponse EHLO. Il stocke chaque message accepté — avec son enveloppe, une petite quantité de métadonnées analysées, et les métadonnées d’origine SMTP de la connexion qui le remet (IP du client, nom HELO/EHLO, TLS, listener, DNS inverse/iprev ; voir pepsi.conf(5)) — dans la table workqueue du schéma pepsi d’une base de données PostgreSQL. Le message est stocké scindé en une colonne headers (tout jusqu’à la ligne vide) et un corps, de sorte que les étapes en aval qui ne touchent que les en-têtes n’aient pas à charger le corps. Le corps est une ligne de sa propre table, workqueue_body, que la ligne du message référence : chaque ligne de destinataire que le pipeline scinde ensuite du message partage cette unique copie (voir pepsi.conf(5), Données stockées). Le message entre dans le pipeline d’étapes à l’étape initiale (stage = init). Une fois la ligne validée, un NOTIFY sur le canal workqueue réveille pepsi-dispatch(1). Cette notification est émise hors bande et coalescée — au plus une par DISPATCH_WAKE_INTERVAL (par défaut 5 ms), de sorte qu’une rafale d’admissions ne réveille le dispatcher qu’une fois — parce qu’une transaction porteuse d’une notification ne peut pas valider en groupe, ce qui sérialiserait chaque session SMTP concurrente derrière son propre fsync. Rien n’est en danger si l’une d’elles est perdue : le balayage POLL_INTERVAL du dispatcher est le filet de sécurité, le coût n’est donc que de la latence.
Le serveur accepte le courrier pour un ensemble configuré de domaines. Au sein de ces domaines, il accepte tout local-part sans essayer de vérifier que la boîte aux lettres existe ; les destinataires en dehors des domaines configurés sont rejetés comme relayage (550). La boîte aux lettres réservée sans domaine <Postmaster> est toujours acceptée quels que soient les domaines configurés (RFC 5321 §4.5.1) et réécrite en une adresse routable — [pepsi-ingress] POSTMASTER lorsqu’il est défini, sinon postmaster@HOSTNAME — de sorte que le reste du pipeline (alias, remise locale, relais) l’achemine comme tout destinataire ordinaire. Pour faire parvenir le courrier postmaster à une personne, soit pointez POSTMASTER vers une vraie boîte aux lettres, soit ajoutez un alias pour l’adresse réécrite dans une carte ALIASES de pepsi-stage-aliases. Un tel message est aussi validé avec state.spam = false afin que les barrières de langue et de péage à l’envoi (pepsi-stage-block-language(1), pepsi-stage-anti-spam(1)) le transmettent, gardant la boîte aux lettres postmaster joignable comme l’exige la RFC 5321 §4.5.1 — mais seulement lorsque ``<Postmaster>`` était l’unique destinataire de la transaction. Un message adressé au postmaster et à une boîte aux lettres ordinaire est validé sans ce contournement et passe les barrières comme tout autre, car autrement un spammeur pourrait exempter le courrier d’une vraie victime simplement en ajoutant à côté un RCPT <Postmaster>. Lorsque SRS est configuré (la section partagée [pepsi-srs] ; voir pepsi-stage-srs(1)), un destinataire dans le domaine SRS dont le local-part est un jeton Sender Rewriting Scheme valide est décodé en sens inverse vers l’expéditeur d’origine et accepté pour un relais vers celui-ci — même si le domaine de cet expéditeur n’est pas un domaine que nous servons, car la signature valide l’autorise ; un jeton falsifié ou expiré est rejeté (550), sans dire lequel des deux il était. Comme accepter un tel destinataire autorise un relais, ce couple accepter/rejeter est un oracle en ligne sur la signature SRS ; une connexion qui présente trois destinataires SRS invalides est donc journalisée en warn et fermée avec 421, de sorte qu’une recherche de jeton valide coûte une connexion par tranche de trois essais et soit visible pour la surveillance des abus. C’est ainsi que les rebonds vers du courrier que Pepsi a redirigé retrouvent leur chemin vers les expéditeurs d’origine. La réception d’un message n’est confirmée au client (250) qu’après que le message a été validé durablement dans la base de données ; si la base de données est indisponible, le client reçoit une erreur transitoire (451) et est censé réessayer.
Comme l’exige la RFC 5321 §4.4, pepsi-ingress ajoute en tête un en-tête de trace Received: à chaque message accepté, nommant le client connecté (nom HELO/EHLO, DNS inverse et IP), ce serveur, le transport utilisé (SMTP, ESMTP, ESMTPS, ESMTPA ou ESMTPSA selon la RFC 3848 — voir Authentification ci-dessous) et l’heure de réception ; une clause for nomme le destinataire seulement pour une transaction à destinataire unique. L’en-tête est ajouté avant l’authentification de sorte que le sceau ARC (ajouté plus tard par pepsi-stage-arc(1)) le couvre.
Pour une session chiffrée, le mot-clé ESMTPS à lui seul n’enregistre pas quelle version TLS ni quelle suite de chiffrement ont été négociées. Comme le recommande la RFC 8314 §4.3, pepsi-ingress ajoute donc les paramètres négociés sous forme d’un commentaire après le mot-clé with, par exemple
by mail.example.org (pepsi-ingress) with ESMTPS
(version=TLSv1_3 cipher=TLS13_AES_128_GCM_SHA256)
Ces valeurs sont les mêmes paramètres TLS notés dans le state du message sous origin.tls (voir pepsi.state(7)) ; celles de la version et de la suite de chiffrement que la pile TLS rapporte sont incluses.
Un en-tête X-Pepsi-List-Hops: (le compteur de boucle des listes de diffusion) est retiré de tout message arrivant sur une session non authentifiée, car un expéditeur pourrait sinon réinitialiser le compteur ; la copie d’une session authentifiée est conservée.
Plusieurs flux SMTP sont traités en concurrence. Plusieurs listeners peuvent être configurés, chacun lié à son propre port TCP, sa socket de domaine UNIX, ou une socket héritée d’un parent systemd via l’activation par socket, et chacun avec son propre mode de transport. Tout le comportement est piloté par un fichier de configuration de style INI (voir pepsi.conf(5)).
Le schéma de base de données est installé par pepsi-setup(1), qui applique les migrations de chaque composant au schéma pepsi partagé ; pepsi-ingress lui-même n’a pas de commande d’initialisation de schéma.
85.1.1.1.3.1. Extensions SMTP¶
La réponse EHLO annonce exactement ces mots-clés, dans cet ordre
SIZE <MAX_MESSAGE_SIZE>
8BITMIME
SMTPUTF8
PIPELINING
CHUNKING
DSN
ENHANCEDSTATUSCODES
SIZE (RFC 1870) annonce [pepsi-ingress] MAX_MESSAGE_SIZE, qui est un réglage valable pour tout le serveur plutôt que par listener. Deux autres mots-clés sont conditionnels : STARTTLS (RFC 3207) est annoncé sur un listener disposant de matériel TLS tant que la session est encore en clair — c’est-à-dire MODE = starttls, puisque MODE = tls est chiffré dès le premier octet — et AUTH PLAIN LOGIN (RFC 4954) uniquement sur une session déjà chiffrée dont le listener configure un arrière-plan SASL_TYPE.
Rien d’autre n’est proposé. BINARYMIME en particulier n’est ni annoncé ni accepté (voir Contenu 8 bits et internationalisé) ; VRFY reçoit la réponse non engageante 252 et EXPN est décliné par 502, puisque le §7.3 de la RFC 5321 fait de la divulgation de l’appartenance à une liste une fuite d’information plutôt qu’un service.
85.1.1.1.3.2. Authentification¶
Avant qu’un message ne soit stocké, pepsi-ingress l’authentifie, afin que les destinataires en aval — après que Pepsi a relayé le courrier et rompu l’alignement SPF et DKIM d’origine — puissent encore voir le verdict auquel Pepsi est parvenu à la limite. Pour chaque message accepté, il :
vérifie SPF (sur l’identité
MAIL FROM, en utilisant l’IP du client connecté), DKIM et DMARC ; etajoute en tête un en-tête
Authentication-Resultsrapportant ces résultats, avec le[pepsi-ingress] HOSTNAMEcommeauthserv-id, et note le texte de résultat rendu sousstate.origin.ar_results.
ARC est géré séparément : l’ingress ne vérifie pas la chaîne ARC et ne scelle pas le message. L’étape de pipeline pepsi-stage-arc(1) vérifie toute chaîne ARC entrante et ajoute le nouvel ensemble ARC (AAR/AMS/AS) sous l’identité [pepsi] ARC_DOMAIN, en lisant le contexte de session (authserv-id, IP du client, HELO/EHLO et le fragment ar_results) que l’ingress a noté dans le state de la ligne. Réutiliser ce fragment permet à l’étape de construire l’AAR sans réexécuter SPF/DKIM/DMARC, de sorte que les vérifications d’authentification ont lieu exactement une fois, et cela garde les clés de signature ARC hors du frontal SMTP.
La vérification est fail-open : un échec DNS ou un message inanalysable ne rejette jamais le courrier ; il est noté comme temperror/none et accepté. Ce n’est que lorsque DMARC_ENFORCE est défini qu’un échec DMARC certain sous une politique quarantine/reject provoque un rejet 550 ; par défaut, un tel courrier est accepté (et l’étape ARC scelle le résultat en échec) pour que le destinataire final en juge. Les verdicts SPF/DKIM/DMARC sont stockés dans l’objet state.auth de la ligne (sous spf/dkim/dmarc/arc), et l”authserv-id sous state.origin ; le verdict arc commence à none et est rempli plus tard par l’étape ARC. Les résolutions DNS ont lieu de façon synchrone dans la phase DATA, de sorte qu’un résolveur lent retarde le 250.
Le travail est borné, car le nombre de signatures est au choix de l’expéditeur et chacune coûte une recherche DNS et une vérification de clé publique. Au plus MAX_DKIM_SIGNATURES (par défaut 10) champs DKIM-Signature sont vérifiés, en commençant par ceux dont le d= est le domaine du From: ou un parent ou un enfant de celui-ci — les seuls que DMARC puisse utiliser —, de sorte que bourrer un message de signatures ne peut pas évincer celle qui compte. SPF et les signatures retenues sont vérifiés en parallèle sous un même délai de VERIFY_TIMEOUT (par défaut 20 s), et DMARC sous un second, de sorte qu’un message ne retient jamais sa session en vérification plus de deux fois cette durée. Une vérification qui dépasse son délai est notée temperror pour elle seule : une signature expirée dont le domaine n’est pas aligné sur celui du From: est une signature que DMARC ignore, de sorte qu’un faussaire ne peut pas ralentir son propre DNS pour transformer un échec DMARC en quelque chose sur quoi DMARC_ENFORCE n’agit pas.
85.1.1.1.3.3. Notifications d’état de remise (DSN)¶
pepsi-ingress annonce l’extension DSN (RFC 3461) dans sa réponse EHLO et accepte ses paramètres : RET et ENVID sur MAIL FROM et NOTIFY et ORCPT sur RCPT TO. Leur syntaxe est validée — une valeur RET/NOTIFY invalide, un NOTIFY=NEVER combiné avec d’autres mots-clés, ou un xtext ENVID/ORCPT malformé est rejeté avec 501 — et les valeurs acceptées sont stockées dans le state.dsn de la ligne (les ret/envid au niveau du message plus un tableau par destinataire de notify/orcpt). Le pipeline d’étapes les honore : les étapes de relais propagent les paramètres au saut suivant lorsqu’il annonce aussi DSN, et pepsi-stage-bounce(1) ne génère un rebond d’échec que lorsque le NOTIFY du destinataire le demande (FAILURE, la valeur par défaut en son absence), en renvoyant en écho ENVID/ORCPT dans le DSN. Chaque étape doit préserver state.dsn.
85.1.1.1.3.4. Contenu 8 bits et internationalisé¶
pepsi-ingress annonce 8BITMIME (RFC 6152) et SMTPUTF8 (RFC 6531) dans sa réponse EHLO, de sorte qu’un message peut porter des octets 8 bits bruts dans son corps et de l’UTF-8 dans ses en-têtes et son enveloppe. Le paramètre BODY= sur MAIL FROM est accepté et noté (state.origin.body_8bit) ; seuls 7BIT et 8BITMIME sont valides — BINARYMIME n’est pas annoncé et est rejeté avec 501. Les étapes de relais ré-annoncent BODY=8BITMIME / SMTPUTF8 à un saut suivant qui les prend en charge, et rétrogradent le message lorsqu’il ne les prend pas en charge : un corps 8 bits vers un saut non-8BITMIME est ré-encodé en MIME vers un codage de transfert de contenu 7 bits, et les en-têtes UTF-8 vers un saut non-SMTPUTF8 sont réécrits en mots encodés RFC 2047, ou selon la RFC 2231 pour un paramètre MIME tel que le nom de fichier d’une pièce jointe (une adresse non ASCII à cet endroit ne peut pas être rétrogradée et rebondit). Un message dont le corps ne peut pas du tout être converti — malformé au-delà de ce que le ré-encodeur peut réparer — rebondit plutôt que d’être envoyé en 8 bits, comme l’exige la RFC 6152 §3. Voir pepsi-stage-relay-to-internet(1).
85.1.1.1.3.5. Validation des paramètres ESMTP¶
pepsi-ingress valide strictement les paramètres MAIL FROM/RCPT TO. Les paramètres reconnus sont SIZE, BODY et SMTPUTF8 (et AUTH) sur MAIL FROM ainsi que les paramètres DSN ci-dessus ; une valeur SIZE= qui n’est pas un entier non négatif est rejetée avec 501 (RFC 1870 §6.1) plutôt que silencieusement ignorée. Un paramètre que le serveur n’implémente pas est rejeté avec 555 5.5.4 (RFC 5321 §4.1.1.11) — il n’est pas silencieusement ignoré. La seule exception est le paramètre AUTH= de la RFC 4954 sur MAIL FROM (qu’un client authentifié peut joindre car AUTH est annoncé) : il est accepté et ignoré — Pepsi ne propage pas l’identité affirmée plus loin — plutôt que rejeté.
85.1.1.1.3.6. Authentification du client¶
Chaque listener peut authentifier ses clients, marquant le courrier accepté comme d’origine locale. Une session est authentifiée lorsque l’un quelconque de quatre mécanismes par listener l’accepte :
MYNETWORKS — l’IP connectée est au sein de l’un d’une liste séparée par espaces/virgules de réseaux CIDR IPv4/IPv6 de confiance (une adresse nue est prise comme un hôte
/32ou/128).SASL — un backend
SASL_TYPE(actuellementdovecot, via la socket auth-clientSASL_PATH) accepte un échangeAUTH.AUTH(mécanismesPLAINetLOGIN) est annoncé et accepté seulement sur une session protégée par TLS (TLS implicite ou aprèsSTARTTLS) ; une tentativeAUTHen clair est refusée avec504 5.5.4(RFC 4954 §4, qui déprécie la réponse538pour ce cas).Certificat client TLS — le client présente un certificat dont le SHA-256 du
SubjectPublicKeyInfo(64 caractères hex) est listé dansTLS_AUTH_CLIENT, ou dont la chaîne se valide par signature jusqu’à une clé de CA listée.AUTH_PEERCRED — sur une socket de domaine UNIX, l’uid du pair attesté par le noyau (
SO_PEERCRED) est résolu en un login et fourni à la machinerieUSERNAME_MAP, de sorte qu’un appelant ne peut envoyer qu’en son propre nom. Vaut ``yes`` par défaut pour un listener ``SERVE = unix`` (mettezAUTH_PEERCRED = nopour le désactiver) ; refusé d’emblée sur un listener TCP, où un pair n’a pas d’identité attestée par le noyau. C’est ce qui authentifie la socket de soumission locale qu’utilise pepsi-sendmail(1), et cela pose le mot-cléESMTPAde la RFC 3848. Pour un listenerSERVE = systemd, il est accepté mais jamais déduit, puisque la famille de la socket vit dans l’unité ; un avertissement est journalisé si le descripteur hérité s’avère être du TCP.
SASL_TYPE et TLS_AUTH_CLIENT nécessitent un listener TLS (MODE = tls ou starttls). Une session authentifiée est notée avec state.local_origin = true (sinon false) et est autorisée à relayer vers n’importe quel domaine, pas seulement vers les domaines servis. Une session qui porte une identité — un AUTH SASL, ou AUTH_PEERCRED sur une socket UNIX, mais ni MYNETWORKS ni un certificat client — pose en outre le A de la RFC 3848 dans le mot-clé with de l’en-tête de trace : ESMTPSA sur TLS, et simplement ESMTPA pour la socket de soumission UNIX en clair, d’où provient le courrier soumis localement (pepsi-sendmail(1), cron). Filtrez sur les deux, pas seulement sur ESMTPSA. Un certificat client non reconnu n’interrompt jamais la poignée de main — il laisse simplement la session non authentifiée.
85.1.1.1.3.7. Soumission de messages (RFC 6409)¶
Un listener portant SUBMISSION = yes est traité comme un Message Submission Agent (RFC 6409) plutôt que comme un MX :
L’authentification est obligatoire. Un
MAIL FROMnon authentifié est refusé avec530 5.7.0 Authentication required; le client doit d’abord satisfaire l’un des mécanismes d”Authentification du client ci-dessus. L’indicateur exige donc au moins un mécanisme configuré (SASL_TYPE,TLS_AUTH_CLIENT,MYNETWORKSouAUTH_PEERCRED) ; un listener de soumission qui n’en a aucun est une erreur de configuration, refusée à l’analyse du fichier — donc par pepsi-setup(1) et par le serveur lui-même au démarrage. Il exige aussi unMODEprotégé par TLS (tlsoustarttls) — sauf sur une socket locale, c’est-à-direSERVE = unixou un listenerSERVE = systemdqui en hérite une, là où il n’y a pas de réseau à protéger. La socket de soumission locale livrée est exactement cela :MODE = plainavecSUBMISSION = yesetAUTH_PEERCRED. Différer la règle n’est pas l’abandonner — un listener de soumission en clair dont il s’avère au moment de la liaison qu’il a hérité d’une socket réseau fait refuser à pepsi-ingress de démarrer.Des corrections de soumission sont appliquées à chaque message accepté : un en-tête
Date:manquant est ajouté (§8.2) et unMessage-ID:manquant est généré sous la forme<random@HOSTNAME>(§8.3). Les en-têtes déjà présents sont laissés intacts, et les corrections s’exécutent avant que l’en-tête de traceReceived:ne soit ajouté en tête, de sorte que les champs ajoutés soient couverts par le sceau ARC et la signature DKIM en aval.
L’indicateur n’a aucun effet sur un listener MX ; laissez-le désactivé (la valeur par défaut) pour le port 25.
85.1.1.1.3.8. Identité de soumission (RFC 6409 §6 / §8.1)¶
Une session qui porte un nom d’utilisateur est liée à l’ensemble des adresses e-mail que ce nom d’utilisateur a le droit d’utiliser. Deux des quatre mécanismes ci-dessus en portent un : un échange SASL AUTH (l”authcid) et AUTH_PEERCRED sur une socket UNIX (le login auquel se résout l’uid rapporté par le noyau). Les deux sont soumis à tout ce que dit la présente section ; MYNETWORKS et un certificat client TLS ne le sont pas, parce qu’ils authentifient une adresse ou une clé plutôt qu’un compte.
Par défaut, le nom d’utilisateur alice est mappé à l’unique adresse alice@HOSTNAME (le domaine [pepsi-ingress] HOSTNAME) ; le fichier facultatif USERNAME_MAP redéfinit ce mappage. Chaque ligne non vide et non commentée est
username: addr1@example.org addr2@example.org
un # commence un commentaire, la clé est le nom d’utilisateur SASL (avant le premier :), et la valeur est la liste séparée par espaces/virgules des adresses autorisées, qui peut être vide. Les noms d’utilisateur sont mis en correspondance sans tenir compte de la casse et les adresses sont mises en minuscules. Le fichier est relu chaque fois que son heure de modification change (vérifiée à chaque authentification), de sorte que les modifications prennent effet sans redémarrage ; un fichier manquant ou illisible est traité comme vide (chaque utilisateur retombe sur la valeur par défaut).
Une adresse autorisée peut utiliser un joker * (un glob * mis en correspondance avec l’adresse candidate), de sorte que alice: *@example.org permet à alice d’utiliser n’importe quelle adresse à example.org et un * nu permet n’importe quelle adresse tout court. Une entrée joker ne nomme aucune identité canonique unique, elle est donc sautée pour la correction Sender: ci-dessous (la première adresse concrète, sans joker, est l’identité canonique, le cas échéant). pepsi-setup vérifie la syntaxe d’un USERNAME_MAP configuré au moment de l’installation (voir pepsi-setup(1)). Le joker est un glob, pas une expression régulière : écrivez *@example.org, jamais .*@example.org (un point littéral suivi d’un glob — il ne correspond à rien, si bien que l’utilisateur se verrait refuser toutes les adresses qu’il était censé lui accorder). Setup rejette un tel motif et nomme le glob que vous vouliez.
Le mappage a trois issues pour un nom d’utilisateur :
listé avec des adresses — exactement ces adresses (et motifs joker) sont autorisées ; la première concrète (sans joker) est l”identité canonique utilisée pour la correction
Sender:;listé sans adresse — l’utilisateur est refusé : même si SASL a accepté les identifiants, la commande
AUTHest refusée par535 5.7.8et la session reste non authentifiée. Sur une session par identifiants de pair il n’y a aucune commande à refuser, si bien que la session ne s’authentifie tout simplement jamais — et, sur un listener de soumission, son premierMAIL FROMse voit alors opposer530 5.7.0. C’est ainsi qu’un compte système (www-data, par exemple) est exclu de toute soumission ;non listé (ou aucun
USERNAME_MAPconfiguré) — la valeur par défautusername@HOSTNAME.
Étant donné cet ensemble, deux vérifications s’exécutent sur une telle soumission :
Identité d’enveloppe et d’en-tête (§6). L’expéditeur d’enveloppe
MAIL FROMet l’adresseFrom:du message doivent chacun être l’une des adresses autorisées, sinon le message est refusé avec550 5.7.1(àMAIL FROMet à la fin duDATArespectivement). L’expéditeur nul<>et un message sans adresseFrom:analysable sont exemptés. La RFC 5322 §3.6.2 fait deFrom:une liste de boîtes aux lettres, de sorte qu’une valeur peut légalement en nommer plusieurs — et chacune d’elles doit être une adresse autorisée, pas seulement l’une d’elles. Vérifier une seule boîte aux lettres ne serait pas une vérification du tout, car les lecteurs de courrier affichent la première alors qu’une boîte autorisée pourrait se trouver n’importe où dans la liste.Correction du Sender (§8.1). Le champ
Sender:appartient au MSA, non au client : une adresse qu’un client y place n’est soumise à aucune vérification d’identité sur aucun chemin, et Pepsi la signerait ensuite en DKIM comme une affirmation sur l’auteur de l’envoi. Ainsi, chaque fois que le compte possède une identité canonique, unSender:existant est remplacé ou retiré, ce que le §8.1 permet explicitement (« The MSA MAY add or replace the “Sender” field »).Lorsque l’adresse
From:est une adresse autorisée qui diffère de l’identité canonique, ou qu’elle nomme plus d’une boîte aux lettres, un en-têteSender:nommant l’identité canonique est ajouté en tête — pour le cas à plusieurs boîtes aux lettres, la RFC 5322 §3.6.2 exige la présence d’unSender:, et c’est le §8.1 qui permet au MSA de le fournir. LorsqueFrom:est une boîte aux lettres unique nommant l’identité canonique, unSender:est redondant avec elle (§3.6.2 : « SHOULD NOT be used if it is redundant with the “From:” field ») et il est retiré à la place.Un utilisateur à qui seuls des motifs joker ont été accordés n’a pas d’identité canonique unique à affirmer : ni l’un ni l’autre ne s’applique et un
Sender:existant est laissé tranquille — il n’y a rien sur quoi il pourrait être démenti. Comme les autres corrections, cela se produit avant l’en-tête de trace, de sorte que le résultat est couvert par le sceau ARC et la signature DKIM.
Ces vérifications s’appliquent à toute session qui porte un nom d’utilisateur, qu’il provienne d’un AUTH SASL ou d”AUTH_PEERCRED. Une session MYNETWORKS ou par certificat client TLS n’en a aucun à mapper et n’y est pas soumise.
Outre la vérification de la section 6, ingress enregistre comment le From: a été autorisé, sous state.origin.from_bound : true exactement lorsque From: nomme une seule boîte aux lettres qui a correspondu à une entrée concrète (sans joker) des adresses autorisées du compte, ou au username@HOSTNAME par défaut ; false sinon, et toujours false pour une session sans nom d’utilisateur. Avoir le droit d’envoyer en tant qu’une adresse suffit pour envoyer ; y être lié est ce qui permet à une soumission de parler en son nom. pepsi-stage-encrypt(1) n’enregistre la clé qu’annonce le client de messagerie d’un utilisateur, ou n’exécute une commande de clé par courriel, que pour un From: lié (ou un relais MYNETWORKS/par certificat client) : une autorisation *@example.org permet à un compte d’envoyer au nom de ses collègues, et ne doit pas lui permettre d’implanter une clé pour eux. pepsi-setup(1) avertit au sujet des comptes qui détiennent des autorisations à joker.
85.1.1.1.3.9. Socket de soumission locale¶
Un listener n’est jamais configuré : la socket de soumission locale à laquelle pepsi-sendmail(1) — et donc /usr/sbin/sendmail, cron, et tout ce qui poste du courrier localement — se connecte. pepsi-ingress la sert inconditionnellement, parce que fournir ce chemin est une propriété du fait d’être un serveur de courrier plutôt qu’une fonctionnalité à activer. Ce n’est pas une section de listener facultative, car cela ferait dépendre le service de l’accord de trois choses distinctes (l’existence de la section, la correspondance de son UNIXPATH avec la socket du client, et le fait que le répertoire d’exécution soit accessible en écriture), chacune facile à manquer avec le même symptôme silencieux : le courrier local disparaît.
Le chemin est [pepsi-sendmail] SOCKET (par défaut /run/pepsi/submission.sock), lu depuis cette unique option par les deux extrémités, de sorte que le client et le serveur ne peuvent pas nommer des sockets différentes. Tout le reste est fixé, parce que rien de tout cela n’est un choix : le listener s’appelle local-submission dans le journal, et il est MODE = plain avec SUBMISSION = yes et AUTH_PEERCRED. Lorsqu’elle est liée ici plutôt qu’héritée de pepsi-ingress.socket, elle est créée en mode 0666 ; ce mode permissif n’accorde rien, car ouvrir la socket ne confère aucune autorité — le noyau fournit l’uid de l’appelant et USERNAME_MAP décide de ce qu’il peut envoyer comme adresse.
Une section [pepsi-ingress-listener-*] explicite qui sert le même chemin l’emporte et rien n’est synthétisé à côté d’elle : un opérateur qui y veut d’autres options (un mode plus strict, un UNIXPATH_GROUP, pas d’identifiants de pair) en écrit donc une. Ne pas réussir à la lier est fatal, comme pour tout autre listener : un serveur debout avec seulement une partie de ce qu’on lui a demandé de servir est précisément la défaillance que cet agencement existe pour supprimer. La seule façon de la désactiver est SOCKET = none, ce qui fait aussi refuser pepsi-sendmail(1) de s’exécuter ; avec cette sentinelle définie et aucune section de listener du tout, le serveur n’écouterait rien et refuse de démarrer.
85.1.1.1.3.10. Quota de boîte aux lettres au RCPT¶
Lorsqu’un destinataire est sur l’un des domaines servis, pepsi-ingress peut le refuser dans la session SMTP parce que la boîte aux lettres est connue pour être pleine — 452 4.2.2 ou 552 5.2.2 selon [pepsi] MAILBOX_OVER_QUOTA. Refuser ici plutôt qu’accepter puis rebondir plus tard garde le MTA émetteur en ligne, de sorte que c’est lui qui prévient son utilisateur, et Pepsi ne génère aucun backscatter vers un expéditeur d’enveloppe qui peut fort bien être falsifié.
Quatre propriétés rendent cela sûr à faire à partir d’une ligne de base de données.
Aucune résolution. Le local-part du destinataire — mis en minuscules et débarrassé de toute sous-adresse au [pepsi] RECIPIENT_DELIMITER — est comparé exactement à un login passwd. Les alias, les fichiers ~/.forward et la base passwd elle-même relèvent de l’étape de remise ; pepsi-ingress n’en devine aucun, de sorte qu’une adresse qui n’est pas écrite comme un login ne correspond simplement à rien et est acceptée.
Aucune mesure. pepsi-ingress ne peut pas lire un Maildir en 0700, et ne doit jamais acquérir le groupe pepsi-maildir qui lui permettrait de lancer le helper qui le peut. Il lit des chiffres écrits par quelqu’un d’autre, et ne détient que SELECT sur la table.
Seule une mesure fraîche compte. L’estimation d’occupation courante n’est délibérément pas consultée : elle surcompte, et refuser sur elle fermerait une boîte aux lettres que son propriétaire a depuis vidée — définitivement, puisque tout message étant refusé, aucune remise ne s’exécuterait jamais pour la remesurer. Une mesure plus ancienne que [pepsi] MAILBOX_QUOTA_MAX_AGE accepte le message et laisse le chemin de remise y jeter un œil neuf ; le reconcile de pepsi-quota(1) maintient mesurés les comptes à leur limite.
Toute incertitude accepte. Aucune ligne, aucune mesure, une erreur de base de données — tout cela signifie accepter, conformément au reste du traitement entrant.
La boîte aux lettres sans domaine <Postmaster> est acceptée quoi qu’il arrive, car la RFC 5321 §4.5.1 exige qu’elle reste joignable.
Mettez [pepsi] MAILBOX_QUOTA_RCPT_CHECK = no pour sauter entièrement la consultation et laisser toute l’application au moment de la remise.
85.1.1.1.3.11. Destinataires inconnus au RCPT¶
Un destinataire d’un domaine servi que rien sur cet hôte n’accepterait est refusé dans la session SMTP avec 550 5.1.1, pour la même raison qu’une boîte aux lettres pleine : le MTA émetteur prévient son propre utilisateur, au lieu que Pepsi fasse rebondir le message plus tard vers un expéditeur d’enveloppe que le spam falsifie (backscatter, le moyen le plus rapide de finir sur une liste noire). [pepsi-ingress] VERIFY_RECIPIENTS (par défaut auto) le contrôle.
pepsi-ingress n’exécute pas le pipeline pour le savoir. Il demande si quelque chose qui peut rendre une adresse réelle s’en porte garant :
un compte local, résolu exactement comme le résolvent les étapes Maildir et
~/.forward;un alias, reconnu par le code même de l’étape d’alias ;
une liste de diffusion ;
postmaster@,abuse@et l’adresse de contrôle de toute étape qui en traite une ;le MDA derrière une étape LMTP, ou le backend derrière une route de passerelle, interrogé avec
MAIL FROM:<>etRCPT TOet sans message.
Lesquelles de ces sources s’appliquent à quel domaine servi est déduit des sections [stage-*] au démarrage ; pepsi-setup run affiche le résultat. Un domaine n’est refusé-si-inconnu que lorsque chaque chemin que peut prendre son courrier dispose de quelque chose qui peut répondre. Un alias fourre-tout, une étape exécutant un programme que Pepsi ne connaît pas, ou un backend que personne ne peut interroger laisse le domaine ouvert, comme auparavant.
Toute incertitude accepte : une table d’alias ou une liste de destinataires illisible, une erreur de base de données, une sonde qui échoue, expire ou est refusée pour toute autre raison que « pas de telle boîte aux lettres », ou plus de 16 sondes en cours. Une adresse n’est refusée que lorsque chaque source applicable a dit non. Le traitement des boîtes aux lettres inconnues par le pipeline lui-même reste derrière, pour les cas que ce contrôle laisse passer.
Un « oui » d’une sonde est mis en cache une heure et un « non » cinq minutes, de sorte qu’une nouvelle boîte aux lettres est joignable en quelques minutes. Après MAX_INVALID_RECIPIENTS (par défaut 10) destinataires inconnus, une session est fermée avec 421. Une attaque par dictionnaire coûte alors des connexions, que les limites de connexion mesurent, plutôt qu’une consultation par essai.
VRFY répond toujours 252 pour chaque adresse : RCPT indique déjà à un expéditeur si une adresse existe, comme sur tout MTA, et un second moyen, moins coûteux, de le demander ne servirait qu’à la collecte d’adresses.
85.1.1.1.3.12. Contrôle d’admission dans la file d’attente¶
pepsi-ingress cesse d’accepter du courrier tant que la file d’attente, ou le disque qui la porte, atteint une limite fixée dans [pepsi] (voir pepsi.conf(5)) :
MAX_QUEUE_ROWS— messages en file (par défaut un million de lignes de destinataire) ;MAX_QUEUE_BYTES— blocs d’en-têtes en file plus corps distincts (par défaut illimité) ;MIN_FREE_SPACE— espace libre sur le système de fichiers deFREE_SPACE_PATH(par défaut 1 Gio sur/var/lib/postgresql).
La réponse est 452 4.3.1 Insufficient system storage, un refus temporaire, donnée à RCPT, à DATA (ou au premier bloc BDAT, dont les octets sont tout de même lus afin que la session reste cadrée) et une fois encore après la fin des données, lorsque la taille propre du message est connue et qu’un message isolé qui franchirait MAX_QUEUE_BYTES ou MIN_FREE_SPACE est refusé à lui seul. Le MTA expéditeur conserve le message et réessaie pendant que la file déjà présente s’écoule ; le bridage se lève de lui-même. La submission locale est traitée de la même façon, et pepsi-sendmail(1) transforme le 452 en EX_TEMPFAIL, de sorte que cron réessaie.
La vérification est peu coûteuse : la file est mesurée (la fonction pepsi.queue_usage(), deux parcours séquentiels) au plus une fois par QUEUE_CHECK_INTERVAL (par défaut 10 s) pour l’ensemble du serveur, et chaque commande dans l’intervalle reçoit sa réponse d’après ce chiffre. pepsi-ingress n’a besoin pour cela d’aucun accès au courrier en file — seulement des colonnes de taille générées header_octets et workqueue_body.octets. Toute incertitude admet : une mesure échouée, ou un chemin de sonde qui n’existe pas (une base de données sur un autre hôte), laisse passer le courrier.
85.1.1.1.3.13. Protection contre l’engorgement¶
Plusieurs mécanismes empêchent un flot de connexions — y compris des clients qui complètent la poignée de main TCP puis deviennent silencieux sans jamais fermer — d’épuiser le serveur. Ils opèrent sur une plage source : une seule adresse IPv4 (un /32), ou le préfixe /32 d’une adresse IPv6. Les connexions locales (socket UNIX) forment à elles seules une plage, exemptée de la limite de débit et de l’éviction — un soumetteur local est un utilisateur de cette machine, et pepsi-sendmail ne doit pas être éconduit parce qu’un hôte distant est occupé — et bornée à la place par son propre plafond.
Timeouts par commande (RFC 5321 §4.5.3.2). Une session qui n’envoie pas sa commande suivante dans
COMMAND_TIMEOUT(par défaut 300 s), ou le bloc suivant d’un corpsDATA/BDATdansDATA_TIMEOUT(par défaut 180 s), est fermée avec un421. Le mêmeCOMMAND_TIMEOUTborne une poignée de mainSTARTTLS(ou TLS implicite) calée.Plafond de connexions avec éviction. Le serveur admet au plus
min(MAX_CONNECTIONS, MAX_OPEN_SOCKETS)connexions concurrentes.MAX_OPEN_SOCKETSprotège contre l’épuisement des descripteurs de fichiers ; lorsqu’il n’est pas défini, il est dérivé duRLIMIT_NOFILEsouple moinsRESERVED_SOCKETS(par défaut 64, retenus pour le pool de base de données, les listeners, stdio et les sockets DNS). Lorsque le plafond est atteint et qu’une nouvelle connexion arrive, le serveur fait de la place en fermant la connexion la plus longtemps inactive dans la plage dont le temps d’inactivité total est le plus grand (un421, puis fermeture), de sorte que la source la plus gourmande perde d’abord son créneau le plus inactif. « Inactif » est le temps qu’une connexion passe bloquée à attendre l’entrée du client entre les commandes — pas le temps passé à recevoir un corps ou à traiter.Plafond par plage. Une plage source peut détenir au plus
MAX_CONNECTIONS_PER_IP(par défaut 16) connexions à la fois, et l’ensemble des connexions locales au plusMAX_LOCAL_CONNECTIONS(par défaut 32). La limite de débit ci-dessous ne borne pas à elle seule la concurrence — une connexion par seconde, chacune maintenue occupée, remplit n’importe quel plafond en quelques minutes, et l’éviction ne récupère que les connexions inactives — et, sans le plafond local, un seul utilisateur local pourrait occuper tous les créneaux et affamer le SMTP distant. Une connexion au-delà de l’un ou l’autre est refusée comme une connexion limitée en débit.0désactive l’un ou l’autre plafond.
En outre, les nouvelles connexions acceptées sont limitées en débit par plage source par un seau à jetons : un taux de recharge CONN_RATE_PER_SECOND (par défaut 1) avec une rafale CONN_RATE_BURST (par défaut 1). Une connexion dépassant le débit reçoit un 421 au mieux puis est fermée, en occupant un créneau pendant la durée de cette écriture — délibérément, afin qu’un flot de connexions limitées en débit ne puisse pas engendrer un nombre illimité de tâches de rejet retenant chacune un descripteur hors de MAX_OPEN_SOCKETS. Lorsque la limite de débit se déclenche alors que le plafond de descripteurs est déjà atteint, aucun créneau n’est pris et la connexion est fermée sans aucune réponse, puisque l’écriture du 421 dépenserait elle-même le descripteur que l’on cherche à préserver. (Sur un listener en TLS implicite, le 421 de refus est de même omis, puisqu’aucun texte en clair ne peut précéder la poignée de main ; la connexion est simplement fermée.)
Au sein d’une session et d’une session à l’autre :
Messages par session. Une session peut démarrer au plus
MAX_MESSAGES_PER_SESSION(par défaut 100) transactions de courrier (RSETne remet pas le compte à zéro) ; leMAILsuivant reçoit421et la connexion est fermée, de sorte qu’un expéditeur ayant davantage à remettre se reconnecte — et la limite de débit le mesure.Boucles de routage. Un message qui est déjà passé
MAX_SELF_HOPSfois (par défaut 5) par l’ingress de cet hôte est refusé en fin de données avec554 5.4.6. Le compte provient des champsReceived:que l’ingress écrit lui-même. Le refus transforme une boucle — une adresse de notre propre domaine relayée vers notre propre MX, encore et encore — en un seul rebond par l’étape qui a tenté de la relayer.Devinette de mots de passe. Trois tentatives
AUTHéchouées ferment une session. D’une session à l’autre, une plage source peut échouerAUTHau plusAUTH_FAILURES_PER_HOUR(par défaut 20) fois, dans un seau à jetons se rechargeant à ce rythme ; une fois celui-ci épuisé, chaqueAUTHsupplémentaire venant de la plage reçoit421et la connexion est fermée sans que le backend SASL soit interrogé, jusqu’à ce que le seau se recharge.Débit de submission locale. Une session sur socket UNIX authentifiée par
AUTH_PEERCREDest limitée par uid, sur l’ensemble de ses sessions, àLOCAL_MESSAGES_PER_MINUTE(par défaut 60) messages avec une rafale deLOCAL_MESSAGE_BURST(par défaut 120). UnMAILau-delà du débit reçoit451 4.7.1, que pepsi-sendmail(1) transforme enEX_TEMPFAIL.
0 désactive n’importe lequel des trois. Les compteurs inter-sessions résident dans la mémoire du serveur, de sorte qu’un redémarrage les oublie.
85.1.1.1.3.14. Abandon de privilèges¶
Les ports SMTP (25/465/587) sont privilégiés, de sorte que pepsi-ingress est souvent démarré en tant que root. Il ne sert cependant pas en tant que root : une fois chaque listener configuré lié (et tout matériel de clé TLS lu), le processus abandonne ses privilèges vers le compte de service pepsi-ingress non privilégié, abandonnant tous les groupes supplémentaires et basculant vers le gid et l’uid de ce compte avant d’accepter la moindre connexion. Le compte doit donc exister avant que le serveur ne soit démarré en tant que root ; créez-le (par exemple useradd --system --no-create-home --shell /usr/sbin/nologin pepsi-ingress) dans le cadre de l’installation.
Si l’abandon ne peut être complété en s’exécutant en tant que root — le plus souvent parce que le compte pepsi-ingress n’existe pas — le serveur journalise une erreur et quitte sans servir, plutôt que de risquer de s’exécuter en tant que root. Lorsqu’il est démarré par un utilisateur non root, aucun changement de privilège n’est effectué (le serveur s’exécute simplement en tant que l’utilisateur invoquant) ; il ne s’exécutera jamais en tant que root.
85.1.1.1.3.15. Matériel de clés TLS¶
Les TLS_CERT/TLS_KEY de chaque listener MODE = tls/starttls sont lus une seule fois, au démarrage, avant l’abandon de privilèges ci-dessus.
Sous systemd, l’unit s’exécute dès le départ sous l’utilisateur non privilégié pepsi-ingress et ne détient jamais root ; elle ne peut donc pas ouvrir /etc/letsencrypt/{live,archive}, réservé à root par certbot. pepsi-setup(1) écrit par conséquent un drop-in /etc/systemd/system/pepsi-ingress.service.d/10-tls-credentials.conf comportant une entrée LoadCredential= par fichier : systemd les ouvre en tant que root au démarrage de l’unit et remet au service des copies privées sous $CREDENTIALS_DIRECTORY, que pepsi-ingress consulte avant de se rabattre sur la lecture du chemin configuré lui-même. Relancez pepsi-setup run après avoir ajouté, déplacé ou supprimé un certificat, afin que le drop-in soit régénéré. Comme les credentials sont matérialisés au démarrage de l’unit, un certificat renouvelé n’est servi qu’après un redémarrage — le hook de déploiement certbot installé par pepsi-setup s’en charge. Démarré directement (en tant que root, avec abandon des privilèges après la liaison des sockets), le serveur lit les fichiers lui-même et aucun credential n’intervient. Voir pepsi-httpd(1), qui décrit ce mécanisme en détail.
Contrairement à pepsi-httpd, qui sert de nombreux certificats et ignore celui qu’il ne parvient pas à charger, un listener n’en a ici qu’un seul : s’il est illisible, il n’y a rien sur quoi se rabattre, et le serveur sort plutôt que de n’offrir silencieusement aucun TLS sur un port que la configuration déclare chiffré.
85.1.1.1.4. Commandes¶
- serve
Lier chaque socket
[pepsi-ingress-listener-*]configurée et servir les connexions SMTP jusqu’à interruption. Nécessite que le schéma ait été installé avec pepsi-setup(1).
85.1.1.1.5. Options globales¶
Ces options globales précèdent la sous-commande (un indicateur placé à la fin est rejeté).
- -c FILE, –config FILE
Lit la configuration depuis FILE au lieu de parcourir les emplacements par défaut (voir FICHIERS).
- -L LOGLEVEL, –log LOGLEVEL
Règle la verbosité de journalisation. LOGLEVEL est l’un de
error,warn,info,debugoutrace(par défaut :info).- -v, –verbose
Affiche les messages de journal de toutes les sources, y compris les bibliothèques tierces qui sont filtrées par défaut.
- -h, –help
Affiche un résumé d’utilisation et quitte.
- -V, –version
Affiche la version et quitte.
85.1.1.1.6. Signaux¶
- SIGINT
Amorce l’arrêt : le processus cesse de servir et sort avec
0. Les sessions encore en cours ne sont pas drainées, et n’ont pas à l’être — un message n’est confirmé à son client (250) qu’une fois validé, de sorte qu’une session interrompue a soit son message en sécurité dans la file d’attente, soit n’a jamais reçu d’assurance du contraire, et le MTA expéditeur réessaie.- SIGTERM
Non capturé : la disposition par défaut met fin au processus, ce qui est sûr pour la même raison. C’est ce qu’envoie
systemctl stop pepsi-ingress.
85.1.1.1.7. Code de sortie¶
- 0
Achèvement réussi (y compris un arrêt propre de serve).
- 1
Une erreur s’est produite, par exemple un fichier de configuration malformé, l’échec d’une connexion à la base de données, ou un listener qui n’a pas pu être lié. La raison est écrite dans le journal.
85.1.1.1.8. Environnement¶
- XDG_CONFIG_HOME, HOME
Utilisé pour localiser le fichier de configuration par défaut lorsque –config n’est pas fourni (voir FICHIERS).
- LISTEN_FDS, LISTEN_PID, LISTEN_FDS_FIRST_FD
Honoré pour l’activation par socket de systemd. Un listener configuré avec
SERVE = systemdadopte le descripteur de fichier transmis par systemd à l’index donné par son optionFD_INDEX.LISTEN_FDS_FIRST_FDdonne le numéro du premier descripteur transmis et vaut3par défaut (SD_LISTEN_FDS_START) ; il est lu au bénéfice des superviseurs autres que systemd qui le définissent.LISTEN_FDNAMESn’est pas consulté : unFileDescriptorName=nomme les descripteurs de toute une unité.socketplutôt qu’un seulListenStream=, il ne pourrait donc pas distinguer ces listeners entre eux. La socket de soumission locale est trouvée autrement, par son adresse liée — voir ci-dessous.Le
pepsi-ingress.socketlivré en transmet quatre : les ports 25, 465 et 587, que les sections de listener revendiquent parFD_INDEX0, 1 et 2, et la socket de soumission locale/run/pepsi/submission.sock, revendiquée autrement — voir ci-dessous. Lier cette socket UNIX dans l’unité plutôt que dans le serveur est délibéré : systemd la crée en tant que root avant le démarrage du service, de sorte que lepepsi-ingressnon privilégié n’a jamais besoin d’un accès en écriture à/run/pepsi, un répertoire d’exécution quepepsi-httpdet la sonnette setup-apply déclarent aussi et que systemd réattribue à celui d’entre eux qui a démarré en dernier.La socket de soumission locale n’a aucun
FD_INDEXet aucune section de listener.pepsi-ingressla sert inconditionnellement (depuis[pepsi-sendmail] SOCKET) et trouve le descripteur en demandant au noyau laquelle des sockets transmises est liée à ce chemin. Un index serait une position parmi les entréesListenStream=de l’unité et se décalerait donc dès qu’un port SMTP est ajouté ou retiré ; l’adresse liée ne peut se désynchroniser de rien. Si aucun descripteur transmis ne correspond, le chemin est lié par le serveur à la place — ce qui est le cas hors systemd, et ce qui y rend fatal l’échec de la liaison.Tout autre descripteur transmis doit être revendiqué par une section de listener. Celui qui ne l’est pas est signalé par un avertissement au démarrage nommant son index, car systemd continue d’y accepter des connexions auxquelles rien ne répondra jamais — un client bloque alors jusqu’à l’expiration de son propre délai de lecture (deux minutes, pour pepsi-sendmail(1)) au lieu d’être refusé aussitôt. Les deux sont un ajournement, mais l’un d’eux fait caler l’appelant à chaque message. Si vous supprimez une section de listener, supprimez aussi son
ListenStream=de l’unité.- JOURNAL_STREAM
Lorsqu’elle est définie (c.-à-d. lors d’une exécution sous systemd), les horodatages sont émis en UTC sans décalage de fuseau horaire local.
85.1.1.1.9. Fichiers¶
Lorsque –config n’est pas fourni, le premier fichier existant de la liste suivante est utilisé :
$XDG_CONFIG_HOME/pepsi.conf$HOME/.config/pepsi.conf/etc/pepsi/pepsi.conf/etc/pepsi.conf
Chaque composant Pepsi partage le même fichier de configuration, c’est donc la même liste que chacun parcourt.
85.1.1.1.10. Exemples¶
Installer le schéma (une fois, avec pepsi-setup) et commencer à servir
pepsi-setup -c /etc/pepsi/pepsi.conf
pepsi-ingress -c /etc/pepsi/pepsi.conf serve
Vérifier pour quels domaines le courrier est accepté (avec pepsi-config(1))
pepsi-config -c /etc/pepsi/pepsi.conf get pepsi-ingress ACCEPTED_DOMAINS
Imprimer la configuration fusionnée avec les annotations de source
pepsi-config -c /etc/pepsi/pepsi.conf dump --diagnostics
85.1.1.1.11. Voir aussi¶
pepsi-config(1), pepsi.conf(5), pepsi-setup(1), pepsi-dispatch(1), pepsi-sendmail(1), pepsi.state(7), systemd.socket(5)
85.1.1.1.12. Bogues¶
Signalez les bogues au gestionnaire de tickets de Pepsi.