ISPMail on RHEL (Part Five)

In this fifth article about configuring an email server on RHEL compatible distro AlmaLinux, we provide tools to other servers to protect themselves from emails that claim to come from our domain.

We are talking about: SPF, DKIM and DMARC.

The first two provide the destination email server, tools to check the authenticity of the incoming message, better, to check if the message they are about to receive comes from a server that is authorized to send messages on our behalf.

DMARC provides a policy about how the destination server should react once a message fails SPF and DKIM. Finally DMARC instructs how to report email traffic that is supposed to come from our email server, this providing us a metric about who is abusing our domain name or who abuse our domain name.

Let’s dig a bit deeper of these three tools.

SPF — Sender Policy Framework

This framework instructs the destination server that the “real” emails coming from our domain must come only from the specified IP(s).

From the practical point of view, we set up a record on our DNS that specifies a list of IPv4 and/or IPv6.

This is a practical example:

300 IN TXT "v=spf1 a mx ip4:192.0.2.10 -all"

In this DNS record we specify:

  • spf1 — SPF version; actually this is the only version;
  • a — The IP associated with the A record is authorized to send email;
  • mx — The IP that is indicated to receive email, can also send them;
  • ip4 — The list of authorized IP explicitly indicated to send emails;
  • -all — The behavior the receiver has to adopt when the incoming email fails the verification; -all mean: Reject all messages that fail the SPF verification (that don’t come from the specified IPs). These are just recommendations for the destination mailserver.

Your setup from a practical point of view

This is where your experience and knowledge come in play. If the only machine allowed to send email from your behalf is the machine configured in the MX record, the DNS has to be configured in such way:

300 IN TXT "v=spf1 mx -all"

This is valid for the majority of SOHO email servers

Slight variations is about the all indication; in case you’re just testing the policy you can indicate a soft fail policy: ~all means: “Don’t refuse it but mark it as suspicious”. Another indication can be: ?all thats means: “Don’t know what to do; decide by yourself” (neutral policy).

DKIM — DomainKeys Identified Mail

With DKIM, every message coming from your email server, is signed with the domain’s private key and the receiver verifies with the public key that the message has not been altered in transit.

Extra headers will be placed on the message:

DKIM-Filter: OpenDKIM Filter v2.11.0 mail.example.com 64C976007041
Authentication-Results: mail.example.com;
dkim=pass (2048-bit key, unprotected) header.d=notifications.example.net header.i=@notifications.example.net header.a=rsa-sha256 header.s=s1 header.b=kWgne00p
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=notifications.example.net;
h=content-type:from:mime-version:subject:to:cc:content-type:from:
subject:to;
s=s1; bh=nOxErjacMsolza/ZGC1cpwYUt6Z7QCpPXZsMr4/diEc=;
b=kWgne00phqP7hDFVdXcaOi8LIiNcAUha5T/t64JOcICh3SBD5u+bpMbWbZkhG6PYANeB
khZugF+GILNUjaRNisXkKWm6dwfLgeg61h8kC64jguWIH5sYxA0JzANV/tPqZo9bfDi/xb
Rd8i2XDqkCGZjcvwZWfJ+LZrpR+n6eL/J7o9vDbzHAd64+ZDX2qbRh34r5dgnzDtidrOl0
CcMd9/RS0TXEU0VDiGAo3hG3jDn9ybvlo/2WYELOtkjze+BpQw/MRYK0r8GuF7JGKNkMnX
wL5/O/eyUmBa33ss4tvIXfVRfgwe2zoMzbgif5NOvsIWo1v4Eh7PjI8JQMSsDAvA==
  • DKIM-Filter — Daemon that verified the signature, version and domain;
  • Authentication-Results — Result of authentication: pass means the authentication is OK;
  • DKIM-Signature — The signature added by the source server.

Your setup from a practical point of view

Install OpenDKIM and OpenDKIM Tools. OpenDKIM is the main daemon that sign and verify messages; OpenDKIM Tools generates the keys needed by OpenDKIM and check the correct propagation of the public key in the DNS.

  • Enable crb repository: Required to install the two packages
