A Reasonably Secure Mailserver Installation
2022/04/06
i need software to receive emails, and possibly to send them too. i.e., a mailserver. the mature mailserver implementations were all written in a time where security was even worse than today. Postfix is among the better ones, but even it has a fair number of CVEs. its intended operation -- where it writes to mailboxes owned by different users -- relies on elevated access control. although the risks are mitigated by its design around separation of concerns -- where only select portions of code get elevated permissions -- and the linux capabilities system, i still would not feel comfortable running this without isolating it from other applications on the same machine.
enter systemd-nspawn. nspawn is an extremely lightweight container. it's more of a transparent chroot: package up the userspace of some linux distribution, place it in a directory, and then nspawn uses the host kernel and performs the whole PID 1 boot sequence of that chroot, virtualizing all the fs access and isolating the processes/etc. we'll use this to create a container dedicated to postfix.
Installation
start by creating the rootfs and launching it as a container. this assumes the host is running Arch:
[root@host /]# pacman -S arch-install-scripts
[root@host /]# mkdir /opt/postfix
[root@host /]# pacstrap -c /opt/postfix base postfix openbsd-netcat opendkim perl
[root@host /]# systemd-nspawn -D /opt/postfix
># passwd # choose some password you can remember for the rest of setup
># exit
if you're ssh'd into the host, you need to relax some security settings in the container before it'll let you login:
[root@host /opt/postfix]$ mv etc/securetty etc/securetty.OLD
[root@host /opt/postfix]$ mv usr/share/factory/etc/securetty \
usr/share/factory/etc/securetty.OLD
# then comment out the line containing securetty in usr/lib/tmpfiles.d/arch.conf
configure myhostname
and mydomain
in /opt/postfix/etc/postfix/main.cf
(it's fine for these to be the same: i use uninsane.org
for both)
using the --network-veth
flag, systemd will create a NAT'd network and expose the downstream to the container.
we can then forward ports across the NAT just like you would forward ports from your router to your PC/server (port 25 here is the SMTP port):
[root@host /]# systemd-nspawn -b --network-veth -p 25:25 -D /opt/postfix
postfix login: root
Password: <enter it>
[root@postfix ~]# systemctl enable --now systemd-resolved
[root@postfix ~]# systemctl enable --now postfix
# then create the db which maps email address to linux user accounts:
[root@postfix ~]# newaliases
for the record, /etc/postfix/aliases
contains the mappings consumed by newaliases
. the defaults work for us now but you'll
want to tweak them later.
like HTTP, the SMTP grammar is human friendly. we can verify our setup with netcat.
you can do this from within the container (substitute localhost
for <container>
), or from another box on your LAN (substitute the host's IP/name for <container>
).
you probably don't want to expose this to the WAN yet:
$ nc <container> 25
helo uninsane.org
mail from:<test@uninsane.org>
rcpt to:<root@uninsane.org>
data
this is a test.
.
quit
mail should show up in the container at var/spool/mail/root
.
if this is intended as a single-user mailserver, you might want a catch-all mail rule:
[root@postfix /]# echo '@uninsane.org root' >> /etc/postfix/virtual
[root@postfix /]# echo 'virtual_alias_maps = hash:/etc/postfix/virtual' >> \
/etc/postfix/main.cf
[root@postfix /]# postmap /etc/postfix/virtual
[root@postfix /]# systemctl restart postfix
try the nc
command from above again, but use rcpt to:<anything@uninsane.org
and
the mail should be appended to that same /var/spool/mail/root
file.
Non-Root User
we'd prefer to be able to read mail without being root. so create a user dedicated to holding the mailboxes:
[root@postfix /]# useradd --create-home --user-group vmail
edit etc/postfix/main.cf
:
- mail_owner = postfix
+ mail_owner = vmail
edit etc/postfix/aliases
:
- root: you
+ root: vmail
edit etc/postfix/virtual
:
- @uninsane.org root
+ @uninsane.org vmail
update the database mappings and then restart the services:
[root@postfix /]# newaliases
[root@postfix /]# postmap /etc/postfix/virtual
[root@postfix /]# postfix set-permissions
[root@postfix /]# systemctl restart postfix
the postfix
Arch package includes the /var/spool
files which are now owned by vmail
, and AFAICT Arch fixes package permissions on each boot.
so for these changes to take permanent effect, you'll need to edit lib/systemd/system/postfix.service
to apply set-permissions
on each boot:
- ExecStart=/usr/bin/postfix start
+ ExecStart=/usr/bin/bash -c '/usr/bin/postfix set-permissions \
+ && /usr/bin/postfix start'
because systemd limits postfix's ability to write outside of /var/spool
, you'll need to change which files postfix tries to enforce permissions on if you want this to succeed.
in etc/postfix/postfix-files
, comment out every line which starts with one of:
$config_directory
$daemon_directoy
$sample_directory
$readme_directory
$html_directory
$shlib_directory
$manpage_directory
since Arch manages these (correctly), you're not really losing anything.
run that nc
command again: this time mail should show up in /var/spool/mail/vmail
,
and that file should be owned by the vmail
user instead of root
.
now we can work on the WAN side of things. to prevent spoofing & improve the likelihood that your messages will be accepted by other servers, you'll want to add some DNS records to your zone file:
- a SPF DNS record (instructs recipients to enforce that your message originates from a specific IP)
- a DKIM DNS record (signs the message content with a key owned by your mailserver)
- a DMARC DNS record (allows you to receive reports from recipient mailservers)
and of course you'll need a MX record so others know where to send mail.
DKIM and DNS
we installed opendkim during the earlier pacstrap
invocation: now we'll configure it to sign
outgoing messages:
[root@host /opt/postfix]$ cp usr/share/doc/opendkim/opendkim.conf.sample \
etc/opendkim/opendkim.conf
open etc/opendkim/opendkim.conf
in an editor and:
- update the
Domain
field - point the
KeyFile
to/home/vmail/dkim/mx1.private
(created later) - set
UserID
tovmail
- make sure
Socket
points toinet:8891@localhost
- and consider changing Canonicalization from
simple/simple
torelaxed/simple
then append this to etc/postfix/main.cf
:
# For use by dkim milter
smtpd_milters = inet:localhost:8891
non_smtpd_milters = $smtpd_milters
milter_default_action = accept
generate the keys (run this as the vmail
user):
[vmail@postfix /home/vmail]$ mkdir dkim && cd dkim
[vmail@postfix /home/vmail/dkim]$ opendkim-genkey -r -s mx1 -d uninsane.org
start the service:
[root@postfix /]# systemctl enable --now opendkim
add the mx1._domainkey
TXT record (documented in /home/vmail/dkim/mx1.txt
) into your zone file.
then run the nc
example again. you should get mail that has an Authentication-Results
header -- which fails,
since we didn't sign our message.
using the postfix sendmail
command we should be able to send something with a valid signature:
[root@postfix /]# sendmail test@uninsane.org
this message should be signed
.
[root@postfix /]# cat /var/mail/vmail
[...]
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=uninsane.org; s=mx1;
t=[...]; bh=[...]
h=Date:From;
b=[...]
Message-Id: <YYYYMMDDTTTTTT.NNNNNNNNNNN@uninsane.org>
Date: [...]
From: root@uninsane.org
this message should be signed
then add a SPF DNS record and a DMARC record to receive delivery reports.
if you're running a large mail server it would be good to install opendmarc
to send delivery reports to other servers (like mine!), but i'll skip that here.
throw in the MX record, and your zone file should look like this:
; mailserver shares an IP with the rest of uninsane.org.
@ MX 10 uninsane.org.
; Sender Policy Framework:
; +mx => mail passes if it originated from the MX
; +a => mail passes if it originated from the A address of this domain
; +ip4:.. => mail passes if it originated from this IP
; -all => mail fails if none of these conditions were met
@ TXT "v=spf1 ip4:203.0.113.1 a mx -all"
; DKIM public key:
mx1._domainkey TXT "v=DKIM1; k=rsa; s=email; p=<big long string>"
; DMARC fields <https://datatracker.ietf.org/doc/html/rfc7489>:
; p=none|quarantine|reject: what to do with failures
; sp = p but for subdomains
; rua = where to send aggregrate reports
; ruf = where to send individual failure reports
; fo=0|1|d|s controls WHEN to send failure reports
; (1=on bad alignment; d=on DKIM failure; s=on SPF failure);
; Additionally:
; adkim=r|s (is DKIM relaxed [default] or strict)
; aspf=r|s (is SPF relaxed [default] or strict)
; pct = sampling ratio for punishing failures (default 100 for 100%)
; rf = report format
; ri = report interval
_dmarc TXT (
"v=DMARC1;p=quarantine;sp=reject;fo=1:d:s;"
"rua=mailto:admin+mail@uninsane.org;ruf=mailto:admin+mail@uninsane.org"
)
Validation
validate your DMARC record (and DKIM, SPF if you want): https://dmarcian.com/dmarc-inspector/.
confirm that your domain/IP isn't blacklisted: https://multirbl.valli.org/.
try sending/receiving mail: https://www.appmaildev.com/en/dkim.
if these fail, check journalctl -u postfix
. if there's no indication of traffic, it may be that your ISP blocks outbound port 25.
you can check for that with nc -vz gmail.com 25
(will exit 0 if the port is open, hang if the port is blocked).
less probably, your ISP might block inbound port 25. check for that here: https://canyouseeme.org/.
in my case, Centurylink blocks both directions, so i can't even use this setup to receive mail. for this case, i'll explore running postfix on a non-standard port and using a mail forwarder or transparent proxy in a subsequent blog post.
but if your mail server is working, then instruct systemd to launch the container when the host boots. while the container's active, run:
[root@host /]# ln -s /opt/postfix /var/lib/machines/postfix
[root@host /]# machinectl enable postfix
[root@host /]# systemctl enable machines.target
alternatively, you could move the whole machine into /var/lib/machines/postfix
instead of symlinking it.
populate /etc/systemd/nspawn/postfix.nspawn
(you may need to create the directory) with the settings we used earlier:
[Network]
VirtualEthernet=on
Port=25:25
then you can stop the machine, restart it, and administer it:
[root@host /]# machinectl stop postfix
[root@host /]# machinectl start postfix
[root@host /]# machinectl login postfix
you now have a postfix instance which starts on boot and can send/receive mail as long as port 25 is accessible. later on you may want to provide client access in a friendlier way than directly reading the spool or invoking sendmail
. that could be done by installing something like dovecot.