Do you use let's encrypt?

4934 votes ~ 18 comments


XML Logo

Posted by lee on Mon 9 Nov 2015 at 11:40
Tags: , ,
My previous config from 2011 suggested the use of custom router and transport files for supporting selective domain use for DKIM in exim, but since the Debian package contains "ifdef" for expansions, you can achieve the same effect with lookups. Assuming the correct DNS records have been set up, add the key into /etc/exim4/dkim-foo.key and make it readable by the exim user (Debian-exim). Create /etc/exim4/dkim_senders with a list of addresses that should have mail signed.
Create /etc/exim4/dkim_domains with the per-domain configs selector=foo key=/etc/exim4/dkim-foo.key canon=relaxed selector=bar key=/etc/exim4/dkim-bar.key
Create /etc/exim4/conf.d/main/00_local_dkim (if you're using split config)
 DKIM_DOMAIN =      ${lookup{$sender_address}lsearch*@{/etc/exim4/dkim_senders}{$sender_address_domain}{}}

 ## make the following active instead if all mail from selected domains should be signed
 # DKIM_DOMAIN =      ${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim_domains}{$sender_address_domain}{}}

 DKIM_SELECTOR =    ${extract{selector}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim_domains}}}{$value}{}}
 DKIM_PRIVATE_KEY = ${extract{key}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim_domains}}}{$value}{}}
 DKIM_CANON =       ${extract{canon}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim_domains}}}{$value}{relaxed}}
 DKIM_STRICT =      ${extract{strict}{${lookup{$sender_address_domain}lsearch*@{/etc/exim4/dkim_domains}}}{$value}{false}}
Run update-exim4.conf and reload exim. For addresses not listed in /etc/exim4/dkim_senders exim should not attempt DKIM signing. This config assumes that the signing domain is the sender's domain. It's reasonable, but not necessarily always true. It also assumes users on the same sender domain use the same signing key. If necessary it wouldn't be too hard to swap the lookups around to allow domains to support different selectors.


Posted by lee on Tue 10 Jun 2014 at 21:35
Tags: , , , ,

A previous entry from 2011 on the subject on using SES and Exim still comes up in web searches. As with the 2011 config, this assumes a standard Debian/Ubuntu exim4 deployment using a split-file config.

SES now allows authenticated relay via port 25, but also submission (port 587). Logically it makes more sense to think of SES as a Submission host since it uses ESMTPSA and makes modifications to the mail in transit, so I explicitly create a submission transport at /etc/exim4/conf.d/transport/40_local_submission

  debug_print = "T: remote_submission for $local_part@$domain"
  driver = smtp
  port = 587
  hosts_require_auth = *
  hosts_require_tls = *
Then I add in a router which activates based on the sender at /etc/exim4/conf.d/router/180_local_aws-ses
  debug_print = "R: send_via_ses for mail from $sender_address"
  driver = manualroute
  host_find_failed = freeze
  domains = ! +local_domains
  senders = AWS_SES_SENDER
  transport = remote_submission
  route_list = * AWS_SES_SERVER
The site specific configuration /etc/exim4/conf.d/main/00_local_aws-ses
## sender email addresses to be routed via SES, one-per-line
AWS_SES_SENDER = lsearch*@;/etc/exim4/ses_senders

## nearest SES ingress point
And assuming the standard auth handling is in place, add a line to /etc/exim4/passwd.client
Note, these are SMTP specific credentials and not the AWS credentials previously used. Run update-exim4.conf and then restart exim.


Posted by lee on Sun 15 Dec 2013 at 17:27
Tags: ,

By the end of 2013 the minimum key-size for signed TLS certificates will be 2048 bits. Older 1024 bit certificates will be revoked and web browsers will throw errors on anyone attempting to connect using 1024 bit keys.

Unfortunately, in the context of server-to-server opportunistic encryption, such as used by ESMTPS, this is a dangerous choice. 1024 bit encryption is still far harder to decode than plaintext (which is currently the usual fall-back delivery).

Sadly that's what's happened in some Debian derived releases of Exim4 (using GnuTLS). An affected mail server will likely see something like the following in the logs:

TLS error on connection to [] (gnutls_handshake): The Diffie-Hellman prime sent by the server is not acceptable (not long enough).

(Even worse, the choice to increase the minimum prime size also breaks connections to many servers that implement EDH - i.e. Ephemeral Diffie-Hellman with smaller encryption keys such as allowed by export.)

The quickest way to check if a remote mail server has a small key is to run a command such as:

