12 KiB
+++ title = "A Reasonably Secure Mailserver Installation" description = "installing postfix into a systemd-nspawn container with SPF/DKIM/DMARC support" date = "2022-04-05" extra.hidden = true +++
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 10 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 modular design -- 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 operating 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 a [temporary] 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 systemd-resolved && systemctl start systemd-resolved
[root@postfix ~]# systemctl enable postfix && systemctl start postfix
# then create the db which postfix uses to map 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.
append @uninsane.org root
to the bottom of etc/postfix/virtual
,
add virtual_alias_maps = hash:/etc/postfix/virtual
to etc/postfix/main.cf
and then (in the container) run postmap /etc/postfix/virtual
and restart the service.
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
change mail_owner
in etc/postfix/main.cf to be vmail
, and restart the service.
in etc/postfix/aliases
change root: you
to root: vmail
.
then change etc/postfix/virtual
to map to vmail
by appending this to the bottom:
@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
the postfix
Arch package includes the /var/spool
files which are now owned by vmail
, and Arch fixes package permissions on each boot.
so for these changes to take 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.
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
# update the `Domain` field
# point the `KeyFile` to /home/vmail/dkim/mx1.private (we'll generate that in a second)
# set `UserID` to `vmail`
# make sure `Socket` points to `inet:8891@localhost`
# and consider changing Canonicalization from simple/simple to relaxed/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 start opendkim && systemctl enable 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/.
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. with the container running:
[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
once it's stable, consider disabling password login for root.