Weblogs for lee

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

remote_smtp_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

dnslookup_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
  no_more

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 = aspmx.l.google.com : gmail-smtp-in.l.google.com

 

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 :

.ifndef AUTHSERV_ID
AUTHSERV_ID = primary_hostname
.endif

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: server.example.com; 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 hosts = !condition = ${lookup dnsdb{ptr=$sender_host_address}{true}{}}
        set acl_c_iprev = permerror
        set acl_m_authresults = $acl_m_authresults; iprev=permerror (no ptr) \
           policy.iprev=$sender_host_address

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}} \
         policy.iprev=$sender_host_address

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

Authentication-Results: server.example.com; iprev=pass policy.iprev=10.11.12.13
  (other.example.net)

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: server.example.com; iprev=pass policy.iprev=10.11.12.13
    (other.example.net); dkim=pass header.d=example.net

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
endif
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}}}{ns1.example.com} {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 s1.example.com s2.example.com | sshfp2tdns
#!/usr/bin/perl
## sshfp2tdns - convert sshfp output for use in TinyDNS
## adaped from code on http://dank.qemfd.net/dankwiki/index.php/SSHFP

use strict;

while (<>) {
  chomp;
  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

vdom_sieve:
   debug_print = "R: vdom_sieve for $local_part@$domain"
   driver = redirect
   domains = +local_domains
   require_files = ACTIVE_SIEVE
   no_verify
   no_expn
   check_ancestor
   allow_filter = true
   local_part_suffix = +* : -*
   local_part_suffix_optional 
   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.

/etc/exim4/conf.d/transport/40_local_sieve

vdom_sieve_file:
    debug_print = "T: vdom_sieve_file for $local_part@$domain ($address_file)"
    driver = appendfile
    delivery_date_add
    envelope_to_add
    return_path_add
    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: 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. SES.pm 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 ses-verify-email-address.pl 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)

/etc/exim4/conf.d/main/00_local_aws-ses

## ses-send-email.pl is available from http://aws.amazon.com/ses/
## ensure SES.pm is in the PERL5 library path
AWS_SES_SEND_EMAIL = /usr/local/bin/ses-send-email.pl

## 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
AWS_SES_ENDPOINT = https://email.us-east-1.amazonaws.com/

/etc/exim4/conf.d/router/180_local_aws-ses

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

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

/etc/exim4/conf.d/transport/40_local_aws-ses

aws_ses_pipe:
  debug_print = "T: aws_ses_pipe for $local_part@$domain" 
  driver = pipe
  command = AWS_SES_SEND_EMAIL -r -k AWS_CREDENTIALS_FILE \
         "${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.

 

Posted by lee on Fri 7 Jan 2011 at 19:48
Tags: , , ,

Support for DKIM signing in Exim is available since version 4.70, and the configuration supplied with Debian makes it fairly straightforward to implement. However it suggests an all or nothing configuration wherein all outgoing mail is signed with the same domain authority.

Where multiple domains are used it may be necessary to selectively switch on DKIM signing, and be able to specify the signing domain. The following details provide a mechanism to do so within the standard Debian Exim configuration.

(This assumes that the keys have been created and the requisite records have been added to DNS for the affected domains. It also assumes a split config.)

Set up a simple look up file such as /etc/exim4/dkim_senders

*@example.com: example.com
test@example.org: example.org

This config should mean that anything sent from any address at example.com is signed as example.com, but only test@example.org will be signed with the example.org key. If default DKIM is not enabled, then no other example.org mail will be signed.

Now create a new router that sits in front of the main router for external main (whatever uses remote_smtp as a transport e.g. dnslookup) such as /etc/exim4/conf.d/router/180_local_primary_dkim (basically a copy of dnslookp with a modified transport)

dnslookup_dkim:
  debug_print = "R: dnslookup_dkim for $local_part@$domain"
  driver = dnslookup
  domains = ! +local_domains
  senders = lsearch*@;/etc/exim4/dkim_senders
  transport = remote_smtp_dkim
  same_domain_copy_routing = yes
  # ignore private rfc1918 and APIPA addresses
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 : 192.168.0.0/16 :\
                        172.16.0.0/12 : 10.0.0.0/8 : 169.254.0.0/16 :\
                        255.255.255.255
  no_more
Then add in a new transport /etc/exim4/conf.d/transport/30_local_remote_smtp_dkim (basically a modified version of remote_smtp)
remote_smtp_dkim:
  debug_print = "T: remote_smtp_dkim for $local_part@$domain"
  driver = smtp
.ifdef REMOTE_SMTP_HOSTS_AVOID_TLS
  hosts_avoid_tls = REMOTE_SMTP_HOSTS_AVOID_TLS
.endif
.ifdef REMOTE_SMTP_HEADERS_REWRITE
  headers_rewrite = REMOTE_SMTP_HEADERS_REWRITE
.endif
.ifdef REMOTE_SMTP_RETURN_PATH
  return_path = REMOTE_SMTP_RETURN_PATH
.endif
.ifdef REMOTE_SMTP_HELO_DATA
  helo_data=REMOTE_SMTP_HELO_DATA
.endif
dkim_domain = ${lookup{$sender_address}lsearch*@{/etc/exim4/dkim_senders}}
dkim_selector = yourhostname
dkim_private_key = /etc/ssl/private/dkim.key
dkim_canon = relaxed
dkim_strict = false
#dkim_sign_headers = DKIM_SIGN_HEADERS
I've left the selector and keys the same since there doesn't appear to be any problem sharing these across domains, but these could also be found via lookups if needed.

 

Posted by lee on Sun 5 Dec 2010 at 03:27
Tags: , ,

Assuming you want to allow uploads to a webhost from a third party that has generated a public key for this purpose.

Set up the account

The following will create a new user and user directory in the standard location
sudo adduser --disabled-password --gecos 'rsync user' rsync01
Alternatively, the home can be set to an existing location as configured in apache. (Note that this shouldn't itself be a directory server by Apache)
sudo adduser --disabled-password --gecos 'rsync user \
  --no-create-home --home /srv/web/example.com rsync01
Then add the id_rsa.pub file into the user's authorized_keys file
sudo su -l rsync01
mkdir -m 700 ~/.ssh
cat /tmp/id_rsa.pub >> .ssh/authorized_keys
chmod 600 .ssh/authorized_keys
mkdir ~/docs

Restricting further access

You'll want to tie the remote user to only using rsync and only in a specific sub-directory, so you probably want to install rrsync.

It's already included in the Debian disribution of rsync.

sudo cp /usr/share/doc/rsync/scripts/rrsync.gz  /usr/local/bin/
sudo gzip -d   /usr/local/bin/rrsync.gz
sudo chmod 755 /usr/local/bin/rrsync
Then modify the new user's authorized_keys
sudo vim ~rsync01/.ssh/authorized_keys
And prefix the key with command specifying the sub-directory to be used, e.g. ~/docs
command="/usr/local/bin/rrsync docs" ssh-rsa AAA...

Note: by locking the command to the specified subdirectory, the "full path" from the point-of-view of the uploader is "/".

 

Posted by lee on Mon 6 Jul 2009 at 16:16
Tags: ,

My mail system has been generating a log of log noise about temporary DNS failures recently. I took a look at the logs and tracked the issue down to a certain (apparently US-based spammer) sending mail out from domains with many MX records associated with it. So many, in fact that the the MX record exceeds the 512 byte limit for UDP, requiring that a TCP query then be made. It's the UDP failure before the TCP retry that's causing the warning in the logs.

While this is technically valid behaviour, it's very unusual and bad practice.

Firstly: TCP-only DNS is unreliable (especially in NAT environs) and considered wasteful network wise if it can be avoided.

Secondly: If you actually need many backup MX records (and you probably don't), it's better to give multiple addresses to a few distinct host names. The algorithm for mail delivery requires going to each host name, not each IP address. In the event of issues on the MX servers, it's an unfair burden for a sender to iterate through each of many hosts before concluding that delivery is not currently possible.

I actually suspect the many-MX design to be some technique for bypassing anti-spam systems, but I don't have any clear example I can point to.

So for now, I'd just like to track them, and later possibly incorporate the information into an anti-spam heuristic.

I'm currently just tagging mails in an ACL, based on the number of MX records associated with the domain of the sender. Oddly, for such a rich set of opperators, Exim doesn't seem to have something counting the number of items in a list. (Note: while this returns the number of MX records, it's not conclusive in recording if TCP was required for a DNS lookup.)

   warn    set acl_m_sender_mx_count = ${reduce {${lookup dnsdb{>: \
            mx=$sender_address_domain}}}{0}{${eval:$value+1}}}
           add_header = X-Sender-MX-Count: ${acl_m_sender_mx_count}

If I actually wanted to act on this information I can apply a test such as:

    condition = ${if >{$acl_m_sender_mx_count}{10}}

 

Posted by lee on Wed 17 Sep 2008 at 11:31
Tags: ,

Mail for a specific domain is passed into a external app via a custom router. When the external app fails the router delays the delivery, but for the case where we need to do a live test on a new installation or configuration we want to freeze any incoming mails and then selectively deliver them from the command line.

A custom router to freeze mail based on the existence of a specific file (in this example "/etc/exim4/eh-freeze") should be placed before the router.

externalhandler_test_freeze:
   debug_print = "R: externalhandler_test_freeze for $local_part_prefix$local_part@$domain"
   condition = "${if exists{CONFDIR/eh-freeze}{true}{false}}"
   driver = redirect
   domains = +eh_domains
   user = www-data
   allow_filter
   allow_freeze 
   data = "#Exim filter \n freeze"

The freezing only works once. A mail manually thawed on the command line will bypass this router regardless of the "eh-freeze" config file existing.