Blog

Debian + DKIM for Dummies

Earlier this year, for reasons of privacy/love of selfhosting, I moved the DanQ.me mailing list from Mailchimp to Listmonk (there’s a blog post about how I set it up), relaying outbound messages via an SMTP server provided by my domain registrar, Gandi.

Subscribe for email updates, or discover other ways to subscribe:

Email no more than
I assume that you knew that you can get an email, no more than once per day or once per week (your choice!) of what I get up to online, right? Email not your jam: there are plenty of other options too!

Unfortunately, Gandi recently announced that they’ll no longer be providing email services for free, so rather than fork out €4/month for an email address I won’t even read, I decided to inhouse it.

And because I learned a few things while doing so, I wrote this blog post so that next time I have to configure Postfix + DKIM, I’ll know where to find a guide. If it helps you in the meantime, that’s just a bonus.

Photograph of a French Bulldog on a wooden floor playing tug-of-war using a multicoloured plaited rope (the human holding the other end of the rope is behind the camera).
If the first rule of computing is “never roll your own crypto” (based on Schneier’s Law), the second rule might be “don’t run your own mailserver”. I don’t have a good picture to illustrate that, so here’s a photo of my dog playing tug-of-war.

Postfix

Running your own mailserver is a pain. I used to do it for all of my email, but – like many other nerds – when spam reached its peak and deliverability became an issue, I gave up and oursourced it1.

Screenshot of a Weekly Digest email from DanQ.me, showing in Mozilla Thunderbird.
Fun fact: when I’m at my desktop, I use a classic desktop email application for my personal email, like it’s the 90s or something2.
Luckily, I don’t need it to do much. I just need a mail transfer agent with an (unauthenticated, but local-only) SMTP endpoint: something that Listmonk can dump emails into, which will then reach out to the mailservers representing each of the recipients and relay them on. A default install of Postfix does all that out-of-the-box, so I ran sudo apt install postfix, accepted all the default options, and put the details into Listmonk.
Screenshot showing Listmonk's SMTP configuration screen. The host "192.168.2.12" and port "25" have been entered, TLS has been set to "STARTTLS", Skip TLS verification is enabled, and Auth Protocol is set to "None".
Listmonk makes adding an SMTP server very easy, and even includes a quick “test connection” link with which you can try out your settings.

Next, I tweaked my DNS configuration to add an SPF record, and tested it. This ought to have been enough to achieve approximate parity with what Gandi had been providing me with. Not bad.

$ dig +short -t TXT danq.link
"v=spf1 a mx a:fox.q-t-a.uk ip4:83.151.206.115 ~all"
You really can’t be doing without an SPF record as a minimum these days.

I sent a test email to a Gmail account, where I noticed two problems:

Screenshot from GMail showing a message with a red slashed padlock icon, which when clicked advises that "mail.danq.link did not encrypt this message".
It turns out that since the last time I ran a mailserver “for real”, the use of TLS for inter-server communication has become… basically mandatory. You don’t strictly have to do it, but if you don’t, some big email providers will put scary security warnings on your messages. This is a good thing.

The first problem was that Postfix on Debian isn’t configured by-default to use opportunistic TLS when talking to other mailservers. That’s a bit weird, but I’m sure there’s a good reason for it. The solution was to add smtp_tls_security_level = may to my /etc/postfix/main.cf.

The second problem was that without a valid DKIM signature on them, about half of my test emails were going straight to the spam folder. Again, it seems that since the last time I seriously ran a mailserver 20 years ago, this has become something that isn’t strictly required… but your emails aren’t going to get through if you don’t.

I’ve put it off this long, but I think it’s finally time for me to learn some practical DKIM.

Understanding DKIM

What’s DKIM, then?

