Hello World, this is an email - Part 1
What's the first thing you do when setting up as an indie gamedev? Why, set up a mailserver, of course! This is Part 1 of the process I went through, where we get basic email running with Postfix and some free Let's Encrypt certificates...
As part of setting up Sidequest Ninja, I've been doing a lot of server admin, setting up a website and a mailserver. Setting up the mail, assuming you want things like TLS support, spam and virus scanning, and support for showing that your emails are authentic like SPF, DKIM, and DMARC, is a pretty involved process, and even having done it once before I couldn't do it without lots of internet searches of tutorials, manuals, and other people asking questions on sites like superuser.com to get help with various issues.
I made notes as I went, and this series of blog posts is the result. I hope that documenting the route I took is useful to someone else. Any grand claim to be the tutorial to end all tutorials about mailservers will rapidly go out of date, and in fact I'm going to say, here and now, that I wrote this in January 2020, and as time passes the chances that this tutorial is obsolete in some way will only increase. Even the official docs sometimes seem out of sync.[1]
So, whatever you do, please don't blindly follow my lead. Software changes, and stuff written on the Internet goes out of date.
With that caveat out of the way, here's where this set of tutorials should take you. By the time we're done, you'll have a running mailserver using Postfix, Dovecot, PostgreSQL, Amavis, ClamAV, SpamAssassin, and Opendmarc. You'll also have a Let's Encrypt SSL certificate to let you send and receive email securely (although not everyone you email might be able to receive email over an encrypted connection), and you'll have set up SPF, DKIM, and DMARC both to check incoming mail and to help your own emails not be incorrectly marked as spam. A few notes before we begin:
- These notes are designed for people who are comfortable working with Linux already, who just haven't set up a mailserver before. I'll explain the reasoning behind each step we take to help you understand what you're doing.
- I'm going to assume that you understand some basics of how the Internet works - that you know what DNS is, and so on.
- My own installation was on Ubuntu 18.04 LTS, and it should be straightforward to use these notes with any Debian-based Linux distribution, but with a bit of tweaking you should be able to use it with any flavour of Linux. We'll only use software from Ubuntu's
main
anduniverse
repositories, so rest easy if you're worried about Ubuntu's approach to providing software that isn't entirely free and open-source. - ClamAV will take up nearly a gigabyte of memory while running, because it loads its virus definitions into memory. This value will only go up as more virus definitions get added. So make sure your server has enough memory available.
The Shape of a Mailserver
You have a server, either a physical one or a virtual machine, sitting in a datacentre somewhere, connected to the internet. To turn it into a mailserver, we're going to need to do a few things.
- First, we're going to need to tell people we have a mailserver, so they can address mail to us.
- Second, we're going to need to run some software that deals with incoming messages. Email is generally exchanged on a standard set of ports, so we'll listen on those for incoming mail. Once mail has arrived on the server, we'll need to do some processing on it and, all being well, put it in someone's mailbox. Assuming you don't want people to log onto your server directly to read their mail, we'll also need to support the use of a mail client or MUA ("Mail User Agent", e.g. Thunderbird) so that users can get access to their emails. We can also set up mail accounts separately to Linux user accounts, so that people who don't have access to your server directly can still have an email account.
- For outgoing mail, we'll need the reverse. We'll need to be able to submit emails from mail clients to the mailserver, which will need to work out where they need to go, and connect to someone else's listening mailserver to send the message.
- Given the nasty things that happen on the Internet, we'll want to be able to check incoming mail to see if it's spam or carrying a virus or other malware, and we'll want to communicate with other mailservers over an encrypted connection where possible.
For this first post, we're going to get the very basics working, sending and receiving emails using the command line.
The backbone of all this is a Mail Transfer Agent (MTA), in our case Postfix. Postfix is a modular system. It itself is divided into a set of small processes, but other software such as Amavis can register themselves with Postfix so that they can examine, alter, and potentially even throw away email before it reaches its final resting place in a mailbox. So we'll set up the basic Postfix system, and then add functionality to our mailserver by adding other programs in.
But before any of this happens, people need to know where they can send you emails. To announce publicly that we can receive emails, we need an MX ("mail exchanger") DNS record which will tell anyone who wants to email us which server to send it to. So, if I want mail addressed to the sidequestninja.com
domain to be handled by the mail.sidequestninja.com
server, I need to put that in an MX record. Decide what you want your mailserver to be called as far as the rest of the world is concerned, and go and set up an MX record. MX records have a "priority" value - assuming you only have one mailserver, a default value (say, 10) will do fine, but MTAs will try to send mail to the server with the lowest-value priority number first if there are several. If you had a system with multiple mailservers you could have several with the same priority, in which case incoming email would be spread across them to balance load, and you could use a mailserver with a large priority value as a backup if the main mailserver(s) are down.
Once that's done, you need to make sure that Postfix knows that it's supposed to handle mail sent to whatever you put in the MX record. The first step is to set your server's hostname
property (the server's idea of its own name) to whatever you put in the MX record. This needs to be the Fully Qualified Domain Name (FQDN) that any server can use to find yours across the open internet. To find out what your server's hostname is, just run the hostname
command.
If this isn't the FQDN in your MX record, you'll need to change it. On Ubuntu, the hostname is in the file at /etc/hostname
. Open that up in a text editor and set the value to the FQDN. That will change the hostname the next time the server starts up. To change it now, run
hostname new-server-name
Running that command on its own only lasts until the server restarts, but at that point it'll read from the updated file. We'll tell Postfix to use this value for its own config later.
Next, let's actually install Postfix. Depending on your Linux distro, getting the latest stable version, which is generally a good idea, might involve setting up a package repository other than the default. For Ubuntu 18.04 LTS, this means making sure you have the bionic-updates
repositories as well as the plain bionic
ones. (In our case, bionic-updates
is particularly important for Amavis, as there's an important bugfix that's not available on bionic
.) Configure your repositories as necessary, then run:
apt-get install postfix
Postfix consists of lots of small daemon processes working together, rather than one big process. This means that if a daemon encounters a problem it can be killed and restarted easily, and if it gets compromised the attacker doesn't gain control of your entire email system. You set up Postfix by configuring the daemons you need to process mail.
The postconf
command lets you view and edit settings in the Postfix config, but we're going to make a lot of changes so you're probably better off opening up the config files directly and editing them.
Under Ubuntu, you can find the master config file that controls which daemons run at /etc/postfix/master.cf
- open it up and take a look. You'll find a lot of it's commented out (the lines starting with a #
), but those comments contain settings you're likely to want if you use the relevant bits of the system. By the way, there'll be another set of files at /usr/share/postfix
where you can find default versions of the various config files. They're not used by Postfix, they're there as a reference. If you manage to make a mess of master.cf
(or main.cf
), you can look there to see what the default file was, which might help you unpick what you did wrong.
First of all, we want an SMTP daemon. SMTP is the Simple Mail Transfer Protocol, and it's the way emails are sent and received between mailservers. Under SMTP, your message consists of various parts you can see (the message body, the subject field, who it's to, etc), and a potentially large number of headers that mail clients don't show you unless you go looking for them, but serve a variety of purposes such as adding a DKIM signature, which proves that email that claims to come from a particular domain really does and hasn't been spoofed by spammers. (We'll set up DKIM later, it'll help your outgoing emails not get classified as spam, which can easily happen when you start sending email from a new domain no-one has heard of.)
Generally, sending mail happens on two ports. Port 25 is used when mailservers talk to each other - this is the standard port defined by the original RFC for SMTP back in 1982. However, when you're sending email from your MUA to your mailserver for it to send on elsewhere it's more common to use port 587, which is known as the "submission" port. So we're going to get the SMTP daemon to listen on both of those ports, for different reasons. Looking at master.cf
, you'll see a line near the top, not commented out, just underneath some comments that explain the various columns, like this:
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
smtp inet n - y - - smtpd
This line defines a service that Postfix supports. The first element of this line, smtp
, tells Postfix what port this particular service runs on. smtp
is an alias for port 25. The type column, inet
, means that the service is listening on a TCP/IP port, which means it can potentially be reached from the outside world - you'll see some of the commented-out services have a type of unix
, which would listen on a local Unix socket and wouldn't be accessible from outside. The private column is set to n
, so that other servers can actually send you mail. A value of y
wouldn't be much use. The -
in the unpriv column uses the default (defined in the comments), since we don't want the daemon to run with root privileges (it doesn't need them, and for security reasons you don't want to give a process more privileges than it needs). Setting chroot to y
goes one step further, allowing the daemon to only access a small part of the server's filesystem (specifically, the mail queue directory). We leave wakeup as the default never
, because we don't want to periodically poke the SMTP daemon for no reason. Some other services do need this because they won't be triggered in any other way, but for SMTP we only want it to do something when some mail actually arrives. The default maxproc
, 100, sets the number of concurrent instances of the SMTP daemon that can run at once. The final column, command + args
, is what we want Postfix to do when some mail arrives at port 25. smtpd
is the SMTP daemon, which is what we want to be invoked to handle incoming mail.
Since this line is uncommented by default, Postfix is already listening on port 25 for incoming mail, though it won't know yet what to do with it if it arrives.
By the way, now is a good time to talk about firewalls. Hopefully you have one already, if not then stop setting up a mailserver and get that sorted, it's an important line of defence against Bad People On The Internet, of which there are many. But you will need to make sure that your firewall allows incoming connections from ports 25 and 587 for mail to work. Later on you'll also need port 993 for IMAP (which is how your MUA gets your mail off the server for you to read), so if you're configuring your firewall you may as well open that up at the same time.
Back to Postfix. What isn't running by default is a service on the "submission" port, 587. Find the line that starts with submission
, and uncomment it. Uncomment (or add) the following lines below it - you might need to edit some of the arguments. We'll add a couple more later, but these will do for now. Make sure you leave the leading spaces on the lines that start with -o
, otherwise Postfix will interpret them as the start of a new service definition and get very confused.
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_wrappermode=no
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_mynetworks,permit_sasl_authenticated,defer
- The first line is the same as the smtp service, except that
submission
is an alias for port 587. The lines starting-o
are "overrides" that set various options for the way the service operates. Without them Postfix will use the values inmain.cf
, or the defaults if nothing's set there either. syslog_name
is just for logging purposes, logs triggered by this service will use whatever you put here as a prefix to the process name.postfix/submission
is pretty clear, so I'd just leave it as it is.smtpd_tls_wrappermode
is to do with using TLS encryption when mail clients connect. We'll talk about encryption later, but by setting this tono
we tell Postfix not to use the obsolete SMTPS protocol if a client can't use the preferred STARTTLS system.smtpd_tls_security_level=encrypt
tells Postfix to refuse to talk to any mail client that isn't using encryption. We'll need to set up an SSL certificate later for this to work.smtpd_sasl_auth_enable=yes
enables SASL authentication.
SASL stands for Simple Authentication and Security Layer. It allows for username and password authentication on mail protocols like SMTP - we're telling Postfix that we intend to supply a username and password when we connect with a mail client, so that nobody can send an email from, say, dave@example.com
unless they can authenticate as Dave with the appropriate username and password. This is different to using an encrypted connection. Encryption allows Postfix to ensure that when Dave submits an email, nobody was able to interfere with it between Dave's computer and the mailserver. SASL allows Postfix to be sure that the person sending the email really is Dave.
smtpd_relay_restrictions=permit_mynetworks,permit_sasl_authenticated,defer
tells Postfix when it should be willing to relay a message (i.e. send it to another mail server rather than deliver it locally). You don't want to relay any old message, otherwise spammers could connect to your mailserver and give you several million emails to deliver to hide their origins from the recipients. The list of options control what Postfix does.- First,
permit_mynetworks
. You can give Postfix a list of IP addresses that it can trust (configured elsewhere). By default in Postfix 3.0 and later, the only trusted IP is the server itself. This means that mail sent locally from the server will pass the relay check. If it does, Postfix doesn't bother checking the other options here, because it's determined that it can relay the message. permit_sasl_authenticated
means that successfully authenticated clients can send mail out to other mailservers. If we've given Dave a username and password and he's used them to log in, he'll be able to send email to external addresses (e.g. Gmail).- The final option,
defer
, tells Postfix what to do as a last resort.defer
rejects the email, but with an error code that tells the client it's allowed to try again later, as opposed toreject
which tells the client never to try resending that email ever again.defer
is a bit safer when you're setting things up and a configuration problem might prevent emails being sent properly.
- First,
You need to give Postfix some sort of sensible fallback position for mail that doesn't pass any of the permit_
checks. Either this setting, or the similar smtpd_recipient_restrictions
must have a fallback of defer
, reject
, or one of a few other "reject or defer unless X" options, otherwise Postfix will refuse to handle mail at all, because it knows there will be cases it can't handle.
With smtpd_relay_restrictions
set to these three settings (in that order), we're allowing mail sent locally or from an authenticated user to be sent, and anyone else can get stuffed.
You may notice that the default smtp daemon that was already active doesn't have any overrides at all, so it's just using the default values which are set in main.cf
, which should be what we want, but let's check. Open main.cf
, and you should find that smtpd_relay_restrictions
is set to permit_mynetworks permit_sasl_authenticated defer_unauth_destination
. Note the difference in the final argument: defer_unauth_destination
- this means that Postfix WILL allow an incoming email from a non-local, unauthenticated source IF (and only if) it realizes that it's receiving an email it needs to deliver to a local mailbox (it is possible to set up a list of domains that Postfix should relay to, but we won't be doing this). This will allow anyone to send Dave an email, but Postfix will still reject emails that would need to be relayed elsewhere.
The first thing we want to change here is to tell Postfix that it's supposed to handle mail sent to the FQDN we settled on earlier. There are two steps to this. First we set the myhostname
parameter in main.cf
to whatever we set hostname and the MX record to. We then need to also set mydestination
, which tells Postfix it should accept mail addressed to the listed domains and deliver it to local mailboxes. Set it to
mydestination = $myhostname, localhost.$mydomain, $mydomain
and incoming mail should be handled correctly whether it was sent internally or from outside. We haven't actually set $mydomain
in main.cf
, but by default it's equal to $myhostname
with the first element taken off. So if you set $myhostname
to mail.example.com
, then $mydomain
is automatically example.com
unless you explicitly override it. With $mydestination
set, the defer_unauth_destination
setting will refuse to accept any email sent to anywhere other than @mail.example.com
or @example.com
, or sent locally.
We need to do a few other things associated with the domain for outgoing mail, too. If we're setting up email for example.com
, on a machine with a hostname of mail.example.com
, and Dave wants to send email from his useraccount dave
on that server, then we want his emails to appear to come from dave@example.com
. Depending on what program he's using, for example mail
from the command line (and also depending on the version of what he's using), any email he sends will probably have a sender of either just dave
(this is what happened when I set up email on my first domain), or dave@mail.example.com
(which is what happened when setting up email for Sidequest Ninja). Neither of these is what we want. We need to tell Postfix how to solve both of these problems.
First, let's deal with mail from dave
. When Postfix sees email with no domain part, it adds something on (at least by default, you can turn this off but it's generally a bad idea). It decides what to add on based on the myorigin
setting. By default this is set to $myhostname
, but that would mean email coming from dave@mail.example.com
. Instead, set
myorigin = $mydomain
To deal with the second problem, we need to set
masquerade_domains = $mydomain
This will strip anything.whatsoever.example.com
down to example.com
, which is what we're after.
In theory, we've now set up all the config we need to send email out into the world in a very basic way, although we need to tell Postfix to reload its config to pick up the changes. Save main.cf
, and then run
/etc/init.d/postfix reload
Then try sending an email to an external address using the mail
command. You may need to install it if it's not already present, on Ubuntu run
apt-get install mailutils
Then, to email alice@example.com
, you can run
mail alice@example.com
and then fill in the various parts of the email. Once you've finished writing the body, press Ctrl+D
to send it.
If everything is working, great. Either way, Postfix will have done a lot of logging that might help you sort out the problem. Take a look at /var/log/mail.log
. First of all in there, you're going to see lots of output like this:
Jan 13 11:19:45 yourservername postfix/smtpd[18147]: connect from unknown[xxx.xxx.xxx.xxx]
Jan 13 11:19:45 yourservername postfix/smtpd[18147]: lost connection after AUTH from unknown[xxx.xxx.xxx.xxx]
Jan 13 11:19:45 yourservername postfix/smtpd[18147]: disconnect from unknown[xxx.xxx.xxx.xxx] ehlo=1 auth=0/1 commands=1/2
This is somebody trying to connect to your mailserver to use it to send spam. Welcome to the Internet. This will be an automated script that's just looking for a wide-open mailserver it can use to deliver a load of spam. It gets told it needs to authenticate (that's what the AUTH bit is about), which it can't do, so it just disconnects and moves on. However, amongst all that lot you should hopefully find some logging about the emails you tried to send. If something went wrong, this may help you diagnose the problem.
If you find that emails aren't arriving at the other end, not even flagged as spam, but Postfix seems happy, you might want to nip over to Part 5 where I talk about setting up a PTR DNS record for a reverse DNS lookup. It's possible that if your outgoing mail is failing this check that it's being discarded out of hand.
Since we're already listening on port 25, you should be able to receive email as well. Reply to the email you sent yourself, then run the mail
command (with no args) on your server and see what turns up. Note that if you run mail
before an email arrives, you'll get a warning that /var/mail/yourusername
can't be opened, because the mailbox won't exist until you've received an email.
All being well, we can now send and receive emails in a very basic way. Right now, though, emails are being sent and received in plain text, so theoretically anybody can snoop on your connection and read your emails. To fix this, we want to start sending and receiving emails over a connection encrypted with TLS. There's one big problem here, which is that email servers aren't required to support encrypted email transmission, which means that your mailserver can't really insist that other mailservers talk to it over a secure connection. Yes, that's really nuts, but there we are. Because you are, hopefully, able to insist that anyone using your mailserver for their email account uses an MUA that supports encryption, you can enforce encryption on the submission port (we did this earlier) and you can be sure that emails are sent from and delivered to mail clients over an encrypted connection (as well as passwords used for SASL). But when your mailserver goes to send outgoing mail to another mailserver, it might not be able to do so over an encrypted connection, and not every mailserver sending mail to your users will support encryption. Most will, and certainly if you set up encryption and send/receive mail to a major provider (Gmail, Hotmail, etc) you'll see lots of headers get added on that show that the email was sent/received securely. So it's worth setting up, just remember that email by itself isn't actually a secure medium for communication.
To set up encryption, you'll need an SSL certificate that other servers will trust. Thankfully you can get one for free these days from Let's Encrypt, even a wildcard one. Detailing how to do that is outside the scope of this post, but the folks behind Let's Encrypt have put a lot of effort into making the process as simple as possible for a wide variety of Linux distros and webservers. Head to https://letsencrypt.org and follow along to get a certificate.
Once you've got the certs you need, and you're ready to tell Postfix about them, open up main.cf
again, and you should see that it has settings for a fake certificate:
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
Change the file locations to wherever your certs are - if you've got Let's Encrypt keys, the location will be something like:
smtpd_tls_cert_file=/etc/letsencrypt/live/example.com/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/example.com/privkey.pem
If you've generated your own Diffie-Hellman key (with Let's Encrypt, depending on how you use certbot it might have generated a 2048-bit one for you), you can specify that for Postfix too, like so:
smtpd_tls_dh1024_param_file=/etc/letsencrypt/ssl-dhparams.pem
(Don't worry that the parameter has 1024
in it, it supports keys with more than 1024 bits). Again, a discussion of why you might want to create your own key is outside the scope of this tutorial, have a read of this if you want to know more: https://weakdh.org/
Oddly enough, the default installation of Postfix on Ubuntu was also using the obsolete smtpd_use_tls=yes
, where the new way of doing this is:
smtpd_tls_security_level=may
so replace that if your setup is doing things the old way. We have to use may
rather than encrypt
because we can't require that other mailservers use encryption. While we're here, we ought to also be willing to send emails over TLS if the other end supports it. So also add
smtp_tls_security_level=may
Note the difference - smtp
instead of smtpd
. This is the setting when Postfix is the client, i.e. the mailserver sending the mail. Whether this'll work depends on the TLS and certificates on the other end, not our Let's Encrypt certificates, so there's no guarantee that you'll be able to send mail over TLS. If you want to check whether it's working, then you can set smtp_tls_loglevel = 1
as well, and send an email to a reputable handler such as Gmail which will definitely offer TLS. Even without turning on logging, if you look at the email headers on the receiving end, you might find output about TLS versions and ciphers used if TLS is working.
Do a final reload of your Postfix installation
/etc/init.d/postfix reload
and your certs will be in use. Other mailservers can send you email over TLS if they support it. There are web-based services out there that allow you to test this - I tested mine using https://www.checktls.com - you give them an email address at your domain (the recipient doesn't need to exist because you don't need to get as far as actually sending an email to test the TLS setup, just the domain bit needs to be OK), and they give you a load of output about their attempt to connect to your mailserver. The service will attempt to verify your SSL certificate chain, amongst other things, so you can be sure that it's working properly.
Success!
That site may give you some useful pointers if some part of your TLS setup isn't quite right. But all being well, you'll be able to receive email over TLS.
That's it for Part 1, the next post will cover using Amavis, SpamAssassin, and ClamAV to deal with unwanted incoming mail.
When I wrote this, Ubuntu's documentation for Postfix and Amavis was recommending using the obsolete
disable_dns_lookups=yes
option when passing mail to Amavis from Postfix. The official Postfix docs note "Some obsolete how-to documents recommend disabling DNS lookups in some configurations with content_filters. This is no longer required and strongly discouraged." ↩︎