postfix: Asserting You’re Not a Spammer

This is a companion piece to articles I wrote about configuring postfix & dovecot 2.4 to handle system and virtual users on a Debian Trixie VPS (the config file details are also available here).

Table of Contents

Nowadays, when you set up an email server, if you don’t take steps to assert you’re not a spammer — and are following the rules to not be a spammer — you are likely to see your emails rejected by at least some sites because they might be spam.

There are several things you should do to configure your mail server so it doesn’t get deemed to be a spammer. First and foremost, don’t send spam!

But beyond that, you should:

  • configure your DNS to include an SPF (Sender Policy Framework) record;
  • configure your DNS to include a DMARC (Domain-based Message Authentication, Reporting, and Conformance) record; and,
  • set up a DKIM (DomainKeys Identified Mail) server to sign your emails with the private part of a public/private key pair, and publish the corresponding public keys (through your DNS) so other sites can verify emails claiming to be from you actually came from an authorized server.

DMARC depends on SPF and DKIM; you can’t set it up without setting up the other two first.

SPF: It’s Not About Sunscreen

Sender Policy Framework provides a simple check on the validity of emails: namely, are they coming from an IP address that is, in fact, authorized to send them?

This is an issue because the researchers who created our basic internet protocols did not foresee anyone would want to pretend to be someone they’re not1.

Setting SPF up is easy. Just add a specially-formatted TXT record to your DNS table:

"v=spf1 mx a ip4:104.168.133.159 -all"

That’s the entry I have in the DNS table for my site make-america-smart-again.com. Translated, it says “if you get an email from make-america-smart-again.com and it’s not coming from this one particular IP address, consider it to be spam.”

return to table of contents

Setting Up DKIM

DomainKeys Identified Mail uses public/private keys to sign every email your server sends. It does this by encrypting parts of the email header and body with a private key. The recipient uses the public key to decrypt that token. If the decrypted token doesn’t conform to the header and body of the email, the recipient concludes the email didn’t come from a server authorized to send mail for the sender domain (that’s where SPF comes into play).

I implemented DKIM on my mail server using OpenDKIM. Start by installing the necessary packages:

sudo aptitude update
sudo aptitude install opendkim opendkim-tools

The install should’ve created a file in the /etc directory called /etc/opendkim.conf. Edit it to include the following lines (I’m only showing you the ones that override the defaults, or which are important to understanding how DKIM works):

# these lines control how DKIM logs its actions. I don't need to be told about its successes,
# but I do want to know as much as I can about its failures
Syslog                      yes
SyslogSuccess         no
LogWhy                   yes

# Canonicalization simple means don't modify anything in the email,
# relaxed means adjust whitespaces
Canonicalization        relaxed/simple

# we want DKIM to both (s)ign emails going out and (v)erify emails coming in
Mode                    sv

# In Debian, the "From" header is oversigned, because it is often 
# the identity key used by reputation systems and thus 
# somewhat security sensitive.
OversignHeaders         From

# since I'm providing email services for several different virtual domains,
# configure DKIM for multi-domain tables
KeyTable                /etc/opendkim/key.table
SigningTable            refile:/etc/opendkim/signing.table
InternalHosts           /etc/opendkim/trusted.hosts

# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
# using a local socket with MTAs that access the socket as a non-privileged
# user (for example, Postfix).
#
# I just realized since I ended up accessing OpenDKIM via a port rather than
# a socket I could probably revert to the default 002 umask. See below on
# why I'm using a port rather than a socket.
UserID                  opendkim:opendkim
UMask                   007

# this defines the port postfix will use to contact OpenDKIM
Socket                  inet:8891@localhost

# the location of the process id file. you have to ensure
# the directory exists and is accessible
PidFile                 /run/opendkim/opendkim.pid

You now have to set up three files, the ones KeyTable, SigningTable and InternalHosts reference. That’s complicated by the fact you need root access to even get into the /etc/opendkim directory where the files reside. So, switch to root via su2.

Here’s the first, /etc/opendkim/key.table for KeyTable (you can call the files whatever you want, but you have to use the correct name in the /etc/opendkim.conf configuration file):

# /etc/opendkim/key.table
default._domainkey.theboilingfrog.net theboilingfrog.net:default:/etc/opendkim/keys/theboilingfrog.net/default.private
default._domainkey.make-america-smart-again.com make-america-smart-again.com:default:/etc/opendkim/keys/make-america-smart-again.com/default.private
default._domainkey.jumpforjoysoftware.com jumpforjoysoftware.com:default:/etc/opendkim/keys/jumpforjoysoftware.com/default.private
default._domainkey.ardsleyhigh73.com ardsleyhigh73.com:default:/etc/opendkim/keys/ardsleyhigh73.com/default.private

I find this file’s layout rather messy. What it’s doing is linking TXT entries in various DNS tables (e.g., default._domainkey.theboilingfrog.net is a TXT entry in theboilingfrog.net’s DNS table) to a particular private key (i.e., a file located at /etc/opendkim/keys/theboilingfrog.net/default.private). There’s one entry for each virtual domain.

We’ll look at how you create the various default.private files in a moment. For now, here’s what the signing table file looks like:

# /etc/opendkim/signing.table
# sender address pattern -> key name in key.table
*@theboilingfrog.net default._domainkey.theboilingfrog.net
*@make-america-smart-again.com default._domainkey.make-america-smart-again.com
*@jumpforjoysoftware.com default._domainkey.jumpforjoysoftware.com
*@ardsleyhigh73.com default._domainkey.ardsleyhigh73.com

This file defines which keys are used to sign emails coming from which users in each of the domains. Since I’m using a single public/private key pair for every email in each domain, the email address definition starts with an asterisk.

