Replacing OpenDKIM with dkimpy-milter

OpenDKIM might be the gold standard, but the DKIM standard is evolving without it, so it's time to retire it.

Written .
Updated with new build instructions.

DKIM is kind of a wax seal to email's envelope.
If you think of email as a letter in an envelope, DKIM would be the wax seal. (Image credit: Postmark)

The History of DKIM

For almost a decade, DKIM has been one of the most important anti-spam and anti-spoofing technologies available. In that little window between someone sending an email and it going out to the Internet, a domain's email server sticks a little hidden signature in each message that says that the sender address is real and the message hasn't been modified. Receiving email servers can verify the signature to prove this.

However, DKIM's age was becoming apparent. Its predecessor, DomainKeys, which dated back as far as , allowed RSA keys as small as 512 bits, and signatures with SHA-1. Naturally, for compatibility reasons, both of these glaring weaknesses made it into the final DKIM specification in (though using either was discouraged). 1024-bit RSA keys were already considered weak at the time, but some people who tried to use 2048-bit (or larger) RSA keys found out that the resulting TXT record was too big for their registrar's DNS zone editor.

All of That Sucks. How Do We Fix It?

In , the IETF created a working group to modernize DKIM, and they did (RFC 8301): 512-bit RSA keys were finally banned, and likewise, SHA-1 signatures were declared historic and MUST NOT be used for signing or verifying. 4096-bit RSA keys were also permitted, but if you're one of the unlucky ones who can't fit a 2048-bit key into your DNS zone, this doesn't help you at all.

More importantly, a new key type was added to DKIM in RFC 8463, Curve25519. The main advantage is that elliptic curve algorithms like this can provide the same or better as RSA, while using shorter keys; for example, a 256-bit EC key is as strong as a 3072-bit RSA key, so everything is faster and uses less power.

To support the old while phasing in the new, emails will be dual-signed with RSA and Ed25519, until RSA eventually falls out of style. Verifiers only need to verify one signature, so old ones will check the RSA one, and newer verifiers will check the Ed25519 one.

And that Brings Us to OpenDKIM

Since 2010, the OpenDKIM app has been one of the most popular DKIM signers and verifiers in the world. However, it's laid dormant since , with bug reports and feature requests being largely ignored. I decided it was time to make the switch to a new app that is being actively developed, dkimpy-milter.

Installing dkimpy-milter

Due to a bug in the pip version of this, I recommend building directly from source. Clone the repository to your server:

Install dkimpy-milter from source.
git clone -b master
cd dkimpy-milter
python3 install --single-version-externally-managed --record=/dev/null

It installed under /usr/local, but that's fine. Then, I used the code from their README to create a user account for it.

Create a user account for this app.
sudo adduser --system --no-create-home --quiet --disabled-password --disabled-login --shell /bin/false --group --home /run/dkimpy-milter dkimpy-milter

And now, set it to start up with your server.