Diagram illustrating the flow of email from sender to recipient. On the way it's signed by the sender's mailserver's private key, which publishes the public key via DNS. Further along, the recipient's mailserver retreives the public key and uses it to verify the signature.
I’ve already got an elementary understanding of how DKIM works, which I’ll summarise below.
  1. A server that wants to send email from a domain generates a cryptographic keypair.
  2. The public part of the key is published using DNS. The private part is kept securely on the server.
  3. When the server relays mail on behalf of a user, it uses the private key to sign the message body and a stated subset of the headers3, and attaches the signature as an email header.
  4. When a receiving server (or, I suppose, a client) receives mail, it can check the signature by acquiring the public key via DNS and validating the signature.

In this way, a recipient can be sure that an email received from a domain was sent with the authorisation of the owner of that domain. Properly-implemented, this is a strong mitigation against email spoofing.

OpenDKIM

To set up my new server to sign outgoing mail, I installed OpenDKIM and its keypair generator using sudo apt install opendkim opendkim-tools. It’s configuration file at /etc/opendkim.conf needed the following lines added to it:

# set up a socket for Postfix to connect to:
Socket inet:12301@localhost

# set up a file to specify which IPs/hosts can send through us without authentication and get their messages signed:
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts

# set up a file to specify which selector/domain are used to each incoming email address:
SigningTable            refile:/etc/opendkim/SigningTable

# set up a file to specify which signing key to use for each selector/domain:
KeyTable                refile:/etc/opendkim/KeyTable

Into /etc/opendkim/TrustedHosts I put a list of local IPs/domains that would have their emails signed by this server. Mine looks like this (in this example I’m using example.com as my domain name, and default as the selector for it: the selector can be anything you like, it only matters if you’ve got multiple mailservers signing mail for the same domain). Note that 192.168.0.0/16 is the internal subnet on which my sending VM will run.

127.0.0.0.1
::1
192.168.0.0/16
*.example.com
example.com

/etc/opendkim/SigningTable maps email addresses (I’m using a wildcard) to the subdomain whose TXT record will hold the public key for the signature. This also goes on to inform the KeyTable which private key to use:

*@example.com default._domainkey.example.com

And then /etc/opendkim/KeyTable says where to find the private key for that:

default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private

Next, we need to create and secure that keypair. In /etc/opendkim/keys/example.com/, we run:

sudo opendkim-genkey -s default -d example.com
sudo chown opendkim:opendkim default.private

At last, we can configure Postfix to filter all mail through OpenDKIM by adding to our /etc/postfix/main.cf file:

milter_protocol = 2
milter_default_action = accept
smtpd_milters = inet:localhost:12301
non_smtpd_milters = inet:localhost:12301

DNS

The public key needs publishing via DNS. Conveniently, when you create a keypair using its tools, OpenDKIM provides a sample (in BIND-style) for you to copy-paste from or adapt: look in /etc/opendkim/keys/example.com/default.txt!

Screenshot from Gandi's Simple DNS management tools, showing danq.link with an SPF record as descibed earlier and a new TXT record on default._domainkey as just described.
Gandi’s DNS “Simple View” is great for one-off and quick operations, but I really appreciate that they have a BIND-style syntax “Advanced View” for when I’m making bigger and more-complex DNS configuration changes.

Once we’ve restarted both services (sudo service postfix restart; sudo service opendkim restart), we can test it!

Screenshot from GMail showing "DKIM: 'PASS' with domain danq.link".
Once the major email providers – who have the worst spam problem to deal with – say that your email signature looks good, you’re good.

So I learned something new today.

If you, too, love to spend your Saturday mornings learning something new, have a look at those subscription options to decide how you’d like to hear about whatever I get up to next.

Footnotes

1 I still outsource my personal email, and I sing the praises of the excellent folks behind ProtonMail.

2 My desktop email client also doubles as my newsreader, because, yes, of course you can still find me on USENET. Which, by the way, is undergoing a mini-revival

3 Why doesn’t DKIM sign all the headers in an email? Because intermediary servers and email clients will probably add their own headers, thereby invalidating the signature! DKIM gets used to sign the From: header, for obvious reasons, and ought to be used for other headers whose tampering could be significant such as the Date: and Subject:, but it’s really up to the signing server to choose a subset.

