From 9dc5d37aef7927733b1269395bcd223f8e9a5739 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sun, 31 Aug 2025 13:37:37 +0200 Subject: [PATCH] start working on relaying page --- .../docs/ispmail-trixie/180-relaying.mdx | 490 ++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 src/content/docs/ispmail-trixie/180-relaying.mdx diff --git a/src/content/docs/ispmail-trixie/180-relaying.mdx b/src/content/docs/ispmail-trixie/180-relaying.mdx new file mode 100644 index 0000000..db2161a --- /dev/null +++ b/src/content/docs/ispmail-trixie/180-relaying.mdx @@ -0,0 +1,490 @@ +--- +title: Sending / Relaying +lastUpdated: 2025-08-30 +slug: ispmail-trixie/relaying +sidebar: + order: 180 +--- + +import { Aside } from "@astrojs/starlight/components"; + +Your mail server is almost ready for use. But one puzzle piece is missing. Your users can receive emails but they cannot +send them yet. That is easy to configure but it needs some important precautions. + +Please note that there is a difference on how **users** send emails versus how **mail servers** send emails. For +comparison: + +- A **mail server** fetches the _MX_ record for the destination domain of the recipient’s email address from DNS. That + tells it which mail server to talk to. Then it opens an SMTP connection (TCP port 25) and sends the email. +- An **end user** with a mail client such as Thunderbird, Mutt or Roundcube cannot handle this directly. Mail clients do + not include any functionality for fetching MX records. In addition, the user is usually on a dynamic IP address that + other mail servers will not trust and will likely reject. Instead, end users are expected to send their emails through + their provider’s (your) mail server: they authenticate with their login credentials and then send the message. This + process is called relaying, because your mail server acts as an intermediary between the user and other mail servers + on the internet. For security reasons, authentication is required before the user is allowed to send emails. Users are + also expected to use the TCP port 587 which is called the _submission_ port. More on that later. + +Put simply, a mail server will accept an email if… + +- it is responsible for handling mail for the recipient’s domain (the domain is listed as a virtual domain), or +- the sender has authenticated successfully with a valid username and password. + +I have created two illustrations to explain it. + +### Outgoing email (without authentication) + +`john@example.org` is on the internet somewhere and wants to send an email to `lisa@example.com` (which is another +domain). Your mail server is not responsible for the _example.com_ domain so it receives John’s email and would have to +forward (relay) it to the actual mail server that is responsible for …@example.com email addresses. This may seem like a +harmless scenario but your mail server must deny that: + +![Diagram that shows an outgoing unauthenticated email getting rejected](images/relaying-unauthenticated-relaying-denied.png) + +Why? Because anyone can claim to be John and make your mail server forward mail. If an attacker (like a spammer) would +send millions of spam emails in John’s name through your server then other organisations will accuse *you* as the +operator of the mail server of spamming. Your mail server would be what people call an +_[open relay](https://en.wikipedia.org/wiki/Open_mail_relay)_. That is not what you want because your mail server would +get blacklisted and you will not be able to send out mail to most other servers. So without any proof that the sender is +indeed John, your server must reject the email. + +Try for yourself: + +```sh +swaks --server localhost:587 --from john@example.org --to list@example.com -tls +``` + +Towards the end of the output you will find: + +``` + ~> MAIL FROM: +<~ 250 2.1.0 Ok + ~> RCPT TO: +<~* 554 5.7.1 : Recipient address rejected: Access denied +``` + +### Outgoing email (with authentication) + +So how does John prove his identity? He needs to use _authenticated_ SMTP. This is similar to the previous case but +John’s email program will also send his username and password. + +![Diagram that shows an outgoing authenticated email getting relayed](images/relaying-authenticated-relaying.png) + +We are making sure that his authentication happens over an encrypted connection so John’s password is safe. + +Try for yourself and provide a valid user name and password: + +```sh +swaks --server localhost:587 --from john@example.org --to list@example.com -tls --auth-user john@example.org --auth-password summersun +``` + +Towards the end of the output you will find: + +``` +<~ 250 2.0.0 Ok: queued as 7513F1F54B +``` + +You can even try a wrong password or a non-existing user. That should lead to… + +``` +<~* 535 5.7.8 Error: authentication failed: (reason unavailable) +``` + +TODO: a word on $mynetworks. Perhaps in the troubleshooting section later. + +## Make Postfix use Dovecot for authentication + +Enabling SMTP authentication in Postfix is surprisingly easy. You already configured Dovecot so it knows all about your +users from the SQL database. So let’s just make Postfix use that by telling it to ask the Dovecot server to verify the +username and password. Postfix just needs some extra configuration. Run these commands on the shell: + +``` +postconf smtpd_sasl_type=dovecot +postconf smtpd_sasl_path=private/auth +postconf smtpd_sasl_auth_enable=yes +``` + +This enables SMTP authentication and tells Postfix that it can talk to Dovecot through a socket file located at +`/var/spool/postfix/private/auth`. Do you remember that Postfix runs in a sandboxed *chroot* directory? That’s at +/var/spool/postfix. It cannot access any files outside of that directory. But fortunately in a previous section you +edited the /etc/dovecot/conf.d/10-master.conf file and made Dovecot place a socket file into +`/var/spool/postfix/private/auth` to allow communication from Postfix. + +TODO: collapsible on anti-spoofing + +## Enable encryption + +TODO: move this section to a separate page on TLS encryption + +We certainly do not want our users to send their password over the wire wihtout encryption. So the following commands +enable encryption, set the key and certificate paths for Postfix. Just run these commands: + +``` +postconf smtpd_tls_security_level=may +postconf smtpd_tls_auth_only=yes +postconf smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.org/fullchain.pem +postconf smtpd_tls_key_file=/etc/letsencrypt/live/mail.example.org/privkey.pem +postconf smtp_tls_security_level=may +``` + + + +The above settings allow encrypted incoming (smtpd\_) and outgoing (smtp\_) connections. But they do not enforce it. If +a remote mail server does not have encryption enabled we will still accept their emails. You may be tempted to enforce +encryption but that would reject email communication with servers who have been configured without encryption. + +However the `smtpd_tls_auth_only=yes` setting makes sure that the user’s authentication information (email address and +password) are always encrypted between the user and your mail server. + + + +
+ Click here to learn how SMTP authentication works… + +Are you curious how SMTP authentication looks on a low level? You probably are not. But let’s do it anyway. Just once, +so that you get the idea. + +``` +telnet localhost submission +``` + +The server will let you in: + +``` +Trying 127.0.0.1… +Connected to localhost. +Escape character is '^\]'. +220 webmail ESMTP Postfix (Debian/GNU) +``` + +Say hello: + +``` +ehlo example.com +``` + +Postfix will present a list of features that are available for you: + +``` +250-mailtest +250-PIPELINING +250-SIZE 10240000 +250-VRFY +250-ETRN +250-STARTTLS +250-ENHANCEDSTATUSCODES +250-8BITMIME +250-DSN +250-SMTPUTF8 +250 CHUNKING +``` + +Let me briefly explain what these lines mean: + +- PIPELINING + This is a feature to speed up SMTP communication. Usually the remote system has to wait for a response to every command + it sends. Pipelining allows the remote server to send several commands in a batch without waiting for a response. Postfix + will just store these commands and execute them one by one. If you told Postfix to forbid pipelining it would disconnect + the remote server when it tries to send bulks of commands without waiting for the proper reply. It is mainly a feature + against rogue senders. +- SIZE 10240000 + The remote server is allowed to send emails up to 10 MB large. This has long been a common maximum size for emails. However + nowadays 40 MB or even more are more common sizes because emails have grown larger. Some mail servers even allow 100 MB. +- VRFY + Allows remote servers to verify a given name or email address. For example the remote server could send “VRFY john” and + your server might respond `250 John Doe `. It can be used to verify that a certain recipient email address + is deliverable +- ETRN + A command that a remote system can send to flush the Postfix queue of mails for a certain domain. It can be used if the + remote system had technical problems and failed to receive email for a while. Then it could send an ETRN command to make + your server start sending outstanding emails for that domain. It is rarely used. +- STARTTLS + This tells the remote system that it might start switching from this unencrypted to an encrypted connection by sending + the “STARTTLS” command. It will then start negotiating a TLS-encrypted connection. You could compare it to an HTTP connection + that suddenly switches over to an encrypted HTTPS connection. The advantage is that you can start talking SMTP on TCP port + 25 and don’t have to open up a second TCP port like 465 which is the “SSMTP” (secure SMTP) port and only accepts encrypted + connections. +- ENHANCEDSTATUSCODES + This enables more three-digit return codes for various conditions. See the [RFC2034](http://tools.ietf.org/html/rfc2034) + if you are curious. +- 8BITMIME + In ancient times SMTP only processed 7-bit characters. You couldn’t transfer special characters like “Ä” or “ß” without + special encoding. 8BITMIME allows a transmission of emails using 8-bit characters. Still many emails are specially encoded + using ISO8859-1 or UTF-8. +- DSN + It enables DSNs (delivery status notofications) that allows the sender to control the messages that Postfix creates when + an email could not be delivered as intended. +- SMTPUTF8 + In addition to 8BITMIME you can use UTF8 encoded characters in header fields. +- CHUNKING + This feature (described in [RFC 3030](https://tools.ietf.org/html/rfc3030)) makes sending of large emails more efficient. + +However one important line is missing here that would allow us to send our username and password: + +``` +250-AUTH PLAIN LOGIN +``` + +We told Postfix to only allow authentication when the connection is encrypted. So we are not offered authentication over +this plain text connection. That’s what we want. + +Are you still connected? Okay, good. So we need an encrypted connection using TLS. Using the STARTTLS feature we can +switch over from unencrypted to encrypted without having to reconnect. Enter… + +``` +STARTTLS +``` + +And the server replies: + +``` +220 2.0.0 Ready to start TLS +``` + +However now it’s getting weird because you would have to speak TLS encryption which is not a language that humans speak. +So let’s quit this using the “QUIT” command and do that differently: + +``` +QUIT +``` + +We can use OpenSSL to help us with the decryption. Run: + +``` +openssl s_client -connect localhost:25 -starttls smtp +``` + +You will see a lot of output. OpenSSL has connected to TCP port 25 and issued a STARTTLS command to switch to an +encrypted connection. So whatever you type now will get encrypted. Enter: + +``` +EHLO example.com +``` + +And Postfix will send a list of capabilities that will look like this: + +``` +250-PIPELINING +250-SIZE 10240000 +250-VRFY +250-ETRN +250-AUTH PLAIN LOGIN +250-ENHANCEDSTATUSCODES +250-8BITMIME +250 DSN +``` + +And now that we are using an encrypted connection Postfix offers us to authenticate. So let us send the authentication +string with a Base64-encoded password: + +``` +AUTH PLAIN AGpvaG5AZXhhbXBsZS5vcmcAc3VtbWVyc3Vu +``` + +Unless you have changed John’s password to something else than “summersun” the server should accept that authentication: + +``` +235 2.7.0 Authentication successful +``` + +Excellent. You are logged in through SMTP. You could now send an email to be forwarded to another mail server. I just +wanted to show that authentication works if you use an encrypted connection. + +Disconnect from Postfix: + +``` +QUIT +``` + +Authentication works. Well done. + +## Base64-encoded passwords + +Wait a minute. What was that crazy cryptic string? There was no username and password in it. Was it encrypted? + +No, that was no encryption. It was merely an _encoded_ version of the username and password. Why that? Well usually in +SMTP you can only transfer ASCII characters. But the password may contain special characters which are not covered by +ASCII. So in the _PLAIN_ method that information is [Base64](https://en.wikipedia.org/wiki/Base64) encoded. + +What is actually converted to Base64… + +``` +NULL-BYTE + USERNAME + NULL-BYTE + PASSWORD +``` + +So for John’s case you can easily create the Base64 string using: + +``` +printf '\0john@example.org\0summersun' | base64 +``` + +As a result you will get the exact same string you used above with “AUTH PLAIN”. + +
+ +## SMTP versus Submission + +Please note that there are two different TCP ports on a mail server that speak SMTP: + +| Service name | SMTP | Submission | +| -------------- | -------------------------- | --------------------- | +| TCP Port | 25 | 587 | +| Encryption | Optional | Mandatory | +| Authentication | Optional | Mandatory | +| Meant for | Servers | Humans | +| Used by | Mail transport agent (MTA) | Mail user agent (MUA) | +| Your home ISP | may block this port | will allows this port | + +I hope that makes the distinction a bit clearer. Our human users will have to use the _submission_ service. + +All Postfix services are declared in the `/etc/postfix/master.cf` file. Please edit the file and look for the +_submission_ section that is commented out by default. Turn that section into the following. Basically I removed the ‘#’ +character on all lines of this section and removed the lines with the mua\_\* variables. + +``` +submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_tls_auth_only=yes + -o smtpd_reject_unlisted_recipient=no + -o smtpd_client_restrictions= + -o smtpd_helo_restrictions= + -o smtpd_sender_restrictions= + -o smtpd_relay_restrictions= + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject + -o milter_macro_daemon_name=ORIGINATING +``` + +Make sure to start the first line in the first column and indent the following lines. + +This service uses the “smtpd” daemon (see the last word of the first line) which is the piece of software that responds +if you open an SMTP connection on TCP port 25. But it gets a few extra options set… + +- in the /var/log/mail.log log file you will see the connections to the submission port as “postfix/submission” (the + _syslog_name_ setting above) +- enforce encryption on this port (_smtpd_tls_security_level_) +- enable authentication (_smtpd_sasl_auth_enable_) +- enforce encryption during authentication (_smtpd_tls_auth_only_) +- allow sending emails to recipients outside of this mail server (_smtpd_reject_unlisted_recipient_) +- remove special restrictions (_smtpd\_\*\_restrictions_) +- allow relaying if the sender was authenticated (_smtpd_recipient_restrictions_) +- send the string _ORIGINATING_ to milter services (_milter_macro_daemon_name_) – you can just leave it like that + +Restart the Postfix server: + +``` +systemctl restart postfix +``` + +Your users can now use the *submission* port to send email. They just use the port 587 in their mail clients instead of +port 25. + +Feel free to send a test mail using the submission port and encryption. You will need to install the libnet-ssleay-perl +package first, so that SWAKS can speak TLS: + +``` +apt install -y libnet-ssleay-perl +swaks --server localhost --to john@example.org \ + --port 587 -tls \ + --auth-user john@example.org \ + --auth-password summersun +``` + + + +## Protecting against forged sender addresses + +A “forged” sender address means that someone claims to be someone else. Let’s say that John has authenticated and the +mail server trusts him. Nothing keeps John from impersonating someone else and sending email in his name? Most email +service providers have restrictions that you can only send emails if the sender matches your actual email address. Let’s +do that, too. + +Postfix provides a setting called +[smtpd_sender_login_maps](http://www.postfix.org/postconf.5.html#smtpd_sender_login_maps) for that purpose. From the +“maps” part you can deduce that it expects a _mapping_ again. This time the two columns mean… + +- Left column: who you claim to be (email’s sender address) +- Right column: who you logged in as (user name after authentiation) + +As we use the email address also as a user name we simply need a mapping where the email address is listed on both +sides. Fortunately we already have a mapping like that: our _email2email_ mapping that we used earlier as a workaround +for catchall forwardings. Let’s reuse it. Please run… + +``` +postconf smtpd_sender_login_maps=mysql:/etc/postfix/mysql-email2email.cf +``` + +This sets the parameter both for the SMTP port (25) and the submission port (587). Defining these maps is not enough +though. You also need to make Postfix act on this. Edit the /etc/postfix/master.cf again and in the _submission_ section +add the following option. Make sure the line is indented like all other options: + +``` + -o smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject +``` + +Restart Postfix after this change: + +``` +systemctl restart postfix +``` + +You can now try to send email as a different user than you are logged in. Let’s us swaks again to send a spoofed email: + +``` +swaks --server localhost --from john@example.com \ + --to john@example.org --port 587 -tls \ + --auth-user john@example.org \ + --auth-password summersun +``` + +Please note that John’s actual email address is **not** john@example.**com** but john@example.**org**. So it must be +rejected. The server should tell you: + +``` +~> MAIL FROM:john@example.com +<~ 250 2.1.0 Ok +~> RCPT TO:john@example.org +<~ * 553 5.7.1 john@example.com: Sender address rejected: not owned by user john@example.org +``` + +Sorry if the mixture of example.org and example.com in my examples makes this guide less readable. But I can’t use any +real world domains here. Please just note that sometimes it’s “.com” and sometimes “.org”. + +Of course you can test your new security feature in the Roundcube web mail interface as well. In Settings/Identities you +can create another sender email address as in: + +![Selecting another identity in Roundcube](images/relaying-roundcube-selecting-identity.png) + +With this wrong sender address any email should get rejected. Your mail.log would read something like: + +``` +NOQUEUE: reject: RCPT from foo.bar\[…\]: + 553 5.7.1 : + Sender address rejected: + not owned by user john@example.org; + from= to=<…> +``` + +