An office scanner sitting in someone else's building needs an SMTP server it can reach from the public internet. The version that takes ten minutes, no authentication, is an open relay, and open relays are found by scanners within hours. The cost is not embarrassment, it is your domain landing on blocklists that take weeks to leave.
Port 25 is for server-to-server delivery. You are not a mail server, you are accepting mail from one device that can authenticate, which is what port 587 exists for.
Remove the smtp inet line from Postfix's master.cf entirely rather than blocking
it at the firewall. A rule can be edited by someone who does not know why it exists. A service that was never
started cannot be exposed by a firewall change:
# master.cf — submission only, no plain smtp listener at all
submission inet n - y - - smtpd
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
That last line is the whole safety property. Authenticated senders may relay. Everything else is refused, full stop, with no clever exceptions for local networks.
Postfix delegates authentication, and Dovecot is the usual backend. You do not need any of Dovecot's mail features, so do not enable them. No IMAP, no POP3, no mailboxes. One account in a password file with a hashed password, used only to prove the scanner is the scanner:
doveadm pw -s SHA512-CRYPT # generate the hash for the passwd-file
Forward only 587 at your router. Nothing on this host should be reachable unauthenticated.
Use a dedicated sending subdomain rather than your root domain. The DNS records for authenticating a sender then live entirely in that subdomain, and a mistake cannot affect mail for the domain people actually write to you at.
Keep the From address inside that subdomain too, so alignment is clean without further work.
The host does not need to serve HTTP, and opening port 80 for a challenge would be adding exposure to remove exposure. Use the DNS challenge with an API token scoped to that one zone, and add a deploy hook so renewal actually reaches Postfix:
/etc/letsencrypt/renewal-hooks/deploy/postfix-reload.sh # systemctl reload postfix
Without the hook you get a fresh certificate on disk and a daemon still serving the old one, which fails ninety days later in a way nobody connects to the renewal.
libsasl2-modules. The error is no mechanism available, and it is
confusing because inbound authentication is already working fine. Those are two different SASL
implementations, and only one of them ships by default.Anyone can confirm a scan arrived. The test that matters is that an unauthenticated stranger is rejected, and it should be part of your routine checks, not a one-off:
# expect a 554 refusal, not an accepted recipient
swaks --to [email protected] --from [email protected] \
--server mail.example.com --port 587 --tls --quit-after RCPT
# certificate still valid?
echo | openssl s_client -starttls smtp -connect mail.example.com:587 2>/dev/null \
| openssl x509 -noout -dates
The smarthost is now load-bearing, and mail providers are less stable than you would like. Ours vanished mid-life: cleaning up an unrelated sending domain in the same account deleted the whole account, which took this relay's outbound path with it.
Two things came out of that. Verify a suspected credential failure with a real authentication attempt rather than inferring it from a config file. And know your fallback in advance, because the relay is a twenty-minute change once you know where the last hop is going.