× × × × × × ×

Woodward Draw

This is a repost promoting content originally published elsewhere. See more things Dan's reposted.

Screenshot showing a completed game of Woodward Draw; the final word was "goat".

  • Explore the set of 4 letter words
  • Either change one letter of the previous word
  • Or rearrange all the letters of the previous word
  • Find all 105 picture words!

Daniel Linssen (via itch.io)

Woodward Draw by Daniel Linssen is the kind of game that my inner Scrabble player both loves and hates. I’ve been playing on and off for the last three days to complete it, and it’s been great. While not perfectly polished1 and with a few rough edges2, it’s still a great example of what one developer can do with a little time.

It deserves a hat tip of respect, but I hope you’ll give it more than that by going and playing it (it’s free, and you can play online or download a copy3). I should probably check out their other games!

Footnotes

1 At one point the background colour, in order to match a picture word, changed to almost the same colour as the text of the three words to find!

2 The tutorial-like beginning is a bit confusing until you realise that you have to play the turn you’re told to, to begin with, for example.

3 Downloadable version is Windows only.

Dan Q found GC54E29 WAG 16 – Elvendon Valley

This checkin to GC54E29 WAG 16 - Elvendon Valley reflects a geocaching.com log entry. See more of Dan's cache logs.

The poor little geopup’s only got tiny legs, and the 8km we’ve walked so-far has got her pretty tired-out, so this’ll be the last cache of the series before we go and find ourselves some lunch and go home. It’s been a very enjoyable series so far, and I fully intend to return to complete it (and perhaps find some of those earlier caches that I failed to spot).

For this final cache of the morning (well, afternoon: barely!), I found the likely spot straightaway and picked up something that looked out of place. Nope, no sign of the cache though; that’s strange. It took a few seconds to realise that yes, the cache was hidden behind the thing I’d picked up… it was just also covered with leaf little and detritus. Soon had it retrieved in the end, though.

A huge number of butterflies flocked in the field to our right: it was quite impressive. I’ve snapped a picture showing just one, so that I can later look up what kind of butterfly it is!

Close-up photograph showing a butterfly atop a purple flower in a grassy meadow.

×

Dan Q found GC54E1W WAG 15 – Action!

This checkin to GC54E1W WAG 15 - Action! reflects a geocaching.com log entry. See more of Dan's cache logs.

Sometimes the geo-sense “just works”. This was one of those moments. I was approaching the area and checking the distance. Then I walked straight to a likely location. Then I picked up the cache. Done and done.

Dan Q found GC72TZ5 WAG 14 – Wander through Wroxhills

This checkin to GC72TZ5 WAG 14 - Wander through Wroxhills reflects a geocaching.com log entry. See more of Dan's cache logs.

Turning South and crossing our own path, the sun came out at last and we were bathed in glorious warm light. Between that, and the familiarity of the trail we passed, the geopup and I completely forgot for a moment that we were out to look for this next cache and overshot it: we had to turn back to get to the coordinates and find the cache. TFTC!

Dan Q found GC54DR7 WAG 9 – Sausages & Bacon!

This checkin to GC54DR7 WAG 9 - Sausages & Bacon! reflects a geocaching.com log entry. See more of Dan's cache logs.

Worra lorra porkers! The geopup is a huge fan of sausages but I don’t think she understood that the cornucopia she was looking at across the field was the same thing, just a few years off being ripe. Great cache container too. TFTC, and let’s chuck an FP in because this series as a whole has definitely earned another one in my mind by now…

A French Bulldog looks out over a field which contains many pigsties and pigs.

×

Dan Q couldn’t find OK00F7 Pig City Vista

This checkin to OK00F7 Pig City Vista reflects an opencache.uk log entry. See more of Dan's cache logs.

Came by this location while doing the nearby WAG series. Was delighted to see that an OpenCache was on the route too so the geopup and I dedicated the time to a decent search. We think we found what was once the hiding place, but the cache itself was sadly nowhere to be seen.

