Relay SMTP interno con Postfix e Microsoft 365: guida completa (con bonus script)

Ovvero: come far parlare stampanti, AS/400 e altri dinosauri con il cloud, senza impazzire.


Indice

  1. Il problema (e perché non puoi ignorarlo)
  2. L’architettura della soluzione
  3. Prerequisiti
  4. Setup del server Postfix
  5. Configurazione del connettore M365
  6. Test e verifica
  7. Il tool mail-mini: diagnostica per esseri umani
  8. Manutenzione ordinaria
  9. Troubleshooting
  10. Considerazioni finali

1. Il problema (e perché non puoi ignorarlo)

Hai migrato la posta su Microsoft 365. Complimenti, benvenuto nel 2015. Ma ora hai un problema: tutti quei dispositivi e applicativi che mandavano mail allegramente verso il vecchio server SMTP interno non sanno cosa sia OAuth2, non supportano TLS 1.2, e alcuni non sanno nemmeno cosa sia una password.

Parlo di:

  • Stampanti multifunzione che scansionano e inviano via mail (scan-to-email)
  • Sistemi legacy tipo AS/400, ERP , gestionali con “SMTP server” come unico campo configurabile
  • Script e cron job su server Linux che usano sendmail o mail
  • Applicazioni interne che non verranno mai aggiornate perché “funziona, non toccarlo”
  • Dispositivi IoT e apparati di rete che mandano alert via mail

La soluzione Microsoft ufficiale? “Configura l’autenticazione SMTP con credenziali dedicate per ogni device”. Certo, e poi configuro anche 47 stampanti con username e password, pregando che nessuno cambi mai le credenziali. No grazie.

La soluzione intelligente: un relay SMTP interno che accetti mail dalla LAN senza autenticazione e le inoltri a M365 tramite un connettore dedicato.


2. L’architettura della soluzione

Il flusso è semplice:

  1. I device interni mandano mail al relay (porta 25, no auth, no TLS obbligatorio)
  2. Postfix verifica che il mittente sia in una subnet autorizzata (mynetworks)
  3. Postfix inoltra a M365 con TLS obbligatorio
  4. M365 accetta perché l’IP pubblico del relay è in whitelist nel connettore

Vantaggi:

  • Configurazione device: un solo campo (IP del relay)
  • Nessuna credenziale da gestire sui device
  • Logging centralizzato
  • Possibilità di fare troubleshooting senza accedere a 47 stampanti diverse

3. Prerequisiti

Lato infrastruttura

  • Una VM Linux (Ubuntu 24.04 LTS consigliato) con:
  • IP statico sulla LAN
  • Accesso in uscita verso internet (porta 25/TCP verso i server Microsoft)
  • Almeno 1GB RAM, 10GB disco (Postfix è leggerissimo)
  • L’IP pubblico attraverso cui il relay esce verso internet (per la whitelist M365)

Lato Microsoft 365

  • Accesso admin a Exchange Online
  • Un dominio verificato e accettato (nel nostro caso nomedelclinete.com)
  • La possibilità di creare connettori inbound

Lato DNS (opzionale ma consigliato)

  • Record SPF che includa l’IP pubblico del relay
  • Un record A o CNAME per il relay (es. smtp-relay.tuodominio.com)

4. Setup del server Postfix

4.1 Installazione

Su Ubuntu 24.04:

Durante l’installazione, seleziona “Internet Site” e inserisci il nome dominio (es. nomedelclinete.com).

Se hai già installato Postfix o vuoi riconfigurare:

sudo dpkg-reconfigure postfix

4.2 Configurazione main.cf

Ecco il cuore della configurazione. Sostituisci il contenuto di /etc/postfix/main.cf:

sudo nano /etc/postfix/main.cf
# ---------------------------
# POSTFIX RELAY → M365
# ---------------------------

# Identità server
myhostname = svr-smtprelay.tuodominio.com
myorigin = tuodominio.com
mydestination = localhost
inet_interfaces = all
inet_protocols = ipv4

