Weblogs for lee
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
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.
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
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); }
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.
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.
- Some headers will be rewritten by SES, including Date: Message-Id:, as well as the envelope sender.
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.
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 locationsudo adduser --disabled-password --gecos 'rsync user' rsync01Alternatively, 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 rsync01Then 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/rrsyncThen modify the new user's authorized_keys
sudo vim ~rsync01/.ssh/authorized_keysAnd 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 "/".
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}}
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.