Dan Q couldn’t find GC54DW0 WAG 11 – Panorama

This checkin to GC54DW0 WAG 11 - Panorama reflects a geocaching.com log entry. See more of Dan's cache logs.

Between nearby GC7QC7R, which acts as a spur to this series, and OK00F7, which sits on (and predates) this series, I was feeling confident of a find here… but after an extended search the geopup and I had to admit defeat. To be honest, she was willing to give up and press on immediately, having seen a muddy puddle up ahead that she wanted to play about it, and her persistent lead-pulling in that direction might have reduced both my patience and the efficacy of my search! But we found a few things that might match the hint and didn’t see success, soo… 🤷‍♂️

Dan Q found GC7QC7R Lothal

This checkin to GC7QC7R Lothal reflects a geocaching.com log entry. See more of Dan's cache logs.

As a quick diversion from the nearby WAG series, the geopup and I meandered out this way to find this cache. Once I was in the vicinity of the cache something stood out to me as unusual, so we went to pick it up… it turned out to be a chunk of wraught iron, but finding it soon pointed me in the direction of the cache. TFTC.

Dan, wearing a white t-shirt and with a red dog lead hanging over his shoulder, stands in a forest, gesturing down a path to a small French Bulldog following him.

×

Dan Q found GC610PB WAG 12 – Wroxhills Wood

This checkin to GC610PB WAG 12 - Wroxhills Wood reflects a geocaching.com log entry. See more of Dan's cache logs.

A quick and easy find: we walked straight to where the coordinates said and there it was. My phone, having been rebooted during the last leg, was now behaving much better at narrowing down a satellite fix!

These woods are really quite amazingly beautiful and serene. It’s quiet and calm here, and both the geopup and I really appreciate the excuse to have come here.

Next, it’s time for a quick diversion from the series to find nearby GC7QC7R!

Dan Q found GC54DXQ WAG 13 – Oh deer!

This checkin to GC54DXQ WAG 13 - Oh deer! reflects a geocaching.com log entry. See more of Dan's cache logs.

Took several attempts to find the correct hiding place and the poor geopup – who didn’t like the tight-knit undergrowth here except when it suited her (when she wanted to chase after a pheasant!) – eventually had to be tied to a tree while I pressed-on without her to get the cache in hand. Phew! TFTC.

A French Bulldog on a forest path pulls against her lead.

×

Dan Q couldn’t find GC78WN6 WAG 8 – Battle Farm

This checkin to GC78WN6 WAG 8 - Battle Farm reflects a geocaching.com log entry. See more of Dan's cache logs.

This was the moment when I found myself wishing that my dedicated GPSr unit was with me and working, as my phone’s GPS fix started jumping all over the place. The geopup and I made a few valiant attempts to search in the obvious places, criss-crossing our way through some quite fierce brambles as we did so, but without success. Eventually, we had to move on and chalk this one up as a DNF. I’ve no reason to believe it’s not out there somewhere, but it’ll be a job for somebody whose satellite navigation kit is playing ball.

Boot stepping into long bramble undergrowth.

×

Dan Q found GC54DF1 WAG 7 – Fuming!

This checkin to GC54DF1 WAG 7 - Fuming! reflects a geocaching.com log entry. See more of Dan's cache logs.

The geopup made herself useful for this cache, running straight to the cache location. (I suspect that some prior canine visitor may have left their mark somewhere very near to the cache, and she was more interested in smelling that than she was at helping me find the container, but I can dream of a dog who’s a useful geocaching assistant, can’t I?) TFTC.

A French Bulldog standing by a rural "gas pipeline" warning sign.

×

Dan Q posted a note for GC54DEF WAG 6 – Water Break

This checkin to GC54DEF WAG 6 - Water Break reflects a geocaching.com log entry. See more of Dan's cache logs.

Working our way through the first part of the WAG series, we unfortunately had to skip this one without an adequate search: the area was crawling with ramblers, consulting their maps and chatting with one another, and I didn’t have a good excuse to stop and search. Maybe next time!