Signing Return Path Addresses with Exim

I have been receiving a fair amount of Spam from an e-mail forwarder.  They are unwilling to correct their problems.  Much of the Spam they forward is the form of bounce notifications.  Attempting to reject other Spam resulted in more notifications.  To control this Spam I implemented signed return path addresses.  As a side benefit, I am also rejecting bogus notifications sent directly to me.

Signing my return path allows me to reject faked notification e-mail.  The SMTP standard requires that no email sent with a null return path “<>” (aka Envelope Sender) be returned.  Its purpose is for allow for notifications about existing messages.  These includes notifications such as address unknown, message delivered, and message read.  E-mail notification which are not about a previously sent message can be refused . Signing the return path allowed me to reject such invalid notifications.

There are various methods of signing the return path (envelope sender address). The method used heres is not appropriate if you send mail with many recipients in the same domain. The prvcheck functionality built into Exim handles this case well, and timestamps the signature. The changes required to replace the mechanism are minimal.

This post outlines the changes required.  The changes required modify the envelope surrounding the message.  No modification is made to the message itself.  The return path and received headers added in transit will contain the signed return path.

Rejecting unsigned addresses on incoming notifications requires that all outgoing mail servers for the domain be known.  All these servers (usually 1 or 2) need to updated.  A Sender Policy Framework (SPF) configuration specifying a hard failure for unlisted servers should also be implemented.

Mobile users and other users outside your network can be provided mechanisms to send mail from your domain. These include:

  • Authenticated connections (preferably on the Submission port), that allow email to pass through your server.  This is suitable service for mobile users.
  • Sending from a local (to the senders network) e-mail address with a Sender address from the originating domain.  This is the preferred mechanism for external servers sending email on your behalf (bulk mailers, outsourced services, etc.).
  • E-mail addresses in a sub-domain dedicated to remote users.  This will likely not have return path signing.  This sub-domain will be at higher risk for Spam.

I based my configuration on the  Spam-Filtering-for-MX HOWTO provided by The Linux Documentation Project.  My configuration unconditionally signs the return path all outgoing mail.  You may want to review other documentation to decide if this is what you want to do this.  Servers in networks hosting mailing lists or other autobots may require a different configuration.

To send mail via a smarthost see the Spam-Filtering-for-MX HOWTO and add the adjustments.

I use a split configuration on Ubuntu.  This is the same mechanism as is used for Debian systems. File names are given relative to the /etc/exim4/conf.d directory.  I use the local ACL file mechanism to separate my ACLs from the standard ACLs. To use a merged configuration add the contents at an appropriate location as noted.

Setting up a Secret

Add a SECRET macro for generating your signature.  This goes in  a local configuration file such as (main/00_localmacros). Change yourSecret to your own secret.

SECRET = yourSecret

WARNING: The value of SECRET can be found by inspection of your configuration.  Users of the system may be able to read this configuration.  Unless there is a security problem, the configuration is not visible the Internet.

Signing the Return Path

Add a new transport (transport/40_local-config_remote_smtp_signed)  to sign the return path.  This can be anywhere in your transports configuration.

<pre># This transport is used to sign sender address for remote deliveries
#
remote_smtp_signed:
 debug_print    = "T: remote_smtp_signed for $local_part@$domain"
 driver         = smtp
 max_rcpt       = 1
 return_path    = $sender_address_local_part=$local_part=$domain=\
 ${hash_8:${hmac{md5}{SECRET}{${lc:\
 $sender_address_local_part=$local_part=$domain}}}}\
 @$sender_address_domain<pre>

Add a new router (router/180_local-config_dnslookup_signed) to use the new transport.  It needs to occur before other routers using dnslookup. Place it after other delivery drivers such as ipliteral and manualroute. This router needs additional information if you are using a smarthost.