# LAN autorizzate (CIDR) - aggiungi qui le tue subnet
mynetworks = 127.0.0.0/8, 192.168.1.0/24, 10.0.0.0/8

# Relayhost: gateway di Microsoft 365
# Sostituisci con il tuo MX record di protezione
relayhost = [tuodominio-com.mail.protection.outlook.com]:25

# TLS obbligatorio (Office 365 richiede connessione sicura)
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt

# Nessuna autenticazione SASL (usiamo IP-based via connettore M365)
smtp_sasl_auth_enable = no

# Regole per i destinatari
# permit_mynetworks: accetta da IP in mynetworks
# reject_unauth_destination: rifiuta tutto il resto (FONDAMENTALE: previene open relay)
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination

4.3 Spiegazione delle direttive chiave

DirettivaCosa faPerché è importante
myhostnameNome FQDN del serverAppare nei log e negli header delle mail
myoriginDominio di default per mail senza @dominioEvita mail da root@localhost
mydestination = localhostNon accettare mail in consegna localeSiamo un relay, non un mail server
mynetworksSubnet autorizzate a usare il relayIL MURO: solo queste reti possono inviare
relayhostDove inoltrare TUTTE le mailLe parentesi quadre disabilitano il lookup MX
smtp_tls_security_level = encryptTLS obbligatorio verso M365Microsoft rifiuta connessioni non cifrate
reject_unauth_destinationRifiuta relay verso destinazioni non autorizzateFONDAMENTALE: senza questo sei un open relay

4.4 Trovare il relayhost corretto

Il relayhost deve puntare al record MX di protezione del tuo tenant M365. Per trovarlo:

nslookup -type=MX tuodominio.com

Cerca il record che termina con .mail.protection.outlook.com. Esempio di output:

tuodominio.com  MX preference = 0, mail exchanger = tuodominio-com.mail.protection.outlook.com

Il tuo relayhost sarà: [tuodominio-com.mail.protection.outlook.com]:25

4.5 Applicare la configurazione

# Verifica sintassi
sudo postfix check

# Ricarica configurazione
sudo systemctl restart postfix

# Verifica che sia in ascolto
sudo ss -tlnp | grep :25

Output atteso:

LISTEN 0      100          0.0.0.0:25        0.0.0.0:*    users:(("master",pid=1234,fd=13))

5. Configurazione del connettore M365

Questa è la parte che permette a M365 di accettare mail dal tuo relay senza autenticazione SMTP.

5.1 Accesso all’admin center

  1. Vai su admin.exchange.microsoft.com
  2. Naviga in Mail flowConnectors

5.2 Crea un nuovo connettore

  1. Clicca + Add a connector
  2. Configura:
  • Connection from: Your organization’s email server
  • Connection to: Office 365
  1. Dai un nome descrittivo (es. Relay_NomeDelCliente o Relay_Interno)
  2. How to identify email sent from your email server: seleziona “By verifying that the IP address of the sending server matches one of these IP addresses”
  3. Inserisci gli IP pubblici del tuo relay (o range CIDR se hai più IP)

5.3 Esempio configurazione

Nel nostro caso il connettore Relay_NomeDelCliente è configurato così:

  • Name: Relay_NomeDelCliente
  • Status: On
  • From: Your organization’s email server
  • To: Office 365
  • IP addresses: 123.114.132.128/28, 132.228.123.204/30 ( quello che è l’ip di provenienza che avete settato sul firewall per la macchina)
  • Requirement: sender’s or recipient’s email address is an accepted domain

5.4 Considerazioni sulla sicurezza

  • Inserisci solo gli IP pubblici strettamente necessari
  • Se possibile, usa range CIDR stretti (es. /30 invece di /24)
  • Il requisito “accepted domain” evita che qualcuno usi il tuo relay per inviare mail a nome di domini che non controlli

5.5 SPF Record

Non dimenticare di aggiungere l’IP del relay al tuo record SPF:

v=spf1 include:spf.protection.outlook.com ip4:TUO.IP.PUBBLICO.QUI -all

6. Test e verifica

6.1 Test da riga di comando (dal relay stesso)

