Un cliente doveva inviare comunicazioni agli iscritti di un’organizzazione. Il software che prepara gli invii è sviluppato internamente da loro: per ogni destinatario genera un messaggio e lo consegna a un server tramite SMTP. Per noi è un client SMTP come un altro, quindi il lavoro stava tutto nel server a cui consegna.
I vincoli erano due. Il primo riguardava i dati: gli iscritti hanno informazioni sensibili, e un servizio commerciale di invio massivo avrebbe visto indirizzi e contenuti. Il secondo era la reputazione del dominio: migliaia di messaggi in pochi minuti da un solo IP sono il modo più rapido per finire in una blacklist o nelle code di rallentamento dei grandi provider.
Abbiamo risolto con software open source che conosciamo bene e tre indirizzi pubblici usati a turno. L’articolo descrive come è fatto il sistema, cosa abbiamo configurato sul DNS e come controlliamo che le code non si fermino. Indirizzi, domini e porte negli esempi sono di fantasia.
Il server di partenza: iRedMail con un relay al posto dell’invio diretto
Come base abbiamo scelto iRedMail. È open source, semplice da installare e ben supportato, e lo conosciamo a fondo: è anche il mail server dietro la posta di anjara.org. Gli utenti sono pochissimi, solo le persone e i programmi autorizzati a inviare. Il software del cliente si autentica su questo server e gli consegna i messaggi, uno per destinatario.
La prima modifica a Postfix è stata togliere l’invio diretto. Il server non contatta i server dei destinatari: passa tutto a un relay indicato con relayhost.
relayhost = [10.0.0.10]:25
La seconda riguarda il ritmo con cui Postfix consegna al relay. Il flusso è continuo, ma ogni connessione viene riusata per un numero limitato di messaggi e poi chiusa.
smtp_connection_reuse_count_limit = 20
smtp_destination_concurrency_limit = 10
Il primo parametro produce blocchi di circa 20 messaggi: Postfix conta i riusi della connessione, quindi il blocco può arrivare a 21, differenza che per noi non conta. Il secondo limita a dieci le consegne in parallelo verso il relay.
Il relay: HAProxy davanti a tre server SMTP
L’indirizzo 10.0.0.10 è un bilanciatore HAProxy, non un server di posta. Lavora in modalità TCP, quindi non interpreta il protocollo SMTP e inoltra soltanto la connessione, distribuendola in round robin su tre backend.
frontend smtp_relay
bind 10.0.0.10:25
mode tcp
default_backend smtp_out
backend smtp_out
mode tcp
balance roundrobin
server smtp1 192.0.2.11:2525 check
server smtp2 192.0.2.12:2525 check
server smtp3 192.0.2.13:2525 check
Poiché Postfix chiude la connessione a fine blocco, quella successiva arriva ad HAProxy come nuova sessione e finisce su un backend diverso. Il primo blocco esce dal primo server, il secondo dal secondo, il terzo dal terzo, poi si ricomincia.
I tre backend sono container LXC con solo Postfix, ospitati su tre hypervisor Proxmox diversi. Uno è lo stesso che ospita il server iRedMail e il bilanciatore, gli altri due sono esterni. Ogni container esce con un IP pubblico distinto, e nessun indirizzo da solo riceve tutto il volume.
Porte non standard e accesso ristretto
I tre server ascoltano su porte non standard per due ragioni. Una riguarda la sicurezza: si riducono le scansioni automatiche sulla porta 25. L’altra è pratica: avevamo pochi IP pubblici, e lasciare libere le porte standard permette di usare gli stessi indirizzi per altri servizi.
L’accesso è limitato al bilanciatore in due modi che si sommano. Il firewall lascia passare solo l’IP di HAProxy verso la porta del servizio, e in Postfix mynetworks contiene solo quell’indirizzo.
mynetworks = 10.0.0.10/32
DNS: far riconoscere tre IP come un solo mittente
Con tre indirizzi pubblici in uscita, i server che ricevono vedono la stessa posta arrivare da IP diversi. Il DNS serve a dire loro che sono tutti legittimi. Il dominio è gestito interamente da noi, quindi tutti i record stanno nella stessa zona.
SPF
Un solo record contiene gli IP dei tre backend:
example.org. IN TXT "v=spf1 ip4:203.0.113.11 ip4:203.0.113.12 ip4:203.0.113.13 -all"
Vanno elencati i tre server SMTP, perché sono l’ultimo passaggio prima della consegna e quindi gli indirizzi che il destinatario controlla. L’IP del server iRedMail non serve: da lì la posta non esce verso l’esterno. Il meccanismo ip4 non richiede interrogazioni DNS aggiuntive, quindi il limite di dieci lookup previsto da SPF non entra in gioco. Il record deve restare unico, perché due record SPF sullo stesso nome fanno fallire il controllo.
DKIM
La firma avviene sul server principale, prima che il messaggio arrivi al relay. La chiave privata sta in un solo posto e la chiave pubblica si pubblica una sola volta:
mail._domainkey.example.org. IN TXT "v=DKIM1; k=rsa; p=<chiave pubblica>"
La firma resta valida qualunque sia il backend da cui il messaggio esce, purché i tre Postfix non modifichino corpo e intestazioni firmate. Nel nostro caso aggiungono solo la riga Received, che non rientra nella firma. Sui backend non c’è quindi nessuna configurazione DKIM da mantenere.
DMARC
Il dominio ha una policy quarantine:
_dmarc.example.org. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.org"
Un messaggio supera DMARC se almeno uno tra SPF e DKIM passa il controllo ed è allineato al dominio del mittente. Qui passano entrambi: SPF perché il backend da cui esce è nell’elenco, DKIM perché la firma viaggia con il messaggio. Che gli IP cambino a ogni blocco non influisce sull’esito, perché nessuno dei due controlli dipende da un IP singolo.
Il parametro rua indica dove i destinatari mandano i report aggregati. Servono a vedere cosa succede dopo la consegna: se un backend finisse fuori dal record SPF, i suoi messaggi passerebbero comunque grazie a DKIM e l’errore resterebbe invisibile, mentre nei report comparirebbe come un IP che fallisce SPF.
Reverse DNS e HELO
Ogni IP pubblico ha il PTR assegnato all’hypervisor che lo ospita. I container non hanno un reverse proprio: si presentano nell’HELO con lo stesso nome che risulta dal PTR, impostato in myhostname.
# server smtp1 (IP 203.0.113.11)
myhostname = pve1.hosting-a.example
I grandi provider controllano che tre cose coincidano: il PTR dell’IP, il nome dichiarato nell’HELO e il record A di quel nome, che deve puntare di nuovo all’IP. Qui coincidono per costruzione, perché il nome nell’HELO è quello del PTR. I tre nomi sono diversi tra loro e non appartengono al dominio del mittente, ma SPF e DMARC guardano l’IP e il dominio dell’indirizzo mittente, non il nome dell’HELO.
Un solo caso fa eccezione. I messaggi di rimbalzo hanno mittente vuoto, e per quelli alcuni server controllano l’SPF sul nome dell’HELO. In quel caso conta il record SPF del dominio dell’hosting, non il nostro. Con un invio da uno a molti i rimbalzi sono pochi, ma conviene saperlo.
Monitoraggio delle code
Tralasciamo i controlli di base sui server, uguali a quelli di qualsiasi sistema. Quello che riguarda la posta si concentra sulla coda deferred di Postfix, dove finiscono i messaggi che non si riesce a consegnare subito e che il server riprova più tardi. Una coda deferred che cresce è il segnale più diretto di un problema: un provider che rallenta o rifiuta, un IP in blacklist, un guasto di rete.
Metriche e allarme
Un exporter per Prometheus espone le metriche della coda su una semplice pagina web. La pagina è letta da due sistemi: Prometheus, per lo storico, e Zabbix, con un controllo HTTP seguito dal parsing del contenuto. Zabbix genera un allarme quando la coda deferred di un server SMTP supera i 100 messaggi. La soglia vale per ogni server, non per il totale.
Messaggi fermi per dominio
Uno script Python legge la coda deferred e produce un file JSON diviso per dominio, che pubblica su una pagina web. Grafana lo legge direttamente e disegna il grafico dei messaggi fermi per dominio. Il totale dice che c’è un problema, il grafico dice con chi: se i messaggi si accumulano verso un solo provider, il problema riguarda i limiti o la reputazione presso quel provider, se sono sparsi su molti domini si cerca altrove.
Pulizia giornaliera
Un secondo script gira una volta al giorno e toglie dalla deferred i messaggi che resterebbero comunque lì, come quelli diretti a caselle piene o a domini inesistenti. Senza questo passaggio si accumulerebbero fino a superare la soglia dei 100, e l’allarme suonerebbe per qualcosa che non richiede intervento.
Sviluppi futuri
Restano due punti fuori da quanto descritto finora.
Il primo è in sviluppo. Quando la coda deferred di un server SMTP supera i 100 messaggi, il server viene tolto da HAProxy e non riceve più nuovi blocchi. Resta acceso, così Postfix può smaltire la coda con i suoi tentativi di consegna. Il rientro ha una soglia più bassa di quella di uscita: il server torna in HAProxy da solo quando la coda scende sotto i 50 messaggi, e il margine evita che entri e esca di continuo se la coda oscilla intorno a 100. Se ne rimane attivo uno solo, scatta un allarme su Zabbix e si interviene a mano. In futuro potremmo aggiungere un controllo che impedisca del tutto di disattivare l’ultimo server.
Il secondo è ancora un’idea: un controllo periodico dei tre IP pubblici sulle principali blacklist. Oggi un indirizzo listato si nota solo in modo indiretto, quando i messaggi verso certi domini iniziano ad accumularsi in deferred.