sudo systemctl daemon-reload
sudo systemctl enable dkimpy-milter
sudo systemctl start dkimpy-milter
sudo systemctl status dkimpy-milter
(If you don't like systemd, I believe dkimpy-milter includes a classic SysV init script.)

Stop, check for errors, and continue.

Configuring dkimpy-milter

I was reading that dkimpy-milter was developed to be a drop-in replacement for OpenDKIM. So, I copied opendkim.conf over dkimpy-milter.conf and tried to start the service. I was greeted with plenty of warnings about unknown or unsupported options. Well, it looks like dkimpy-milter is still a work in progress on that front. I removed all of the "offending" lines.

I also adjusted permissions inside Postfix's chroot to make sure dkimpy-milter could put its socket there.

Importing your old keys?

The creators of dkimpy-milter stuck to OpenDKIM syntax. Instead of overloading options like KeyTable and SelectorTable, there are new ones like KeyTableEd25519 and SelectorTableEd25519 that behave identically. I have multiple domains with multiple selectors, so I went ahead and created new tables, started dkimpy-milter, and…

…I got an error saying none of those were valid options.

It turns out that OpenDKIM-style tables will be supported in the next version. So, for now, it's back to the drawing board.

Generating new keys

I decided to create two keys for all of my domains to share, one RSA key and one Ed25519 key. Go to whatever directory you keep your keys in, and create and secure some new keys:

Cut some DKIM keys.
dknewkey -k rsa boring-old-selector
dknewkey -k ed25519 cool-new-selector
chown dkimpy-milter:dkimpy-milter boring-old-selector.key
chown dkimpy-milter:dkimpy-milter cool-new-selector.key
chmod 440 boring-old-selector.key
chmod 440 cool-new-selector.key

You'll get two files, a .dns file with your DKIM DNS record -- go deploy those now (preferably with DNSSEC) -- and a .key file that contains your secret, that should only be readable by dkimpy-milter.

dkimpy-milter.conf Configuration

Make your configuration file look something like mine.

Relevant content of /usr/local/etc/dkimpy-milter.conf:
# List all of your domains.

# Provide your RSA key information.
KeyFile         /etc/dkimkeys/boring-old-selector.key
Selector        boring-old-selector

# Provide your Ed25519 key information.
KeyFileEd25519  /etc/dkimkeys/cool-new-selector.key
SelectorEd25519 cool-new-selector

# Copy whatever setup you had with OpenDKIM.
UserID          dkimpy-milter:postfix
UMask           007
PidFile         /var/run/dkimpy-milter/
Socket          local:/var/spool/postfix/dkimpy-milter.sock

# Make dkimpy-milter sign as well as verify, so
# Postfix can also check incoming DKIM signatures.
Mode sv

# This Postfix variable will tell dkimpy-milter what to do.
MacroList       daemon_name|ORIGINATING
MacroListVerify daemon_name|VERIFYING

Restart dkimpy-milter. It still won't do anything, though, until we have our MTA send it some messages.

Postfix Configuration

Edit your file, and look for your SMTP and Submission services. Obviously, we'll change the socket name. However, we need to add a macro name that will tell dkimpy-milter whether it will sign a message or verify one. This is new behavior in dkimpy-milter compared to OpenDKIM. Fortunately, Postfix already has this functionality via milter_macro_name:

Changes to your /etc/postfix/ file.
smtp      	inet	n	-	-	-	-	smtpd
  -o smtpd_milters=sock:/opendmarc.sock
  -o smtpd_milters=sock:/dkimpy-milter.sock
  -o milter_macro_daemon_name=VERIFYING

submission	inet	n	-	-	-	-	smtpd
  -o smtpd_milters=sock:/opendmarc.sock
  -o smtpd_milters=sock:/dkimpy-milter.sock
  -o milter_macro_daemon_name=ORIGINATING

You should check your file to see if you've referenced OpenDKIM in there. If so, go ahead and change it accordingly. When you're all done, postfix reload.

We're done!

Send some email and read the source, or hop over to my favorite DKIM testing site and send it an email. When it arrives, you should see two separate signatures, one RSA and one Ed25519.

DKIM Information:

DKIM Signature

Message contains this DKIM Signature:
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/simple;;; q=dns/txt; s=colin-ed25519;
	t=1567049647; h=from : content-type : mime-version : subject :
	message-id : date : to : from;
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple;;; q=dns/txt; s=colin-rsa; t=1567049647; h=from :
	content-type : mime-version : subject : message-id : date : to : from;

Signature Information:
v= Version:         1
a= Algorithm:       ed25519-sha256
c= Method:          relaxed/simple
d= Domain:
s= Selector:        colin-ed25519
q= Protocol:        dns/txt
bh=                 zrU0Ey8SLpTcRJGyugNEgYdAi2nzWHakc1f42rljNfg=
h= Signed Headers:  from : content-type : mime-version : subject : message-id : date : to : from
b= Data:            7PieJLlpiMRV0DYkyMeTZWavJIRhjAF2irj2qRzjlQflkpluUvr3t97NDkjBhMq6JSH+Nz/DX1o2wVhQluJxAg==

Public Key DNS Lookup

Building DNS Query for
Retrieved this publickey from DNS: v=DKIM1; k=ed25519; s=email; t=s; p=csY5YoFbP8dojeDjEIQwFmb88vdA8l6Ip7fESx39wNc=

Validating Signature

result = pass
(No, there weren't any details.) But, hey, look! A dual-signed email! And this Ed25519-aware validator checked the newer of the two signatures.

Congratulations! You're now helping to move the Internet forward, one tiny step at a time. Hopefully your dual signatures will make another somewhere Google to find out what you're doing, and why.


Wikipedia: DomainKeys [the predecessor to DKIM]
Wikipedia: DomainKeys Identified Mail [DKIM]
RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
RFC 8301: Cryptographic Algorithm and Key Usage Update to DomainKeys Identified Mail (DKIM)
RFC 8463: A New Cryptographic Signature Method for DomainKeys Identified Mail (DKIM)
DKIM, SPF, SpamAssassin Email Validator