echo "Test dal relay SMTP interno" | mail -s "Test Relay" tuoindirizzo@tuodominio.com

6.2 Test con telnet/netcat (da un client nella LAN autorizzata)

nc svr-smtprelay 25

Poi digita manualmente:

HELO test.local
MAIL FROM:<test@tuodominio.com>
RCPT TO:<tuoindirizzo@tuodominio.com>
DATA
Subject: Test manuale

Corpo del messaggio di test.
.
QUIT

6.3 Verifica log in tempo reale

Sul server relay:

sudo tail -f /var/log/mail.log

Dovresti vedere qualcosa tipo:

postfix/smtp[12345]: ABC123DEF: to=<dest@example.com>, relay=tuodominio-com.mail.protection.outlook.com[52.101.x.x]:25, status=sent (250 2.0.0 OK)

6.4 Test di sicurezza: verifica che NON sia un open relay

Da un IP esterno alle mynetworks, prova a inviare. Deve fallire:

550 5.7.1 <dest@esterno.com>: Relay access denied

7. Il tool mail-mini: diagnostica per esseri umani

Grep sui log di Postfix funziona, ma dopo la terza volta che scrivi quel sed | awk | grep chilometrico, vuoi qualcosa di meglio. Ecco mail-mini.

7.1 Cosa fa

  • Visualizza la coda con comandi utili suggeriti
  • Ultime N consegne con timestamp, mittente, destinatario, IP sorgente, relay
  • Deferred e reject formattati per capire subito cosa non va
  • Ricerca per destinatario veloce
  • Top destinatari per giorno o storico

7.2 Installazione

Crea il file:

sudo nano /usr/local/bin/mail-mini

Incolla il contenuto dello script (vedi sotto) e rendi eseguibile:

sudo chmod +x /usr/local/bin/mail-mini

7.3 Uso

Menu interattivo (per chi preferisce i menu):

sudo mail-mini

Comandi diretti (per chi preferisce la CLI):

# Coda Postfix
sudo mail-mini queue

# Ultime 100 mail inviate
sudo mail-mini sent 100

# Ultimi 50 deferred
sudo mail-mini deferred 50

# Cerca mail per destinatario (ultime 500 righe di log)
sudo mail-mini search to utente@example.com 500

# Top destinatari di oggi
sudo mail-mini top today

7.4 Esempio di output

ULTIME 50 CONSEGNE (status=sent)
──────────────────────────────────────────────────────────────────────────────────
DATA/ORA             QUEUE-ID      CLIENT-IP        FROM                       TO                          RELAY
──────────────────────────────────────────────────────────────────────────────────
2025-03-25 10:15:32  A1B2C3D4E     172.10.4.55      scanner@tuodominio.com     utente@tuodominio.com       tuodominio-com.mail.protection.outlook.com[52.101.73.4]:25
2025-03-25 10:14:01  B2C3D4E5F     172.10.1.10      erp@tuodominio.com         amministrazione@tuodominio  tuodominio-com.mail.protection.outlook.com[52.101.73.4]:25

7.5 Il codice completo

Lo script è lungo ma ben commentato. Eccolo:
Clicca per espandere mail-mini (circa 250 righe bash)

#!/usr/bin/env bash
# mail-mini — Mini interfaccia CLI per Postfix (coda + storico dai log) con output leggibile
# - Timestamp corretta: supporta ISO8601 (YYYY-MM-DDTHH:MM:SS) e syslog classico (Mon dd HH:MM:SS)
# - Colonne allineate
# - IP di provenienza (client IP)
#
# Uso:
#   mail-mini                 # menu interattivo
#   mail-mini queue           # coda
#   mail-mini sent 100        # ultime 100 consegne
#   mail-mini deferred 50     # ultimi 50 deferred
#   mail-mini rejects 50      # ultimi 50 rifiuti
#   mail-mini search to user@example.com [MAX_LOG_LINES]
#   mail-mini top today|yesterday|all
#
set -o pipefail

LOG_YEAR="$(date +%Y)"
LOG_MONTH="$(date +%m)"