$ echo QUIT | openssl s_client -connect -starttls smtp 2>/dev/null| grep "public key is"

If the server public key is less than 2048 bit in size, you may want to contact the postmaster or other administrative contact to let them know they need to generate a new server key.[1] If the key is 2048 bit and above, you may need to use another tool to see what's happening.

As of Exim 4.80 there is a new transport option, tls_dh_min_bits, which lets the minimum key size to be set.

So if this affects you, you could modify the minimum key size for the default transport (add " TLS_DH_MIN_BITS = 512 "). However, for these sort of workarounds it's my preference to add in special-case configuration.

(Filenames assume the split-config of debian deployments.)

Create /etc/exim4/conf.d/transport/40_temp_tls_smallprime (a modified copy of 30_exim4-config_remote_smtp)

  debug_print = "T: remote_smtp_tls_smallprime for $local_part@$domain"
  driver = smtp
  multi_domain = false
  tls_dh_min_bits = 512
dkim_domain = DKIM_DOMAIN
dkim_selector = DKIM_SELECTOR
dkim_private_key = DKIM_PRIVATE_KEY
dkim_canon = DKIM_CANON
dkim_strict = DKIM_STRICT
dkim_sign_headers = DKIM_SIGN_HEADERS

Then create /etc/exim4/conf.d/router/177_temp_tls_smallprime (i.e. so it appears before the normal external router) with two entries, one for mail servers that have <2048 keys, and ones that negotiate EDH.

  debug_print = "R: dnslookup_tls_smallprime for $local_part@$domain"
  driver = dnslookup
  domains = ! +local_domains : ! +relay_to_domains
  condition = ${if forany{${lookup dnsdb{>: mxh=$domain}}}{match_domain{$item}{+smallkey}}}
  transport = remote_smtp_tls_smallprime
  same_domain_copy_routing = yes

  debug_print = "R: dnslookup_tls_edh for $local_part@$domain"
  driver = dnslookup
  domains = ! +local_domains : ! +relay_to_domains
  condition = ${if forany{${lookup dnsdb{>: mxh=$domain}}}{match_domain{$item}{+tlsexport}}}
  transport = remote_smtp_tls_smallprime
  same_domain_copy_routing = yes

Then you add in the lists of mail servers to your main config. For example adding the following to /etc/exim4/conf.d/main/10_temp_smallprime

## Servers that present small keys
domainlist smallkey = :
## Hosts that use large keys to sign small "export" size keys for encryption
domainlist tlsexport = *

[1] It may be more complex than that, of course.[3] While most MTAs operating in client mode would be expected to deal with a 2048 bit key, there are known older email clients that would not be able to cope with keys larger than 1024 bits. Servers may need to maintain backward compatibility for these older clients.

[2] Using GnuTLS to inspect the key negotiation is slightly more involved since you need to do the SMTP negotiation

$ gnutls-cli --crlf --insecure --starttls --port 25
EHLO Hello
250 HELP
220 TLS go ahead
(Hit Ctrl-D)
*** Starting TLS handshake
- Ephemeral Diffie-Hellman parameters
- Using prime: 768 bits
- Secret key: 766 bits
- Peer's public key: 767 bits

[3] You'll note that the example above, adapted from a real "secure email provider", tops out at 768 bits. Which is still the US export limit for asymmetric algorithms used by exported software not in the public domain. They use a 2048 bit key merely to sign the small key used to encrypt the session.


Posted by lee on Fri 9 Aug 2013 at 19:18
Tags: ,

With no notice, email provider shut down following undisclosed requests from the US government. I find the issue surrounding the shutdown personally concerning, but I've also got a big queue of undelivered mail for (former) customers.

The service allowed people to use their own domains with the service and point the mx records at lavabit's mail servers. These mail servers are no longer responsive - they're not rejecting the mail, just the connection, which means normal smtp servers such as exim will continue to retry.

I'm confident these servers won't be back in any useful form any time soon, therefore anything still using them as MX records is undeliverable right now. Normally I'd use Exim's retry configuration but I want to send a specific message in the rejection.

So instead, I've used a quick-n-dirty router at /etc/exim4/conf.d/router/101_local_lavabit_is_dead

  debug_print = "R: lavabit_is_dead for $local_part@$domain"
  driver = redirect
  condition = ${if forany{${lookup dnsdb{>: mxh=$domain}}}{match_domain{$item}{}}}
  data = :fail: mail for $domain rejected because has been shut down
A more traditional accelerated timeout would probably look like: timeout_connect_MX F,1h,20m


