From 3cb34170c7e2fd18302b0be3930a44a2f1245517 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 10 Sep 2025 23:32:18 +0200 Subject: [PATCH] add title to verbatim blocks. finish relaying page --- .../ispmail-trixie/130-install-packages.mdx | 2 +- .../docs/ispmail-trixie/155-database.mdx | 2 +- src/content/docs/ispmail-trixie/157-dns.mdx | 7 +- .../docs/ispmail-trixie/158-certbot.mdx | 6 +- .../docs/ispmail-trixie/160-postfix.mdx | 10 +- .../docs/ispmail-trixie/165-dovecot.mdx | 18 +- src/content/docs/ispmail-trixie/167-lmtp.mdx | 8 +- .../docs/ispmail-trixie/170-webmail.mdx | 2 +- .../docs/ispmail-trixie/180-relaying.mdx | 376 ++++++++---------- 9 files changed, 194 insertions(+), 237 deletions(-) diff --git a/src/content/docs/ispmail-trixie/130-install-packages.mdx b/src/content/docs/ispmail-trixie/130-install-packages.mdx index 824506a..5e57e76 100644 --- a/src/content/docs/ispmail-trixie/130-install-packages.mdx +++ b/src/content/docs/ispmail-trixie/130-install-packages.mdx @@ -10,7 +10,7 @@ import { Aside } from "@astrojs/starlight/components"; Let's install the necessary Debian packages. Run this command as root on your server as _root_: -```sh +```sh title="Run this on your server" apt update DEBIAN_FRONTEND=noninteractive \ apt -y install postfix-mysql dovecot-mysql \ diff --git a/src/content/docs/ispmail-trixie/155-database.mdx b/src/content/docs/ispmail-trixie/155-database.mdx index fcd1b8a..80e086c 100644 --- a/src/content/docs/ispmail-trixie/155-database.mdx +++ b/src/content/docs/ispmail-trixie/155-database.mdx @@ -30,7 +30,7 @@ you’re actually using MariaDB. This step is simple. Connect to the database using the `mariadb` command in your shell: -```sh +```sh title="Run this on your server" mariadb ``` diff --git a/src/content/docs/ispmail-trixie/157-dns.mdx b/src/content/docs/ispmail-trixie/157-dns.mdx index da064b4..ec7a66f 100644 --- a/src/content/docs/ispmail-trixie/157-dns.mdx +++ b/src/content/docs/ispmail-trixie/157-dns.mdx @@ -29,6 +29,7 @@ records: - **A / AAAA record (`smtp.example.com`)** The hostname specified in the MX record (`smtp.example.com`) must resolve to an IP address so that other mail servers know how to reach it. + - An **A record** maps the hostname to an IPv4 address. - An **AAAA record** maps the hostname to an IPv6 address. @@ -63,14 +64,14 @@ servers may be more likely so flag your sent emails as spam if you do not have a Use the `host` command to check your domain's MX record. -```sh +```sh title="Run this on your server" $> host -t mx example.com example.com mail is handled by 10 smtp.example.com. ``` Then check if that result resolves to an IP address: -```sh +```sh title="Run this on your server" $> host smtp.example.com smtp.example.com has address 49.13.89.249 smtp.example.com has IPv6 address 2a01:4f8:c012:62d1::1 @@ -78,7 +79,7 @@ smtp.example.com has IPv6 address 2a01:4f8:c012:62d1::1 Preferably the IP addresses should resolve back to the mail server's name: -```sh +```sh title="Run this on your server" $> host 49.13.89.249 249.89.13.49.in-addr.arpa domain name pointer smtp.example.com. $> host 2a01:4f8:c012:62d1::1 diff --git a/src/content/docs/ispmail-trixie/158-certbot.mdx b/src/content/docs/ispmail-trixie/158-certbot.mdx index d029e11..df6d528 100644 --- a/src/content/docs/ispmail-trixie/158-certbot.mdx +++ b/src/content/docs/ispmail-trixie/158-certbot.mdx @@ -40,7 +40,7 @@ Please replace `mail.example.org` by the FQDN you chose. -```sh +```sh title="Run this on your server" certbot --apache --register-unsafely-without-email --agree-tos -d mail.example.org ``` @@ -108,7 +108,7 @@ checks your certificate frequently and triggers a renewal one month before it ex These commands will show you the definition and status of the timer and the renewal service: -```sh +```sh title="Run this on your server" systemctl status certbot.timer systemctl cat certbot.timer systemctl status certbot.service @@ -121,7 +121,7 @@ look cooler. It is important that services that use the certificate load the new certificate after an automatic renewal. This shell command will create a file `/etc/letsencrypt/cli.ini` that handles it: -```sh +```sh title="Run this on your server" echo > /etc/letsencrypt/cli.ini "post-hook = systemctl reload postfix dovecot apache2" ``` diff --git a/src/content/docs/ispmail-trixie/160-postfix.mdx b/src/content/docs/ispmail-trixie/160-postfix.mdx index dc8a55b..a8c86db 100644 --- a/src/content/docs/ispmail-trixie/160-postfix.mdx +++ b/src/content/docs/ispmail-trixie/160-postfix.mdx @@ -47,7 +47,7 @@ SELECT "yes" FROM virtual_domains WHERE name='example.net' Run the following code in your shell to create a configuration file creating that mapping: -```sh +```sh title="Run this on your server" cat > /etc/postfix/mariadb-virtual-mailbox-domains.cf << EOF user = mailserver password = MAILSERVER-PASSWORD-HERE @@ -81,7 +81,7 @@ Apparently, there aren't any more. So the email will go to `john@example.org`. Run this code to create the appropriate mapping file: -```sh +```sh title="Run this on your server" cat > /etc/postfix/mariadb-virtual-alias-maps.cf << EOF user = mailserver password = MAILSERVER-PASSWORD-HERE @@ -97,7 +97,7 @@ The query now gets all the `destination` email addresses from the database for a The last mapping we need is a query to find valid mailboxes. Without further ado: -```sh +```sh title="Run this on your server" cat > /etc/postfix/mariadb-virtual-mailbox-maps.cf << EOF user = mailserver password = MAILSERVER-PASSWORD-HERE @@ -119,7 +119,7 @@ sufficient here. You have created the config files for the three mappings. Now you just need to tell Postfix to use them: -```sh +```sh title="Run this on your server" postconf virtual_mailbox_domains=mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf postconf virtual_mailbox_maps=mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf postconf virtual_alias_maps=mysql:/etc/postfix/mariadb-virtual-alias-maps.cf @@ -138,7 +138,7 @@ access. After all a database password is found in these files. Give the mappings a quick test using the `postmap -q` command (_q_ stands for _query_): -```sh +```sh title="Run this on your server" postmap -q example.org mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf postmap -q jack@example.org mysql:/etc/postfix/mariadb-virtual-alias-maps.cf postmap -q john@example.org mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf diff --git a/src/content/docs/ispmail-trixie/165-dovecot.mdx b/src/content/docs/ispmail-trixie/165-dovecot.mdx index 46aecd1..5a3b6e4 100644 --- a/src/content/docs/ispmail-trixie/165-dovecot.mdx +++ b/src/content/docs/ispmail-trixie/165-dovecot.mdx @@ -18,7 +18,7 @@ In this chapter we will mainly configure Dovecot so that it knows how to deliver On your disk all the mailboxes will live in the directory `/var/vmail`. To separate the mailboxes from the rest of your system, let's create a new user and group that will own the mailboxes: -```sh +```sh title="Run this on your server" groupadd --system vmail useradd --system --gid vmail vmail mkdir -p /var/vmail @@ -161,7 +161,7 @@ a fixed schema for the home directory (`/var/vmail/DOMAIN/USER`) and the user-ID Make sure that only root can access the SQL configuration file so nobody else is reading your database access passwords: -```sh +```sh title="Run this on your server" chown root:root /etc/dovecot/conf.d/auth-sql.conf.ext chmod go= /etc/dovecot/conf.d/auth-sql.conf.ext ``` @@ -170,13 +170,13 @@ chmod go= /etc/dovecot/conf.d/auth-sql.conf.ext Restart Dovecot from the shell: -```sh +```sh title="Run this on your server" systemctl restart dovecot ``` Check your logs: -```sh +```sh title="Run this on your server" journalctl -fu dovecot ``` @@ -196,20 +196,20 @@ However in the age of _systemd_ the output of the processes is written to system To read the complete logs of Dovecot, run (u=unit): -```sh +```sh title="Run this on your server" journalctl -u dovecot ``` To `tail -f` the logs (f=follow): -```sh +```sh title="Run this on your server" journalctl -fu dovecot ``` However on a busy server it will show the entire journal and then follow any changes. You may want to limit the output to the last 10 lines: -```sh +```sh title="Run this on your server" journalctl -n 10 -fu dovecot ``` @@ -221,7 +221,7 @@ If you prefer the old way of having log files, just install the `rsyslog` packag To test the `userdb` query: -```sh +```sh title="Run this on your server" doveadm user john@example.org ``` @@ -237,7 +237,7 @@ mail_path /var/vmail/example.org/john/Maildir To test the `iterate` query that gets a list of all users: -```sh +```sh title="Run this on your server" doveadm user '*' ``` diff --git a/src/content/docs/ispmail-trixie/167-lmtp.mdx b/src/content/docs/ispmail-trixie/167-lmtp.mdx index c0e6340..53abdbd 100644 --- a/src/content/docs/ispmail-trixie/167-lmtp.mdx +++ b/src/content/docs/ispmail-trixie/167-lmtp.mdx @@ -54,7 +54,7 @@ The “_virtual_transport_" in Postfix defines the service to use for delivering created a socket file and is ready to listen to incoming LMTP connections. We just need to tell Postfix to send emails there. So please run: -```sh +```sh title="Run this on your server" postconf virtual_transport=lmtp:unix:private/dovecot-lmtp ``` @@ -73,7 +73,7 @@ TODO: move to optional chapter Finally we are ready for our first email. Using the `swaks` command you can create a test email to `john@example.org` and send it to Postfix: -```sh +```sh title="Run this on your server" swaks --server localhost --to john@example.org ``` @@ -122,7 +122,7 @@ If you see a `250 2.0.0 Ok: queued as…" near the end of this dialog, then Post Check if Postfix managed to pass the email to Dovecot: -```sh +```sh title="Run this on your server" journalctl -n 10 -u dovecot ``` @@ -134,7 +134,7 @@ Aug 24 21:36:22 mail dovecot[23214]: lmtp(john@example.org)<…><…>: msgid=<20 Does it say `saved mail to INBOX`? Awesome. The email should be visible in John's home directory. Check it: -```sh +```sh title="Run this on your server" find /var/vmail/example.org/john ``` diff --git a/src/content/docs/ispmail-trixie/170-webmail.mdx b/src/content/docs/ispmail-trixie/170-webmail.mdx index 53a0326..43c4ed2 100644 --- a/src/content/docs/ispmail-trixie/170-webmail.mdx +++ b/src/content/docs/ispmail-trixie/170-webmail.mdx @@ -143,7 +143,7 @@ $config['plugins'] = array( Plugins are configured through files located in the `/etc/roundcube/plugins` directory. Let's quickly set up the password plugin: -```sh +```sh title="Run this on your server" cat > /etc/roundcube/plugins/password/config.inc.php << 'EOF' @@ -116,7 +141,7 @@ Please replace `mail.example.org` by the FQDN you chose. -```sh +```sh title="Run this on your server" postconf smtp_tls_security_level=may postconf smtpd_tls_security_level=may postconf smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.org/fullchain.pem @@ -143,153 +168,12 @@ be set for the `submission` service to enable authentication there.[^1] either from a remote mail server or one of your users. -## Relaying - -Feel free to send a test mail using the `submission` port and encryption (using STARTTLS): - -``` -swaks --server localhost:587 \ - --from john@example.org \ - --to lisa@example.com \ - -tls \ - --auth-user john@example.org \ - --auth-password summersun -``` - -Also give the `submissions` service a try: - -``` -swaks --server localhost:465 \ - --from john@example.org \ - --to lisa@example.com \ - --tls-on-connect \ - --auth-user john@example.org \ - --auth-password summersun -``` - -Did you notice the difference? This time we used port **465** and instead of the `-tls` option (which uses STARTTLS to -switch to encryption) we use `--tls-on-connect` that uses TLS right from from the start. - -TODO: try sending from roundcube - -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. - -### Without authentication = denied - -`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 -``` - -TODO: submissions does not work yet - -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 -``` - -### With authentication = accepted - -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/dovecot-auth -``` - -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 - ## Protecting against forged sender addresses -At this stage, a security issue remains. While Postfix is configured to relay emails only after a successful login with -a valid username and password, it does not prevent an authenticated user from impersonating another sender. For example, -someone could log in as `john@example.org` but send an email appearing to come from `brunhilde@example.net`. - -You want to see that this work? Let's pretend to be _brunhilde_ while authenticating as _john_: - -```sh -swaks --server localhost:587 \ - --from brunhilde@example.org \ - --to list@example.com \ - -tls \ - --auth-user john@example.org \ - --auth-password summersun -``` - -That should not be allowed. But Postfix does not know better and responds with: - -``` -<~ 250 2.0.0 Ok: queued as EEEB420F51 -``` +Before we can actually test the setup, there is another security issue we need to address. While Postfix is configured +to relay emails only after a successful login with a valid username and password, it does not prevent an authenticated +user from impersonating another sender. For example, someone could log in as `john@example.org` but send an email +appearing to come from `brunhilde@example.net`. We should fix that. For that purpose Postfix provides a setting called [smtpd_sender_login_maps](http://www.postfix.org/postconf.5.html#smtpd_sender_login_maps). From the "maps" part you can @@ -304,7 +188,7 @@ sender, he must also authenticate as `john@example.org`. 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. Let's create it: -```sh +```sh title="Run this on your server" cat > /etc/postfix/mariadb-email2email.cf << EOF user = mailserver password = MAILSERVER-PASSWORD-HERE @@ -315,24 +199,20 @@ EOF chown root:postfix /etc/postfix/mariadb-email2email.cf chmod u=rw,g=r,o= /etc/postfix/mariadb-email2email.cf -``` -Tell Postfix to use this mapping for our purpose: - -``` postconf smtpd_sender_login_maps=mysql:/etc/postfix/mariadb-email2email.cf ``` -You have already added `smtpd_sender_restrictions=reject_sender_login_mismatch` earlier in the master.cf for the -`submission` and `submissions` services. So you have achieved these three steps to prevent sender impersonation: +What you accomplished: -1. Create a mapping that matches an email address to itself. -2. Tell Postfix to use that mapping for `smtpd_sender_login_maps` which controls who can be a sender. -3. Enforce that restriction for the `submission` and `submissions` services. +1. Create the _mapping_ file +1. Fix the permissions of it so that only Postfix can read it (there is a password in it) +1. Make Postfix use that mapping for its `smtpd_sender_login_maps` setting +1. Add `smtpd_sender_restrictions=reject_sender_login_mismatch` to the `submission` and `submissions` services 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: -```sh +```sh title="Run this on your server" swaks --server localhost:587 \ --from brunhilde@example.org \ --to list@example.com \ @@ -350,33 +230,113 @@ The server should rightfully reject that wrong sender: <~ * 553 5.7.1 john@example.com: Sender address rejected: not owned by user john@example.org ``` -You can also see that restriction in action by adding a fake persona in Roundcube and try to use it. In -Settings/Identities you can create another sender email address as in: +Now let's send a proper email from John: -![Selecting another identity in Roundcube](images/relaying-roundcube-selecting-identity.png) +```sh title="Run this on your server" +swaks --server localhost:587 \ + --from john@example.org \ + --to list@example.com \ + --tls \ + --auth-user john@example.org \ + --auth-password summersun +``` -With this wrong sender address any email should get rejected. Roundcube will show you an error that is generated by -Postfix: +That will work: ``` -553 5.7.1 : -Sender address rejected: -not owned by user john@example.org +<~ 250 2.0.0 Ok: queued as AFFF12292A ``` -## How does authentication work? +## Test it -
- Click here if you want to learn how SMTP authentication works… +### No encryption, no authentication -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. +So far you have set up the `submission` service(s), enabled encryption and connected Postfix to Dovecot for +authentication. Let's try it. Our first test email will be sent without encryption and authentication: + +```sh title="Run this on your server" +swaks --server localhost:587 \ + --from john@example.org \ + --to lisa@example.com +``` + +Postfix should tell you that it will not accept an email without encryption: + +``` +<** 530 5.7.0 Must issue a STARTTLS command first +``` + +### With encryption, no authentication + +Our second attempt will **enable** (STARTTLS) encryption – but still no authentication: + +```sh title="Run this on your server" +swaks --server localhost:587 \ + --from john@example.org \ + --to lisa@example.com \ + -tls +``` + +Postfix will still reject the email because we refused to authenticate: + +``` + ~> MAIL FROM: +<~ 250 2.1.0 Ok + ~> RCPT TO: +<~* 554 5.7.1 : Sender address rejected: Access denied +``` + +### With encryption and authentication + +```sh title="Run this on your server" +swaks --server localhost:587 \ + --from john@example.org \ + --to lisa@example.com \ + -tls \ + --auth-user john@example.org \ + --auth-password summersun +``` + +Also give the `submissions` service a try: + +```sh title="Run this on your server" +swaks --server localhost:465 \ + --from john@example.org \ + --to lisa@example.com \ + --tls-on-connect \ + --auth-user john@example.org \ + --auth-password summersun +``` + +Did you notice the difference? This time we used port **465** and instead of the `-tls` option (which uses STARTTLS to +switch to encryption) we use `--tls-on-connect` that uses TLS right from from the start. + +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. + +## Gory details of authentication + + ``` telnet localhost submission @@ -387,7 +347,7 @@ The server will let you in: ``` Trying 127.0.0.1… Connected to localhost. -Escape character is '^\]'. +Escape character is '^]'. 220 webmail ESMTP Postfix (Debian/GNU) ``` @@ -426,8 +386,8 @@ Let me briefly explain what these lines mean: 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 + your server might respond `250 Jo You can skip it for now.hn 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 @@ -551,7 +511,3 @@ printf '\0john@example.org\0summersun' | base64 ``` As a result you will get the exact same string you used above with "AUTH PLAIN". - -TODO: move further down - -