collect_plain_logs() {
  local files=""
  for f in /var/log/mail.log /var/log/mail.log.1; do
    [ -r "$f" ] && files+=" $f"
  done
  echo "$files"
}

collect_all_logs() {
  local files=""
  for f in /var/log/mail.log /var/log/mail.log.1 /var/log/mail.log.[2-9].gz; do
    [ -r "$f" ] && files+=" $f"
  done
  echo "$files"
}

PLAIN_LOGS="$(collect_plain_logs)"
ALL_LOGS="$(collect_all_logs)"
ZGREPOPTS="-h --no-group-separator"

need_tools() {
  for c in tac grep awk sed date postqueue postsuper zgrep; do
    command -v "$c" >/dev/null 2>&1 || { echo "Comando richiesto non trovato: $c"; exit 1; }
  done
}

rule() { printf '%s\n' "────────────────────────────────────────────────────────────────────────────────────────────────────────"; }
hdr()  { printf '\033[1m%s\033[0m\n' "$1"; rule; }

fmt_ts_line() {
  local line
  IFS= read -r line || return
  if echo "$line" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}'; then
    echo "$line" | sed -E 's/^([0-9]{4}-[0-9]{2}-[0-9]{2})T([0-9]{2}:[0-9]{2}:[0-9]{2}).*/\1 \2/'
  else
    printf '%s\n' "$line" | awk -v Y="$LOG_YEAR" -v M="$LOG_MONTH" '
      function mon2num(m){
        split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec",a," ");
        for(i=1;i<=12;i++) if(a[i]==m) return i;
        return 1
      }
      {
        mon=$1; day=$2; time=$3;
        if (length(day)==1) day="0"day;
        mn = mon2num(mon);
        year = Y;
        if (mn > M + 1) year = Y - 1;
        printf "%04d-%02d-%s %s\n", year, mn, day, time
      }'
  fi
}

extract_id()           { echo "$1" | sed -n 's/.* \([A-F0-9]\{7,\}\):.*/\1/p'; }
extract_from()         { echo "$1" | sed -n 's/.* from=<\([^>]*\)>.*/\1/p'; }
extract_to()           { echo "$1" | sed -n 's/.* to=<\([^>]*\)>.*/\1/p'; }
extract_relay()        { echo "$1" | sed -n 's/.* relay=\([^,]*\).*/\1/p'; }
extract_status()       { echo "$1" | sed -n 's/.* status=\([a-z][a-z]*\).*/\1/p'; }
extract_ip_from_line() { echo "$1" | sed -n 's/.*\[\([^]]*\)\].*/\1/p'; }

extract_client_ip_by_id() {
  local id="$1"
  [ -z "$id" ] && return
  [ -z "$PLAIN_LOGS" ] && return
  grep -h " $id: " $PLAIN_LOGS 2>/dev/null | grep -m1 'client=' | \
    sed -n 's/.*client=[^[]*\[\([^]]*\)\].*/\1/p'
}

format_sent() {
  local n="${1:-50}"
  hdr "ULTIME $n CONSEGNE (status=sent)"
  printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-30s\n" \
         "DATA/ORA" "QUEUE-ID" "CLIENT-IP" "FROM" "TO" "RELAY"
  rule
  [ -z "$PLAIN_LOGS" ] && { echo "Nessun file di log (plain) trovato."; return; }

  { for f in $PLAIN_LOGS; do tac "$f"; done; } 2>/dev/null | \
    grep 'status=sent' | head -n "$n" | \
    while IFS= read -r line; do
      ts="$(printf "%s\n" "$line" | fmt_ts_line)"
      id="$(extract_id "$line")"
      to="$(extract_to "$line")"
      relay="$(extract_relay "$line")"
      from="$(extract_from "$line")"
      if [ -z "$from" ] && [ -n "$id" ]; then
        from="$(grep -h " $id: " $PLAIN_LOGS 2>/dev/null | \
                 grep -m1 ' from=<' | sed -n 's/.* from=<\([^>]*\)>.*/\1/p')"
      fi
      ip="$(extract_client_ip_by_id "$id")"
      printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-30s\n" \
             "$ts" "$id" "${ip:-?}" "${from:-?}" "$to" "$relay"
    done
}

