Entrambe le modalità di connessione trasportano lo stesso messaggio verso lo stesso account di relay, e la differenza sta in dove inizia TLS, non in ciò che viene inviato. Una si apre in testo in chiaro e si aggiorna all’interno della connessione; l’altra è cifrata dal primo byte. Questa unica differenza decide cosa accetterà una politica di relay rigorosa, cosa può osservare un dispositivo intermedio e quale messaggio di errore passerete un pomeriggio a leggere. Ecco come scelgo tra le due quando un’applicazione su una rete privata consegna un messaggio a un servizio di invio interno.

Dove le due modalità divergono davvero

Il compito è identico in entrambi i casi: autenticarsi come account approvato, indicare un mittente consentito, elencare i destinatari e attendere che il relay si assuma la responsabilità. Ciò che cambia è l’ordine delle operazioni. La porta 587 inizia come sessione SMTP in testo in chiaro, annuncia STARTTLS nella risposta EHLO e si aggiorna solo se il client lo chiede. La porta 465 attende un handshake TLS prima che venga pronunciato qualsiasi comando SMTP.

Invio e relay sono lavori diversi, per questo i numeri di porta non sono abitudini intercambiabili. RFC 6409 descrive l’invio dei messaggi come servizio a sé, e RFC 8314 copre la variante TLS implicita che la maggior parte dei relay moderni espone sulla 465. Se il vostro relay documenta una delle due, quella documentazione prevale su qualsiasi preferenza acquisita presso un precedente datore di lavoro.

Che cosa vi dà STARTTLS sulla 587

L’aggiornamento avviene all’interno della conversazione SMTP, quindi il relay può annunciarlo e il client può rifiutarsi di proseguire senza di esso. In Python, starttls() solleva un’eccezione quando il server non offre l’estensione, il che trasforma un tentativo di downgrade in un errore visibile invece che in un accesso silenzioso in chiaro. Questo comportamento è l’argomento principale della modalità, e vale la pena affidarvisi deliberatamente e non per caso.

La finestra di esposizione è piccola ma reale: lo scambio EHLO iniziale è leggibile, e nulla prima dell’aggiornamento trasporta credenziali. Su un percorso privato tra una sottorete applicativa e un relay interno, quella finestra è di solito accettabile. Su un percorso che non controllate, è il motivo per cui alcuni team insistono sull’altra modalità.

Un dispositivo di intercettazione che rimuove la capacità STARTTLS produce un invio fallito, non un ripiego. È il risultato che volete, ed è anche il modo più rapido per scoprire che qualcosa riscrive SMTP sul percorso.

Che cosa cambia il TLS implicito sulla 465

Non esiste alcuna fase in chiaro. Il client esegue un handshake TLS non appena si apre il socket, convalida il certificato rispetto al proprio archivio di fiducia e solo allora scambia EHLO, AUTH e il messaggio. Nulla può effettuare il downgrade della sessione perché non esiste uno stato in chiaro da cui negoziare, e questo elimina un’intera classe di errori di intercettazione.

Il compromesso è la diagnosticabilità. Un relay che risponde sulla 465 con SMTP in chiaro, o un bilanciatore di carico che termina TLS con il nome del certificato sbagliato, fallisce all’handshake e vi dà un errore SSL invece di un errore di posta. Quando accade, il problema è sotto l’applicazione, e nessuna costruzione del messaggio lo risolverà.

Una funzione di invio, due trasporti

Il codice differisce appena tra le modalità, ed è per questo che la scelta va fatta in base alla politica del relay e non alla comodità della libreria. Esportate prima le impostazioni, poi eseguite un unico script che seleziona il trasporto in base alla porta configurata.

export SMTP_HOST=smtp.relay.internal
export SMTP_PORT=587
export SMTP_USER=svc-notify
export [email protected]
export [email protected]
export SMTP_PASSWORD="$(cat /run/secrets/smtp_password)"
import os, smtplib, ssl
from email.message import EmailMessage

ctx = ssl.create_default_context()
msg = EmailMessage()
msg["From"] = os.environ["MAIL_FROM"]
msg["To"] = os.environ["MAIL_TEST_TO"]
msg["Subject"] = "relay submission test"
msg.set_content("One authorized test message from the app.")

host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))

if port == 465:
    client = smtplib.SMTP_SSL(host, port, context=ctx, timeout=15)
else:
    client = smtplib.SMTP(host, port, timeout=15)
    client.ehlo()
    client.starttls(context=ctx)
    client.ehlo()

with client:
    client.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    refused = client.send_message(msg)

print("refused:", refused)

create_default_context() verifica la catena e il nome host, quindi una CA privata va installata nell’archivio di fiducia invece di essere aggirata con un contesto non verificato. send_message() restituisce un dizionario dei destinatari che il relay ha rifiutato; un dizionario vuoto significa che li ha accettati tutti. Il timeout di quindici secondi è un punto di partenza, non un valore tarato per la vostra rete.

Tenete la password fuori dall’immagine e fuori dai log. Un segreto montato a runtime e letto nell’ambiente va bene; fare eco dell’ambiente in una riga di log non va bene, e nemmeno una traccia di debug dello scambio AUTH.

Scegliete in base alla politica del relay e al percorso, non all’abitudine

Tre condizioni lo decidono. Prima, ciò che il relay annuncia davvero: un servizio che ascolta solo sulla 587 con STARTTLS non è raggiungibile sulla 465, per quanto preferiate il TLS implicito. Seconda, se il percorso tra l’applicazione e il relay è abbastanza fidato da accettare uno scambio EHLO leggibile. Terza, se qualcosa nel mezzo riscrive o fa proxy di SMTP, perché questo decide se il rischio di downgrade è teorico o reale.

Quando il relay offre entrambe, prendo la 465 per un servizio che gira senza supervisione e la 587 per uno in cui una persona segue il primo rilascio, solo perché i messaggi di errore puntano a livelli diversi. Questa preferenza è mia e non è una politica.

Qualunque cosa scegliate, annotate la porta, il materiale di fiducia e il dominio del mittente insieme al rilascio. Poi inviate un messaggio di prova autorizzato e confermate che il relay lo ha accettato, conservando il risultato dell’invio e ogni successivo rapporto di consegna come registrazioni separate. Una consegna riuscita al relay è la fine della responsabilità dell’applicazione, non la prova che il messaggio abbia raggiunto una casella di posta.

Articoli correlati