# This router handles signing sender addresses on messages not delivered locally..
#
# Sign the envelope sender address (return path) for deliveries to
# remote domains. Bounce messages (null sender) are never signed.
# Other envelope sender addresses are always signed.
#
dnslookup_signed:
 debug_print   = "R: dnslookup_signed for $local_part@$domain"
 driver        = dnslookup
 transport     = remote_smtp_signed
 senders       = ! :
 domains       = ! +local_domains : !+relay_to_domains
 no_more

Handling Incoming Notifications

Add a new redirect router (router/350_local-hashed_local) for Local Deliveries.  It should be early in the redirect routers list .

# This router strips sender signatures from local addresses.
#
hashed_local:
 debug_print       = "R: hashed_local for $local_part@$domain"
 driver            = redirect
 domains           = +local_domains
 local_part_suffix = =*
 data              = $local_part@$domain

To being accepting signed notification,  add this statement to the recipient ACL after the recipient is verified, and before any other statements which might defer or deny the message.  At the start of the ACL file referenced by the CHECK_RCPT_LOCAL_ACL_FILE macro would be appropriate.

# Accept the recipient address if it contains our own signature.
# This means this is a response (DSN, sender callout verification...)
# to a message that was previously sent from here.
# Only accept if the sender is null or matches the signature.
#
accept
  domains     = +local_domains
  condition   = ${if and {{match{${lc:$local_part}}{^(.*)=(.*)}}\
                          {eq{${hash_8:${hmac{md5}{SECRET}{$1}}}}{$2}}}\
                         {true}{false}}
  senders     = : ${extract{2}{=}{lc:$local_part}}@\
                  ${extract{3}{=}{lc:$local_part}}

Any e-mail with a the null sender sent to an unsigned local address (return path) should now be bogus notifications or a recipient callout.  Flag any invalid notifications in the recipient ACL.  Refusal is deferred to the data ACL to enable sender callouts to us.  The ACL variable acl_m10 is used to carry the flag.

Place this statement in the recipient ACL  before any conditions which would accept a bogus notification.   Immediately after the statement above would be a good location.

# Flag if this is a bogus notification.
# IMPORTANT: Don't reject here so that sender callouts to us work.
warn
  domains     = +local_domains
  senders     = :
  !condition  = ${if and {{match{${lc:$local_part}}{^(.*)=(.*)}}\
                          {eq{${hash_8:${hmac{md5}{SECRET}{$1}}}}{$2}}}\
                         {true}{false}}
  set acl_m11 = bogus notification.

# Bypass additional checks for sender callouts to us
accept
 domains     = +local_domains
 senders     = :

Start out will a warn message in the data ACL for the first week or two. A few notifications from before sender address signing was implemented may be received.   After a week or two this can be changed from warn to deny or defer. If the network has mailing lists or other autobots, wait until their notifications are known to be handled correctly.

Place this statement in the data ACL before any statements which would accept the notification. At the start of the ACL file referenced by the CHECK_DATA_LOCAL_ACL_FILE macro would be appropriate.

# Deny if this is a bogus notification or other deferred rejection.
 deny
   condition   = ${if def:acl_m11}
   message     = This address does not match a valid, signed \
                 return path from here.\n\
                 You are responding to a forged sender address.
   log_message = $acl_m11

These changes need to be done on all boundary e-mail servers that send to or receive from the Internet.  A partial implementation with deny policy, will drop legitimate notifications.

Implement or verify the SPF configuration.  The SPF string should end with “-all” as bounce notifications sent by other servers will be refused.  Newer versions of bind now support the SPF record type.  If supported, ensure there are SPF records in addition to the existing TXT records for SPF.  The contents of both should be the same.  If the MX server  handles all Internet mail, a reasonable set and forget record is "v=spf1 mx -all".  A better choice would be to use ip4 (and ip6) specifications instead of mx.  Be sure you have an SPF record for the outgoing mail server allowing it to send mail.   Receive only MXs should not be on the permitted list.