format_deferred() {
  local n="${1:-50}"
  hdr "ULTIMI $n DEFERRED (temporanei, Postfix ritenta)"
  printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-48s\n" \
         "DATA/ORA" "QUEUE-ID" "CLIENT-IP" "FROM" "TO" "MOTIVO"
  rule
  [ -z "$PLAIN_LOGS" ] && { echo "Nessun file di log (plain) trovato."; return; }

  { for f in $PLAIN_LOGS; do tac "$f"; done; } 2>/dev/null | \
    grep -E " status=deferred " | head -n "$n" | \
    while IFS= read -r line; do
      ts="$(printf "%s\n" "$line" | fmt_ts_line)"
      id="$(extract_id "$line")"
      to="$(extract_to "$line")"
      from="$(extract_from "$line")"
      if [ -z "$from" ] && [ -n "$id" ]; then
        from="$(grep -h " $id: " $PLAIN_LOGS 2>/dev/null | \
                 grep -m1 ' from=<' | sed -n 's/.* from=<\([^>]*\)>.*/\1/p')"
      fi
      reason="$(echo "$line" | sed -n 's/.*(\(.*\)).*/\1/p')"
      ip="$(extract_client_ip_by_id "$id")"
      printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-48s\n" \
             "$ts" "$id" "${ip:-?}" "${from:-?}" "$to" "$reason"
    done
}

format_rejects() {
  local n="${1:-50}"
  hdr "ULTIMI $n RIFIUTI (reject)"
  printf "%-19s  %-16s  %-40s  %-42s  %-18s  %-48s\n" \
         "DATA/ORA" "CLIENT-IP" "FROM" "TO" "CODICE" "MOTIVO"
  rule
  [ -z "$PLAIN_LOGS" ] && { echo "Nessun file di log (plain) trovato."; return; }

  { for f in $PLAIN_LOGS; do tac "$f"; done; } 2>/dev/null | \
    grep -E " reject|denied " | head -n "$n" | \
    while IFS= read -r line; do
      ts="$(printf "%s\n" "$line" | fmt_ts_line)"
      from="$(extract_from "$line")"
      to="$(extract_to "$line")"
      code="$(echo "$line" | sed -n 's/.* \(5\.[0-9]\+\.[0-9]\+\).*/\1/p')"
      reason="$(echo "$line" | sed -n 's/.*reject: \([^;]*\).*/\1/p')"
      ip="$(extract_ip_from_line "$line")"
      printf "%-19s  %-16s  %-40s  %-42s  %-18s  %-48s\n" \
             "$ts" "${ip:-?}" "$from" "$to" "$code" "$reason"
    done
}

search_to() {
  local addr="$1"
  local max_lines="${2:-50}"
  [ -z "$addr" ] && { echo "Uso: mail-mini search to user@example.com [MAX_LOG_LINES]"; exit 1; }

  hdr "RICERCA DESTINATARIO: $addr (analizzo ultime $max_lines righe di log)"
  printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-10s  %-40s\n" \
         "DATA/ORA" "QUEUE-ID" "CLIENT-IP" "FROM" "TO" "STATUS" "RELAY/MOTIVO"
  rule
  [ -z "$PLAIN_LOGS" ] && { echo "Nessun file di log (plain) trovato."; return; }

  { for f in $PLAIN_LOGS; do tac "$f"; done; } 2>/dev/null | \
    head -n "$max_lines" | \
    grep " to=<${addr}>" | grep 'status=' | \
    while IFS= read -r line; do
      ts="$(printf "%s\n" "$line" | fmt_ts_line)"
      id="$(extract_id "$line")"
      from="$(extract_from "$line")"
      to="$(extract_to "$line")"
      status="$(extract_status "$line")"
      if [ -z "$from" ] && [ -n "$id" ]; then
        from="$(grep -h " $id: " $PLAIN_LOGS 2>/dev/null | \
                 grep -m1 ' from=<' | sed -n 's/.* from=<\([^>]*\)>.*/\1/p')"
      fi
      if [ "$status" = "sent" ]; then
        extra="$(extract_relay "$line")"
      else
        extra="$(echo "$line" | sed -n 's/.*(\(.*\)).*/\1/p')"
      fi
      ip="$(extract_client_ip_by_id "$id")"
      printf "%-19s  %-12s  %-16s  %-40s  %-42s  %-10s  %-40s\n" \
             "$ts" "$id" "${ip:-?}" "${from:-?}" "$to" "$status" "$extra"
    done
}