# dnf config-manager --set-enabled crb
  • Install OpenDKIM and OpenDKIM tools:
# dnf install opendkim opendkim-tools
  • Add the Postfix user to the OpenDKIM group: Allows the communication between Postfix and OpenDKIM via the OpenDKIM socket at: /var/spool/postfix/opendkim/opendkim.sock .
# gpasswd -a postfix opendkim

The installed packages versions are:

Installed Packages
Name         : opendkim
Version      : 2.11.0
Release.     : 0.36.el9
Architecture : x86_64
Size         : 552 k
Source       : opendkim-2.11.0-0.36.el9.src.rpm
Repository   : @System
From repo    : epel
Summary      : A DomainKeys Identified Mail (DKIM) milter to sign and/or verify mail
URL          : http://opendkim.org/
License      : BSD-3-Clause AND Sendmail
Description  : OpenDKIM allows signing and/or verification of email through an open source
             : library that implements the DKIM service, plus a milter-based filter
             : application that can plug in to any milter-aware MTA, including sendmail,
             : Postfix, or any other MTA that supports the milter protocol.

Name         : opendkim-tools
Version      : 2.11.0
Release      : 0.36.el9
Architecture : x86_64
Size         : 139 k
Source       : opendkim-2.11.0-0.36.el9.src.rpm
Repository   : @System
From repo    : epel
Summary      : An open source DKIM library
URL          : http://opendkim.org/
License      : BSD-3-Clause AND Sendmail
Description  : This package contains the tools necessary to create artifacts needed
             : by opendkim.
  • On the directory /var/spool/postfix create the opendkim directory and assign ownership to opendkim; this directory will contain the OpenDKIM socket.
  • Now we have to modify the OpenDKIM setup file at /etc/opendkim.conf . Below only the parameters I modified from the default setup.
# Requires relaxed signatures for headers (tolerant for spaces)
# and simple (well: It’s strong!) signature for body;
# a simple space on body fails the signature
Canonicalization relaxed/simple

# Daemon operating mode: Both for (s)ignature and (v)erification
Mode sv

# In case daemon crash, try to restart, max 5 times / hour
AutoRestart yes    
AutoRestartRate 5/1H

# This is the standard, recommended algorithm
SignatureAlgorithm rsa-sha256

# User the daemon run under! The socket owner is opendkim:opendkim
UserID opendkim

# Unix socket to communicate with postfix - Be sure the directory exists
Socket    local:/var/spool/postfix/opendkim/opendkim.sock

# When a message to sign arrives, this is the first table the daemon
# looks at, looking for signature to be used identified by the selector
SigningTable refile:/etc/opendkim/SigningTable

# Maps domains to the keys used to sign emails
# using the selector as entry point
KeyTable refile:/etc/opendkim/KeyTable

# Ignore these hosts when verifying incoming messages
ExternalIgnoreList /etc/opendkim/TrustedHosts

# Internal hosts authorized to sign the messages
InternalHosts /etc/opendkim/TrustedHosts
  • Comment out the following on opendkim.conf: We don’t have a default key file.
##  Gives the location of a private key to be used for signing ALL messages. This
##  directive is ignored if KeyTable is enabled.
# KeyFile /etc/opendkim/keys/default.private

Note: “This directive is ignored if KeyTable is enabled.” — I saw this is not true; if we execute any query with the command: opendkim-testkey the query fails with an error:

# opendkim-testkey -d example.com -s default -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: /etc/opendkim/keys/default.private: stat(): No such file or directory
  • The file /etc/opendkim/TrustedHosts has to contain, as declared by the .conf file above, the hosts authorized to sign and the hosts that are safe to ignore the signature when a message arrive. The content should be obvious.
127.0.0.1
::1
localhost
  • The file /etc/opendkim/SigningTable is the entry point for any signature that contains the identification of the key to be used for each domain the server handles; this has to be adapted for your domain name!
