./run quando-autenticare-non-vuol-dire-fidarsi
Lo spam che passava DMARC: quando autenticare non vuol dire fidarsi
Ho migrato da zero un mail server self-hosted con Postfix, Dovecot e Rspamd. La lezione più dura non era TLS o DKIM: era una whitelist che dava -7 a chiunque passasse DMARC.
- status
- draft
- project
- antilia-newserver
- updated
- 2026-07-07
- tags
Rebuild di un mail server di produzione da zero: Postfix, Dovecot, Rspamd, tutta l’autenticazione email (SPF, DKIM, DMARC, DANE), WireGuard, DNS. Poi un runbook da 10 capitoli così da non doverlo più ricordare a memoria. La spina di questo diario, però, non è nessuno di quei pezzi presi singolarmente. È una riga di config che sembrava perfetta e che invece faceva entrare lo spam in inbox — passando dalla porta principale, con tanto di firma valida.
Il problema
Un vecchio server (example.com, chiamiamolo così) andava dismesso. Doveva rinascere identico su una macchina nuova (<MAILHOST>): stesso stack, stessa versione di Postfix (3.7.11) e Dovecot (2.3.19.1), 9 caselle su 3 domini locali più 9 domini in solo-relay. Migrazione fatta in una notte: rsync --checksum delle mailbox (34G il dominio più grosso), cutover DNS, spegnimento del vecchio.
La parte tecnica interessante non è il “come spostare la posta”. È l’autenticazione. Un mail server self-hosted è utile solo se gli altri si fidano di lui — e se lui non si fida di chiunque bussi. Su questo mi ero convinto di aver fatto tutto bene: SPF con -all, DKIM RSA-2048 su tutti i domini, DMARC in quarantine. E invece.
L’architettura, in breve
Niente Amavis, niente SpamAssassin: il filtraggio è interamente Rspamd, collegato a Postfix come milter.
# Postfix main.cf
smtpd_milters = inet:localhost:11332
milter_protocol = 6
milter_default_action = accept
Dietro Rspamd, quattro istanze Redis dedicate — non una:
| Porta | Ruolo | Persistence |
|---|---|---|
| 6381 | history, DMARC, ratelimit, greylist | RDB |
| 6382 | classificatore Bayes | RDB + AOF |
| 6383 | fuzzy hash (expire 90g) | RDB |
| 6384 | reputation IP/SPF/DKIM/dominio | RDB |
La separazione non è pignoleria: Bayes impara nel tempo, e un FLUSHDB sull’istanza sbagliata ti cancella mesi di training. Bayes è l’unico con appendonly yes — è l’unico dato che deve sopravvivere a un crash; gli altri possono permettersi di perdere qualche minuto.
La spina: -7 a chiunque
Analizzando il flusso post-migrazione, trovo spam evidente che finiva in inbox con score negativo. La causa era questa regola, ereditata e apparentemente sensata:
# whitelist.conf — SBAGLIATO
WHITELIST_DMARC {
valid_dmarc = true;
score = -7.0; # -7 a CHIUNQUE passi DMARC
}
Il ragionamento ingenuo: “se una mail passa DMARC è autenticata, quindi fidati”. Falso. DMARC “pass” garantisce una cosa sola: che la mail proviene davvero dal dominio che dichiara. Non dice nulla sul fatto che quel dominio sia legittimo. Uno spammer moderno registra spam-del-giorno.tld, gli configura SPF e DKIM validi del proprio dominio, e da quel momento passa DMARC in scioltezza. Con quella regola, si beccava -7 e atterrava dritto in casella.
Il fix è passare da “fidati di chi è autenticato” a “fidati di chi è autenticato e nella mia lista”:
# whitelist.conf — CORRETTO
ISPC_WHITELIST_DMARC {
valid_dmarc = true;
domains = [ "$LOCAL_CONFDIR/local.d/maps.d/dmarc_whitelist.inc" ];
score = -7.0;
inverse_symbol = "ISPC_BLACKLIST_DMARC";
}
Il campo domains = [...] restringe il bonus ai soli domini davvero fidati — clienti, partner, il Workspace/M365 dell’utente. Senza la mappa, nessuna penalità negativa viene concessa. In alternativa, se non vuoi gestire mappe: pesi piccolissimi (-0.5 per SPF valido, -1.0 per SPF+DKIM) che premiano l’autenticazione senza mai bastare da soli a salvare uno spam.
E già che c’ero, ho ritarato i pesi DMARC verso il basso, dove il default di Rspamd è troppo timido: DMARC_POLICY_QUARANTINE da 1.5 a 3.5. Con 1.5, uno spam che passa il proprio DMARC veniva penalizzato di due nulla. E le soglie d’azione: add_header da 6 a 4, rewrite_subject da 8 a 6 — due punti di margine perché il Bayes contribuisca prima del punto di dolore, non dopo.
Gotcha onesti
“Versione ≠ capacità.” Questa me la sono scritta a caratteri cubitali. I due nodi (mailer principale e relay) avevano config Rspamd “identiche” ma versioni diverse — 4.0.1 contro 3.6. Il modulo reputation di Rspamd 4.0 emette nomi di simbolo diversi dalla 3.6, e i log si riempivano silenziosamente di:
unknown symbol IP_REPUTATION_HAM_SPAM
unknown symbol URL_REPUTATION
Simbolo emesso a runtime ma non dichiarato in groups.conf = score perso senza un solo errore visibile. Config simmetrica non è funzionalità simmetrica: l’unica prova è il dump dei simboli runtime (/symbols via HTTP API), non un diff dei file. Da qui uno script di audit in cron su entrambi i nodi.
Il bug che configtest non vede. Nel modulo replies avevo password 'xxx' senza =. rspamadm configtest diceva syntax OK. Il modulo però falliva silenziosamente la connessione a Redis. Sempre password = "xxx" con l’uguale e le virgolette.
Il carattere sbagliato che nessuno segnala. Anni di client diversi lasciano cartelle IMAP duplicate: Sent + Inviati + Inviata, e il mio preferito, Cestino e Cestino — con uno spazio finale. Sono due mailbox distinte per IMAP. Le scovi solo con doveadm mailbox list -u $U | cat -A | grep -i cestino: se vedi Cestino $, c’è lo spazio.
Come va
Shipped. Migrazione completata, vecchio server spento, DNS ripuliti da ogni riferimento al vecchio IP. Tutti i test end-to-end verdi: STARTTLS su 25 in TLSv1.3, submission 587, IMAPS 993 con SNI multi-dominio (ogni dominio serve il suo certificato), DKIM che firma, Received-SPF: Pass, coda vuota. TLS minimo forzato a 1.2 (in pratica i client negoziano sempre 1.3), DANE in uscita (smtp_tls_security_level = dane con smtp_dns_support_level = dnssec), certificati via dehydrated con challenge DNS-01 su Cloudflare — rinnovo automatico ogni notte alle 3:00, nessuna porta 80/443 da esporre.
Una cosa che il runbook dichiara ma che non è ancora chiusa, per onestà: l’allineamento major di Rspamd tra i due nodi. Il relay è bloccato su 3.6 perché ha un OS ibrido (userland bookworm con kernel e libbinutils da trixie) e il pacchetto 4.x non si installa in nessuna delle due direzioni senza prima sanare l’OS. Documentato come pendente, non nascosto.
Cosa ho imparato
- Autenticare non è fidarsi. SPF/DKIM/DMARC provano l’origine, non la legittimità. Ogni bonus basato solo sull’autenticazione, senza una lista, è una porta aperta.
- Il silenzio è il nemico. Il bug peggiore non è quello che urla nei log: è quello che li lascia puliti mentre ti perde punteggio (
passwordsenza=, simboli sconosciuti dopo un upgrade major). L’unica difesa è verificare il runtime, non la config. - Un runbook vale la migrazione stessa. Rifare a memoria 4 istanze Redis, SNI multi-dominio, pesi Rspamd tarati e hook DNS-01 è impossibile. Il valore vero non è il server nuovo: è il documento che rende il prossimo server un pomeriggio invece di una settimana.