Securing and reducing spam is an ongoing battle.
Prerequisits:
* DNSSEC [usually managed by your domain provider and if you run bind]
* PTR [usually setup by your ISP unless you run an authoritative DNS. Implies a static IP]
* Exim >4.7
Useful Links:
http://www.dnssec-or-not.com
https://dnssec-analyzer.verisignlabs.com
https://dnschecker.org/domain-health-checker.php
https://en.internet.nl/
SPF (Sender Policy Framework)
You can put the following into it's own config eg acl_check_spf or place it in the global acl (acl_check_data:).
deny condition = ${if eq{$sender_helo_name}{} {1}} message = Nice bots say HELO first # reject messages from senders listed in these DNSBLs deny dnslists = zen.spamhaus.org # SPF validation deny spf = fail : softfail message = SPF validation failed: \ $sender_host_address is not allowed to send mail from \ ${if def:sender_address_domain \ {$sender_address_domain}{$sender_helo_name}} log_message = SPF validation failed\ ${if eq{$spf_result}{softfail} { (softfail)}{}}: \ $sender_host_address is not allowed to send mail from \ ${if def:sender_address_domain \ {$sender_address_domain}{$sender_helo_name}} deny spf = permerror message = SPF validation failed: \ syntax error in SPF record(s) for \ ${if def:sender_address_domain \ {$sender_address_domain}{$sender_helo_name}} log_message = SPF validation failed (permerror): \ syntax error in SPF record(s) for \ ${if def:sender_address_domain \ {$sender_address_domain}{$sender_helo_name}} defer spf = temperror message = temporary error during SPF validation; \ please try again later log_message = SPF validation failed temporary; deferred # Log SPF none/neutral result warn spf = none : neutral log_message = SPF validation none/neutral # Use the lack of reverse DNS to trigger greylisting. Some people # even reject for it but that would be a little excessive. warn condition = ${if eq{$sender_host_name}{} {1}} set acl_m_greylistreasons = Host $sender_host_address \ lacks reverse DNS\n$acl_m_greylistreasons accept # Add an SPF-Received header to the message add_header = :at_start: $spf_received logwrite = SPF validation passed
You will also need a TXT record publishing with the registrar and/or internal DNS.
Host name | Type | TTL | Data |
---|---|---|---|
example.com | TXT | 1 hour | "v=spf1 ip4:xxx.xxx.xxx.xxx ip6::1 -all" |
Looking at the record itself, we see that the version indicator, 'v=spf1', is followed by a typical SPF policy: first a list of systems that are authorised to send mail for the domain, then '-all', which means that all other systems are not authorised. The alternative to ending the record with '-all' is to end with '~all'. That is known as a 'soft fail', meaning that messages from non-validating systems should not be blocked, but forwarded with a tag.
DKIM (Domain Keys Identified Mail)
Before the ACL Configuration, place the following:
# # DKIM macros # # get the sender domain from the outgoing mail SENDER_DOMAIN = ${if def:h_from:{${lc:${domain:${address:$h_from:}}}}{$qualify_domain}} # # the key file name will be based on the domain name in the From header DKIM_KEY_PATH = /etc/exim/keys DKIM_KEY_FILE = dkim_rsa.private
Put the following under the ACL Configuration.
# This access control list is used to process DKIM status. acl_check_dkim: # Skip DKIM checks for all authenticated connections (probably MUAs) accept authenticated = * # Record the current timestamp, in order to delay crappy senders warn set acl_m0 = $tod_epoch # Warn no DKIM warn dkim_status = none set acl_c4 = X-DKIM-Warning: No signature found # RFC 8301 requires 'permanently failed evaluation' for DKIM signatures signed with 'historic algorithms (currently, rsa-sha1)' # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html warn condition = ${if !def:acl_c4 {true}{false} } condition = ${if eq {$dkim_verify_status}{pass} } condition = ${if eq {${length_3:$dkim_algo} }{rsa} } condition = ${if or { {eq {$dkim_algo}{rsa-sha1} } \ {< {$dkim_key_length}{1024} } } } set acl_c4 = X-DKIM-Warning: forced DKIM failure (weak hash or short key) set dkim_verify_status = fail set dkim_verify_reason = hash too weak or key too short # RFC6376 requires that verification fail if the From: header is not included in the signature # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html warn condition = ${if !def:acl_c4 {true}{false} } condition = ${if !inlisti{from}{$dkim_headernames}{true}{false} } set acl_c4 = X-DKIM-Warning: From: header not included in the \ signature, this defies the purpose of DKIM # Warn invalid or failed signatures warn condition = ${if !def:acl_c4 {true}{false} } dkim_status = fail:invalid set acl_c4 = X-DKIM-Warning: verifying signature of $dkim_cur_signer \ failed for $sender_address because $dkim_verify_reason # Add a DKIM-Received: line to the message header (regardless of DKIM status) warn add_header = Received-DKIM: $dkim_verify_status ${if \ def:dkim_cur_signer {($dkim_cur_signer with \ $dkim_algo for $dkim_headernames)} } # Set up for finalisation: add header and write to log warn condition = ${if def:acl_c4 {true}{false} } add_header = $acl_c4 logwrite = $acl_c4 accept
Again a TXT record needs to be defined.
Host name | Type | TTL | Data |
---|---|---|---|
<selector>._domainkey.example.com | TXT | 1 hour | "v=DKIM1; k=rsa; p="encrypted rsa key" |
To enable DKIM-validating mail servers to validate our digital signatures, the public key from the DKIM key pair generated earlier has to be published in the zone file of the signing domain. The first step is to generate the public key from the DKIM key file:
[root@system keys]# openssl rsa -in dkim_rsa.private -out /dev/stdout -pubout -outform PEM writing RSA key -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxMUk9Ac+aZVcqPkgSPny UOkWGrIvXcMJvUHjObpWlMNix3D74hE4KZ+Z18ZvOCUlUQGftzv0MJND/S4kXMlJ xuoxNMCKGozD/O71Rblz7RDUHxrhud2rjtSmXdmDHpH713djNiIxxZgeEeNBzfX3 UGdCJlRMVQJXUcEozqgI5BmUTsdYtrb2Trr99IZtgaLEI92yXVdholtIyt83gnhA YLnvAzOQRV4zE/eBB/pfpbFrkPh1uQQxVIBi0pARj3xk9B8yXiCXUX+gyyBrw3zi /rnXFDe0ORjtDo/3WsSrwaivJ6KjywauYgnwYAx1eNyBGnPquVR6d8OlI15YIXy+ 1wIDAQAB -----END PUBLIC KEY-----
The public key can be inserted directly into a DKIM record as follows:
dkim202205615._domainkey.example.com. 3600 TXT ( "v=DKIM1; p=" "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxMUk9Ac+aZVcqPkgSPny" "UOkWGrIvXcMJvUHjObpWlMNix3D74hE4KZ+Z18ZvOCUlUQGftzv0MJND/S4kXMlJ" "xuoxNMCKGozD/O71Rblz7RDUHxrhud2rjtSmXdmDHpH713djNiIxxZgeEeNBzfX3" "UGdCJlRMVQJXUcEozqgI5BmUTsdYtrb2Trr99IZtgaLEI92yXVdholtIyt83gnhA" "YLnvAzOQRV4zE/eBB/pfpbFrkPh1uQQxVIBi0pARj3xk9B8yXiCXUX+gyyBrw3zi" "/rnXFDe0ORjtDo/3WsSrwaivJ6KjywauYgnwYAx1eNyBGnPquVR6d8OlI15YIXy+" "1wIDAQAB")
Note the 'dkim20220615': that is the 'selector', which specifies the key pair used for signing. As you'll see shortly, the selector is also included in the 'DKIM Signature' header, so that when the receiving mail server follows the validation procedure, it knows exactly which public key to request from the DNS.
DMARC (Domain-based Message Authentication, Reporting and Conformance)
Put the following before the ACL Configuration.
# DMARC dmarc_tld_file=/usr/share/publicsuffix/public_suffix_list.dat dmarc_history_file=/var/spool/exim/opendmarc/history.dat dmarc_forensic_sender=postmaster@example.com
Put the following under the ACL Configuration.
acl_check_data: # DMARC warn dmarc_status = quarantine !authenticated = * log_message = Message from $dmarc_used_domain failed sender's DMARC policy; quarantine #control = dmarc_enable_forensic set acl_m_quarantine = 1 # this variable to use in a router/transport deny dmarc_status = reject !authenticated = * message = Message from $dmarc_used_domain failed sender's DMARC policy; reject #control = dmarc_enable_forensic warn add_header = :at_start: ${authresults {$primary_hostname}}
You'll also need to generate the key pair.
The DKIM key pair is generated as follows:
mkdir /etc/exim/keys/ cd /etc/exim/keys/ openssl genrsa -out dkim_rsa.private 2048
The new file 'dkim_rsa.private' contains the private key, which has to be kept secret. It's therefore important to ensure that the key file access rights provide appropriate security:
chmod 640 dkim_rsa.private chown root:exim dkim_rsa.private
Although generating a longer key (4096 bits, rather than 2048 bits) is an option, DKIM signatures remain valid for relatively short periods. They are, after all, used exclusively for delivering messages, which, even in the worst-case scenario, only takes a few days. Restricting the key length to 2048 bits allows DNS traffic to go via the efficient UDP protocol, whereas it would be necessary to switch to the more onerous TCP protocol if longer keys were used.
As usual, you will need to submit a DMARC record to DNS:
Host name | Type | TTL | Data |
---|---|---|---|
_dmarc.example.com | TXT | 6 hours | "v=DMARC1;p=reject;rua=mailto:example@example.com;ruf=mailto:example@example.com;fo=1;aspf=r;adkim=r;" |
I also added the following to the Transports section of exim.conf.
quarantine_delivery: driver = appendfile directory = /home/$local_part_data/Maildir/.INBOX.quarantine maildir_format delivery_date_add envelope_to_add return_path_add
Make sure the directory exists on the mail server.
I also have a cron setup to download the dat file (referenced above):
# DMARC 03 5 * * 1 cd /usr/share/publicsuffix && wget -c https://publicsuffix.org/list/public_suffix_list.dat
MTA-STS (Mail Transfer Agent Strict Transport Security)
This is just adding 2 TXT entries into DNS.
Host name | Type | TTL | Data |
---|---|---|---|
_mta-sts.exmaple.com | TXT | 1 hour | "v=STSv1; id=0002" |
_smtp._tls.example.com | TXT | 1 hour | "v=TLSRPTv1;rua=mailto:example@example.com" |
More info can on this can be found here (yes it's the UK gov)