Posted by lee on Wed 5 Sep 2012 at 23:53
Tags: , ,

If you attempt to send an email to multiple addresses that are handled by google's email servers, but that have multiple domains, only the mail for one of the domains will be accepted.

All the other addresses will receive a temporary rejection message:

451-4.3.0 Multiple destination domains per transaction is unsupported.  Please
451 4.3.0 try again.  random.string

What happens next is up to the logic of the server sending the mail. It'll try sending the messages to the next in the MX priority list - which for google-handled domains are going to be the same. And again, all but one of the delivery domains will get rejected.

I've yet to see a mail bounce as a result of this, but for mails with many different domains I've seen deliveries hit their retry limits and hang around on the mail queue for over an hour. Sub-optimal. A web-search for "Multiple destination domains per transaction is unsupported" will likely locate a few annoyed mail-admins.

You can work around this in Exim. Exim has a transport option "multi_domain" that, when set to false, prevents multiple domains from being delivered per transaction. So you need to configure mail to route all google-handled domains via a transport that has this set.

First, set-up a new transport called "remote_smtp_single_domain" - this should be the same as your existing remote_smtp transport, but with "multi_domain = false" /etc/exim4/conf.d/transport/40_temp_single_domain

  debug_print = "T: remote_smtp_single_domain for $local_part@$domain"
  driver = smtp
  multi_domain = false

Then add a new router just before your dnslookup router /etc/exim4/conf.d/router/180_temp_single_domain

  debug_print = "R: dnslookup_single_domain for $local_part@$domain"
  driver = dnslookup
  domains = ! +local_domains : ! +relay_to_domains
  condition = ${if forany{${lookup dnsdb{>: mxh=$domain}}}{match_domain{$item}{+single_domain_mx}}}
  transport = remote_smtp_single_domain
  same_domain_copy_routing = yes

This will cause any domain with an MX record in the domain list "single_domain_mx" to use the new transport.

The easiest way to add the domain list is to add it to your main config, as below. /etc/exim4/conf.d/main/10_temp_single_domain

domainlist single_domain_mx = :


Posted by lee on Mon 5 Sep 2011 at 00:21
Tags: , ,

RFC 5451 describes the email header "Authentication-Results:" which contains the results of online email authentication tests that can be used by the mail receiver client and filtering software.