Note that even though, on my system, theboilingfrog.net is the system domain, not a virtual domain, it’s still included here. That’s because we want its emails to be signed and verified, too.

Finally, here’s the trusted hosts file:

# /etc/opendkim/trusted.hosts
# hosts whose mail we SIGN (not verify); localhost must be here since
# locally submitted mail also flows through the milter
127.0.0.1
::1
localhost
104.168.133.159

These are the only hosts which should be signing any of my emails.

Let’s talk about creating the public/private keys, on which DKIM depends. There’s a utility in the OpenDKIM package which creates them, opendkim-genkey. But you also have to create a directory within /etc/opendkim/keys for each domain you’re signing (I’m just showing the commands for one of my domains):

mkdir -p /etc/opendkim/keys/theboilingfrog.net

opendkim-genkey -b 2048 -d theboilingfrog.net -s default -D /etc/opendkim/keys/theboilingfrog.net

chown -R opendkim:opendkim /etc/opendkim/keys
chmod 600 /etc/opendkim/keys/theboilingfrog.net/default.private

I’m pretty sure sudoing these commands won’t work, because only root can get into /etc/opendkim/keys. I just became root to execute these commands.

While the word default in these commands could be any word, it must be the same word you use in the key table and the signing table. Basically, it’s the name of the public/private key set.

BTW, you could defer issuing line 5 until you’re done creating all the key folders and the keys, but I find it cleaner to keep re-issuing it for each domain.

Now comes the hardest part of setting up DKIM: you must create a DKIM TXT entry in each domain’s DNS table. That sounds like it should be simple — just create the entry, paste the necessary contents into it (which is simply the contents of each public key file) and you’re done.

Unfortunately, DNS TXT entries cannot be more than 255 characters long…and Base64 encoded 2048 bit DKIM keys are always longer than that.

Supposedly, all modern DNS editors (e.g., the one at the place hosting your VPS) can handle this by some behind the scenes trickery. In my experience, at Hostwinds.com at least, the only way to get the entries created is to send the public key text for each domain to tech support and have them create the records.

You get the TXT contents by doing a cat on each of the public key files (which, if you’ve been using the word default like I did, should be named default.txt):

 > cat default.txt
default._domainkey      IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; "
          "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5bfaWUwPmxcgThdrcSAdXd2Hh8QepZ+8NV5cAr5JPxE5TsH8yELzsstkOjxiAcE76KOLOC+mDrccFYDh4OCb1RGnIJWvi1qhsNvLvnVGViNcn+K3KyDdntZznaTfQqf+tkO7FhFXMtSIX0MuqRFjVsr8vfoEodIpjbGEqDufMXUQeSV6xTgkQYIR5bwx8aKfJtxJmwFHmPZUWz"
          "J3rGAEE9xdUdPNSGcPfyZDi3mXUwheFTSzL9a5NVo/caeIY8BaHW+IVYmp0JG5Q4Lb9eo2NlK6cFUL0GuLu7TQep3veyp4/o/AmJuiseG6gWHReHexCrUbsplAxybvqYr0eUEVRQIDAQAB" )  ; ----- DKIM key default for theboilingfrog.net

The part you want to get into the TXT record starts with v= and ends with AB in this example (i.e., it’s the part between the opening parentheses and the closing parentheses).

Here’s what this all looks like in one of my DNS tables:

Note that the name of the TXT record is default._domainkey. That word default is important — once again, it must match the word you used to name the public/private key pairs (which has been default in these articles I’ve been writing).

Also note that not all of the TXT record’s value is displayed — that’s that 255 character limit in play again.

return to table of contents

Setting Up DMARC

Domain-based Message Authentication, Reporting, and Conformance DNS records are simple TXT records that spell out how recipients should handle spam emails that purport to be coming from your authorized mail server. Here’s the value of the TXT record for one of mine:

v=DMARC1; p=none; rua=mailto:765146b9@mxtoolbox.dmarc-report.com; ruf=mailto:765146b9@forensics.dmarc-report.com; fo=1

The name of the DMARC TXT record must be _dmarc.

Here’s how you read the DMARC specification. Fields are separated by semicolons:

v=DMARC1Required. Must be first. Says we’re using version 1 of DMARC
p=noneRequired. Defines what the recipient should do if a spam email is encountered. Choices are:

none – just monitor, do nothing else
quarantine – send failed emails to spam/junk
reject – block/drop failed emails
rua=mailto:765146b9@mxtoolbox.dmarc-report.comOptional but recommended. Where to send aggregate reports of spam emails received. Not sure where I got the email address from. Maybe from an online utility that creates DMARC records?
ruf=mailto:765146b9@forensics.dmarc-report.comOptional. Where to send detailed forensic reports of spam emails. Not sure where I got the email address from. Maybe from an online utility that creates DMARC records?
fo=1Optional. When to send a forensic report. Choices are:

0 – default if omitted. Only if both SPF and DKIM fail to produce a DMARC pass.

1 – if either SPF or DKIM fails to produce a DMARC pass. What you have. Much noisier, good for debugging.

d – if DKIM signature fails, regardless of alignment.

s – if SPF fails, regardless of alignment.

You can combine multiple choices, separated by colons.

return to table of contents


  1. The internet was originally set up to provide a fault-tolerant way of government/military agencies communicating with each other after a global thermonuclear attack. The players implicitly trusted each other — you couldn’t be a member of the collective unless you’d already been vouched for. And maybe they were just somewhat more naive than the run of the mill human. ↩

  2. I know this isn’t best practice…but I don’t know any other way of gaining access to that directory. Simply going superuser with an account which can get superuser privileges doesn’t work, at least not on my system. ↩

Leave a Comment

Your email address will not be published. Required fields are marked *

Categories
Archives