top_recipients() {
  local range="$1" logs since=""
  case "$range" in
    today)
      logs="$PLAIN_LOGS"
      since="$(date '+%Y-%m-%d')"
      echo "ATTENZIONE: analizzo i log di oggi, potrebbe volerci qualche secondo..."
      ;;
    yesterday)
      logs="$PLAIN_LOGS"
      since="$(date -d 'yesterday' '+%Y-%m-%d')"
      echo "ATTENZIONE: analizzo i log di ieri, potrebbe volerci qualche secondo..."
      ;;
    all|"")
      logs="$ALL_LOGS"
      ;;
    *)
      logs="$ALL_LOGS"
      ;;
  esac

  hdr "TOP DESTINATARI ${range:-all} (solo consegne riuscite)"
  [ -z "$logs" ] && { echo "Nessun file di log trovato."; return; }

  if [ -n "$since" ]; then
    zgrep $ZGREPOPTS "status=sent" $logs 2>/dev/null | \
      while IFS= read -r line; do
        ts="$(printf "%s\n" "$line" | fmt_ts_line)"
        printf '%s|%s\n' "$ts" "$line"
      done | grep "^${since}" | \
      sed -n 's/.* to=<\([^>]*\)>.*status=sent.*/\1/p' | \
      awk '{c[$0]++} END{for(k in c) printf "%6d  %s\n", c[k], k}' | \
      sort -nr | head
  else
    zgrep $ZGREPOPTS "status=sent" $logs 2>/dev/null | \
      sed -n 's/.* to=<\([^>]*\)>.*status=sent.*/\1/p' | \
      awk '{c[$0]++} END{for(k in c) printf "%6d  %s\n", c[k], k}' | \
      sort -nr | head
  fi
}

print_queue() {
  hdr "CODA POSTFIX (postqueue -p)"
  postqueue -p
  printf '\nSuggerimenti:  %s  %s  %s\n' "postqueue -f" "postsuper -d <ID>" "postsuper -r <ID>"
}

menu() {
  need_tools
  while true; do
    echo
    echo "Mail MiniUI — scegli un'azione:"
    echo " 1) Vedi coda (postqueue -p)"
    echo " 2) Ultime 50 consegne (status=sent)"
    echo " 3) Ultimi 50 deferred"
    echo " 4) Ultimi 50 rifiuti (reject)"
    echo " 5) Cerca per destinatario (ultime 50 righe)"
    echo " 6) Top destinatari di oggi"
    echo " 7) Top destinatari di ieri"
    echo " 8) Flush coda (postqueue -f)"
    echo " 9) Esci"
    read -rp "Selezione: " ans
    case "$ans" in
      1) print_queue ;;
      2) format_sent 50 ;;
      3) format_deferred 50 ;;
      4) format_rejects 50 ;;
      5) read -rp "Inserisci destinatario (es. user@example.com): " r ; search_to "$r" 50 ;;
      6) top_recipients today ;;
      7) top_recipients yesterday ;;
      8) echo "Flush..." ; postqueue -f ;;
      9) exit 0 ;;
      *) echo "Scelta non valida." ;;
    esac
  done
}