*@example.com default._domainkey.example.com

The above means: For each address at the domain name example.com use the key identified by the selector default . Selectors are useful when rotating the keys.

The above requires the following directory structure under /etc/opendkim :

.
├── KeyTable
├── SigningTable
├── TrustedHosts
└── keys
    └── example.com
        ├── default.private
        └── default.txt

Note that when the remote server has to verify one email arriving from example.com will query DNS for the record default._domainkey.example.com . We will setup this later.

  • KeyTable indicates where to find the private key that has to be used to sign the outgoing messages:
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private

The line above close the instruction on where to find the private key; so default._domainkey.example.com is the same indication coming from the SigningTable, example.com is the domain to sign, default is the selector and finally /etc/opendkim/keys/example.com/default.private is the full path of the private key to be used.

  • Assuming that you have created the directory structure as above, so you have the directory example.com under keys, you can run the following commands to create a key pair:
# opendkim-genkey -d example.com -D /etc/opendkim/keys/example.com -s default -v

This generates the keys for example.com that will be stored under /etc/opendkim/keys/example.com with the key selector default and the command will interact verbosely.

# chown opendkim:opendkim /etc/opendkim/keys/example.com/default.private
# chmod u=r /etc/opendkim/keys/example.com/default.private

The two commands above should be obvious for any linux admin and are pointed to allow the reading of the private key only to opendkim.

That’s it.

What about the public key?

Oh, yes! We also have a public key under /etc/opendkim/keys/example.com/default.txt!

What exactly does this file contain?

default._domainkey.piraneo-canepa.ch 300  IN TXT "v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtEgKRdnkv0oWuelOK6M9K5AVfsM0Z1xkku18sy40ck9+N8p4+/wv3rRSMVliEp4MfrG4JBKcofFvHwy3JWV3GRGvH5QvL5R1zD+eY8mtxrN2sXApe6qUjKOFjq9k5JEZFlEx4HutDx7gqR0rs3mnkeKToOIyUUkHFbYsp1jsEm3QqPkqqhVVAMiZyUGpnjqdfsLy3GXkGm4tuIBog295GaHIhmmjPIEMGCfRPfgGBk/JXGlKiiZLrYNwdz0en1ETq51D3TbV8LwvlZ53Q6aobdCswB0RlSWbfNqiM3kp0aZp7Tl96DXiIuC7NuN4xdahw9Zpc20JM54XvYiE6BqvCQIDAQAB"

This is the content that has to be published on your DNS! The public key the receiver counterpart has to use to validate your messages!

The above is exactly the format of the DNS record that has to be adapted to your DNS configuration interface.

Integration with Postfix

OpenDKIM and Postfix have to talk to each other; they talk with the milter protocol.

The communication and the behavior between the two has to be setup in /etc/postfix/main.cf as follows:

# DKIM operations
milter_protocol = 6
milter_default_action = tempfail
smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters

Few remarks:

  • milter_protocol — Protocol version; 6 is the most recent and safe to use with actual releases of Postfix and OpenDKIM;
  • milter_default_action — What postfix has to do when OpenDKIM fails or is not available? In such case with tempfail we inform the sender that it has to retry later;
  • smtpd_milters — Unix socket where Postfix and OpenDKIM talk together when receiving or sending emails;
  • non_smtpd_milters — Same as above but for locally generated emails (cron, php, …whatever).

DMARC: The end of the story

When everything is done, we have to tell the receiver what to do if something goes wrong; here it is the DMARC DNS record:

_dmarc        300  IN TXT   "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com; ruf=mailto:postmaster@example.com"

With this DNS record we instruct to quarantine the messages that claims to come from our domain in case they fail the above checkings and — periodically — send us two reports:

  • rua — A basic resumé of the mail the server handled and the related results; useful to check who is abusing our name;
  • ruf — A forensic report of all the failures; I’ve never encountered a server that sends these reports!

Leave a Reply

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