Exim doesn't, at this time, include native support for adding the Authentication-Results headers, but it's possible to add it using standard ACLs. (But there's a big caveat in doing this, see below.)

In /etc/exim4/conf.d/main/99_local_config :

AUTHSERV_ID = primary_hostname

In where ever your DATA ACL lives add the following:

warn  !condition = ${if def:acl_m_authresults {true}}
         set acl_m_authresults = ; none

warn add_header = :at_start:Authentication-Results: ${AUTHSERV_ID}${acl_m_authresults} 

Exim should now be adding a header like the following to incoming mails:

Authentication-Results:; none

Now to add some authentication checks. The easiest is the "iprev" policy which merely checks if the reverse DNS of the sending server has been properly configured. (The actual suitability and usefulness of rDNS checks is not covered here.)

Add the following to the RCPT ACL (or to the DATA ACL, above the entry listed above):

warn !condition = ${lookup dnsdb{ptr=$sender_host_address}{true}{}}
        set acl_c_iprev = permerror
        set acl_m_authresults = $acl_m_authresults; iprev=permerror (no ptr) \

warn verify = reverse_host_lookup
        set acl_m_authresults = $acl_m_authresults; iprev=pass \
           policy.iprev=$sender_host_address ($sender_host_name)

warn  !condition = ${if eq{$acl_c_iprev}{permerror}{true}}
       condition = ${if and{{def:sender_host_address}{!def:sender_host_name}} {yes}{no}}
       set acl_m_authresults = $acl_m_authresults; iprev=${if \
         eq{$host_lookup_failed}{1}{fail}{temperror}} \

The header on the incoming mail should now have rDNS details included:

Authentication-Results:; iprev=pass policy.iprev=

Adding another check is fairly straight forward. To add a DKIM check add something like the following to your DKIM ACL. (It might need to be slightly more comprehensive, but this example shows the basics)

warn dkim_status = invalid:fail
        set acl_m_authresults = $acl_m_authresults; dkim=neutral ($dkim_verify_reason) \
             header.${if eq{$dkim_identity}{}{d}{i}}=$dkim_cur_signer

warn dkim_status = pass
        set acl_m_authresults = $acl_m_authresults; dkim=pass \
            header.${if eq{$dkim_identity}{}{d}{i}}=$dkim_cur_signer

A DKIM signed message might now have the following in the header:

Authentication-Results:; iprev=pass policy.iprev=
    (; dkim=pass

Different authentication schemes are listed at IANA, and most of them can be incorporated using Exim ACLs.

Which is great, but unfortunately, while the header line seems correct, I can't configure Exim to be fully compliant with RFC5451. The issue is with Authentication-Results headers that already exist in the mail as it is delivered. For obvious reasons it is necessary to remove any Authentication-Results headers (where the remote server is not trusted) that contain the locally used auto-server ID. (Note this is not necessarily a forgery, as mails may legitimately have passed through a system using that ID, but can still not be trusted.)

Unfortunately there's no current way to remove any headers using Exim's ACLs, let alone selectively remove headers that match a specific criteria.

The only way I can think to do this, and it's not very elegant, is to change the generated header so that it consists of a secret string:

warn !condition = ${if def:acl_m_authresults {true}}
     set acl_m_authresults = ; none

warn add_header = :at_start:X-Authentication-Results: ${AUTHSERV_ID}${acl_m_authresults} 
warn add_header = :at_start:X-4354-2827: ${AUTHSERV_ID}${acl_m_authresults} 

And then ensure that all mail deliveries pass through a system filter that contains the following:

if $h_Authentication-Results is not "" then
  headers add "X-Orig-Authentication-Results: $h_Authentication-Results"
  headers remove Authentication-Results
headers add "Authentication-Results: $h_X-4354-2827"
headers remove X-4354-2827

The downside of this approach is that there's no way (I know) of specifying in system filters that headers should be added at the top (as a trace field). Which means it's still fails to meet the requirement of RFC 5451 which considers the header position significant.

Oh well.


Posted by lee on Fri 29 Jul 2011 at 12:52
Tags: , ,

There's a prolific spammer that registers a fresh new domain every day and sends out DKIM signed mail via changing IP addresses. Keeping a blacklist of sending domains and IP addresses is fairly useless after the fact.

However, the one constant is that the nameservers they use for the domains always have the same domain names, and since that domain is registered to the spammer it's unlikely to be used for anything legitimate.

Therefore it's trivial to block based on a lookup of the nameserver in Exim's acl_check_rcpt

deny    message = Domain is blacklisted here
        condition = ${if match{ \
           ${lookup dnsdb{>: ns=$sender_address_domain}}}{} {yes}} 
        set acl_m_sender_nameservers = ${lookup dnsdb{>: ns=$sender_address_domain}}
        log_message = nameservers for $sender_address_domain: $acl_m_sender_nameservers


Posted by lee on Thu 12 May 2011 at 21:04
Tags: , ,

Everytime I set up a new sever, I always seem to have forgotten how to generate the fingerprint data to store in DNS. So, for the benefit of my future self:

Install sshfp (a python script packaged for debian/ubuntu)

Then to get the output in a format usable by TinyDNS, run it through another script.

sshfp -s | sshfp2tdns
## sshfp2tdns - convert sshfp output for use in TinyDNS
## adaped from code on

use strict;

while (<>) {
  my ($host, $in, $sshfp, $alg, $fptype, $fp) = split " ", $_;
  my $out = sprintf("\\%03o\\%03o", $alg, $fptype);
  for (my $i = 0; $i < length($fp); $i += 2) {
        $out .= sprintf("\\%03o", hex substr($fp, $i, 2));
  printf(":%s:44:%s:\n", $host, $out);


Posted by lee on Fri 29 Apr 2011 at 19:03
Tags: , ,

Client-side email filtering seems useless to anyone using multiple clients for reading email - especially if they're using iPhone, iPad, and the like.

For sites deploying "virtual email" IMAP accounts, the standard solution would be to support the upload of Sieve filter files using the managesieve protocol.

If you then want to use these filters at delivery time using Exim4, you have two choices:

  • Pass off the mail delivery to a local delivery agent (that supports Sieve) using a pipe transport (e.g. Dovecot LDA)
  • Write a router/transport that uses Exim's built-in filter support

Should you want to use Exim's Sieve support there are three main caveats:

  • Sieve files accessed from redirect routers need to be readable by the uid of the process that handles the SMTP connection, (e.g. Debian-exim)
  • While the Sieve RFC specifies that files use CRLF as linebreaks, Exim filters usually require the use of LF only.
  • Exim requires that sieve filter files identify themselves with "# Sieve filter" which is not part of the Sieve spec.

The first issue can easily be solved by adding the Debian-exim user to a group that can read the Sieve files. To work around the other issues you can use "data" (rather than "file") to munge the files to be usable.

The following configuration assumes a split config, maildir directories, and an active Sieve file (or likely symlink) is available from the sieve manager (if not, it should carry on to the next router for delivery e.g. standard maildir deivery)

ACTIVE_SIEVE = /var/lib/sieve/${domain}/${local_part}/active
VACATION_DIR = /var/lib/sieve/${domain}/${local_part}/vacation
VDOM_MAILDIR = /var/vmail/${domain}/${local_part}

The following router is installed to /etc/exim4/conf.d/router/350_local_sieve

   debug_print = "R: vdom_sieve for $local_part@$domain"
   driver = redirect
   domains = +local_domains
   require_files = ACTIVE_SIEVE
   allow_filter = true
   local_part_suffix = +* : -*
   data = "#Sieve filter\n${sg{${readfile{ACTIVE_SIEVE}}}{\r}{}}"
   sieve_useraddress = "$local_part"
   sieve_subaddress = "${sg{$local_part_suffix}{^.}{}}"
   sieve_vacation_directory = VACATION_DIR
   pipe_transport = address_pipe
   reply_transport = address_reply
   file_transport = vdom_sieve_file 

Note, the redirect router allows either "+" or "-" as a suffix, which may need to be tweaked depending on site requirements.


    debug_print = "T: vdom_sieve_file for $local_part@$domain ($address_file)"
    driver = appendfile
    directory = VDOM_MAILDIR/${sg {.${sg {$address_file}{/}{.}}/} \
      {^.(INBOX|inbox)/} {}}
    maildir_format = true
    user = vmail
    group = vmail

Note: The directory line might need a little fixing to fully support Maildir, but currently it replaces "/" characters with dots and assumes "inbox" as the user maildir root.


Posted by lee on Sun 13 Mar 2011 at 11:38
Tags: , , , ,

Update: You can now use Submission/ESMTPSA with SES directly

Update: Note that since this entry was originally written, SES introduced an SMTP interface making this redundant.

I was recently required to look into routing messages via Amazon Simple Email Service (Amazon SES), however the documentation provided by Amazon doesn't include details for integrating it with Exim.

(Note: this was done for testing purposes and has not been used in a live configuration.)

Firstly, download and install the perl scripts as per Amazon's Getting Started guide.

On a Debian/Ubuntu box you'll probably need to fulfil the dependencies with

apt-get -y install libcrypt-ssleay-perl 

I've placed the .pl files in /usr/local/bin and made them executable. is placed in /usr/local/lib/site_perl/ .

Set up the access key, for example in /etc/aws_credentials, and make sure the file is readable by a subprocess spawned by Exim. e.g. :

chgrp Debian-exim /etc/aws_credentials 
chmod 640 /etc/aws_credentials

Then use the to set up your test addresses.

The Exim configuration I'm using is done so that delivery via AWS SES is only attempted if the sender has been specified in the configuration (otherwise it attempts to treat it as normal and send via smtp). So in the config example I set-up a file /etc/exim4/ses_senders that contains a list of sender email addresses (one per line) that are routed to SES.

(The following assumes a split config)


## is available from
## ensure is in the PERL5 library path
AWS_SES_SEND_EMAIL = /usr/local/bin/

## File must be readable by the running exim group (e.g. Debian-exim)
AWS_CREDENTIALS_FILE = /etc/aws-credentials

## the SES verified sender
AWS_SES_SENDER = lsearch*@;/etc/exim4/ses_senders

## currently useless as there's only one endpoint offered


## to send all mail via SES, remove the "senders" line

  debug_print = "R: aws_ses for $local_part@$domain"
  driver = accept
  senders = AWS_SES_SENDER
  transport = aws_ses_pipe


  debug_print = "T: aws_ses_pipe for $local_part@$domain" 
  driver = pipe
         "${if !eq{AWS_SES_ENDPOINT}{} {-e}}"\
         "${if !eq{AWS_SES_ENDPOINT}{} {AWS_SES_ENDPOINT}}" \
         -f $sender_address $local_part@$domain
  freeze_exec_fail = true
  message_prefix =
  return_fail_output = true

A few things to keep in mind:

The process does not produce a Received: header for the hand-off from your server to SES.
Some headers will be rewritten by SES, including Date: Message-Id:, as well as the envelope sender.
Mail with unrecognised headers will be rejected (see the Developer docs)
This won't use Exim's DKIM implementation as it's tied to the smtp transport. There might be workarounds, but they're not covered here.