case "$1" in
  queue)    need_tools; print_queue ;;
  sent)     need_tools; format_sent "${2:-50}" ;;
  deferred) need_tools; format_deferred "${2:-50}" ;;
  rejects)  need_tools; format_rejects "${2:-50}" ;;
  search)   need_tools; shift;
            if [ "$1" = "to" ]; then
              shift
              search_to "$1" "${2:-50}"
            else
              echo "Uso: mail-mini search to user@example.com [MAX_LOG_LINES]"
            fi ;;
  top)      need_tools; top_recipients "${2:-all}" ;;
  "" )      menu ;;
  *)        echo "Uso: mail-mini [queue|sent [N]|deferred [N]|rejects [N]|search to <addr> [MAX_LOG_LINES]|top [today|yesterday|all]]"; exit 1 ;;
esac

8. Manutenzione ordinaria

8.1 Aggiungere una rete trusted

  1. Backup prima di tutto:
   sudo cp -a /etc/postfix/main.cf /etc/postfix/main.cf.bak.$(date +%F_%H%M)
  1. Modifica main.cf:
   sudo nano /etc/postfix/main.cf
  1. Aggiungi la subnet a mynetworks in formato CIDR:
   mynetworks = 127.0.0.0/8, 192.168.1.0/24, 192.168.2.0/24, 10.0.0.0/8
  1. Ricarica Postfix:
   sudo systemctl restart postfix

8.2 Verificare lo stato

# Stato del servizio
sudo systemctl status postfix

# Coda attuale
sudo postqueue -p

# Oppure con mail-mini
sudo mail-mini queue

8.3 Forzare l’invio della coda

sudo postqueue -f

8.4 Eliminare un messaggio dalla coda

sudo postsuper -d QUEUE_ID

8.5 Bonus: pfqueue per gestione visuale

Se preferisci un’interfaccia TUI:

sudo apt install pfqueue
sudo pfqueue

Tasti utili: f flush, d delete, q quit.


9. Troubleshooting

9.1 Log in tempo reale

sudo tail -f /var/log/mail.log

9.2 Cercare errori per un orario specifico

# Tutte le righe delle 12:23:20 con contesto
grep -A3 -B3 "12:23:20" /var/log/mail.log

9.3 Errori comuni

SintomoCausa probabileSoluzione
Relay access deniedIP non in mynetworksAggiungi la subnet del mittente
Connection timed outFirewall blocca porta 25 in uscitaVerifica regole firewall/NAT
Certificate verification failedProblema certificati CAsudo update-ca-certificates
Host not foundRelayhost erratoVerifica MX record con nslookup
Mail consegnata ma non arrivaConnettore M365 non configuratoVerifica IP whitelist nel connettore
454 4.7.0 TLS not availableM365 non accetta la connessioneVerifica smtp_tls_security_level = encrypt

9.4 Test connessione TLS verso M365

openssl s_client -connect tuodominio-com.mail.protection.outlook.com:25 -starttls smtp

Deve mostrare il certificato Microsoft e la connessione TLS stabilita.


10. Considerazioni finali

Sicurezza

  • Mai rimuovere reject_unauth_destination — è quello che ti protegge dall’essere un open relay
  • Limita mynetworks alle sole subnet che ne hanno davvero bisogno
  • Firewall: blocca la porta 25 dall’esterno, solo la LAN deve raggiungerla

Perché Postfix e non sendmail/exim/altro?

  • Postfix è leggero, veloce, e ha una configurazione comprensibile da esseri umani
  • È il default su Ubuntu e ben mantenuto
  • La documentazione è eccellente

Evoluzione possibile

  • Aggiungere la generic map per riscrivere mittenti fantasiosi (tipo root@localhost) — il file c’è già, basta attivarlo
  • Monitoraggio con Zabbix/Prometheus: esporta metriche dalla coda Postfix
  • Rate limiting se qualche device impazzisce e inizia a sparare mail

Replicare su altre sedi

La stessa configurazione funziona identica su altre sedi. L’unica differenza è:

  • IP diverso in mynetworks per le reti locali
  • IP pubblico diverso nel connettore M365

Nel nostro caso abbiamo un relay identico anche sull’infrastruttura secondaria del cliente (172.19.1.15) con lo stesso setup.


Articolo scritto nel 2026 per riccardorenda.it — perché documentare è importante, soprattutto quando parli con te stesso del futuro.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Torna in alto