Computing Service Mail Support |
||||
Tony Finch <dot@dotat.at>
20 Jul 2006: Good configuration style for Exim |
My CV expressed as a list of email addresses.
I went to University at Cambridge, where I started using Exim in 1997. After University I worked at Demon Internet, who use Exim - although I wasn't a postmaster there.
I didn't escape Cambridge for long: I soon returned, and I've now been working for the University of Cambridge Computing Service for four years. I help to run the central email systems and provide technical support to others running email servers in the University.
I have in the past contributed to the Apache HTTP server and FreeBSD projects.
This talk is about some of what I've learnt about working with Exim.
Exim is a big program with several significant components. It's important to understand how they fit together, and how to configure them so that they work with each other cleanly.
So I'm going to start off by talking about style in the large - the high-level structure of your configuration. Later on I will talk a little about small-scale style, including some layout suggestions.
One way of making it easier to learn a program is to find out which parts you can safely ignore. If you are a long-time Exim user, or if you are coming to Exim from other MTAs, you might be led astray by differences in terminology etc.
Some MTAs do everything via rewriting. In general you should look at Exim's routing functionality (especially the redirect router) when you think of rewriting.
The system filter is a legacy from the early days of Exim. Nowadays ACLs are generally more powerful for anti-spam checks, and they run at SMTP time which is better. Routers are much more flexible now, so most routing hacks no longer need the system filter.
The interestnig parts of your configuration are implemented in the ACLs and routers. Both are very flexible, so you want a rule of thumb about what parts of your configuration should be implemented using ACLs and which using routers.
The rule of thumb for ACLs is that they make decisions entirely based on who the client is, identified by hostname or IP address or TLS client certificate or SASL authentication.
accept hosts = +relay_from_hosts accept authenticated = * |
A couple of simple examples where we decide to accept a message based on who the client is.
deny message = You are blacklisted:\ see ${dnslist_text} dnslists = sbl-xbl.spamhaus.org |
Another simple example where we decide to reject a message based on who the client is.
VDOMS = /etc/mail/domains require domains = dsearch;VDOMS local_parts = lsearch;VDOMS/$domain |
|
This is an example of what not to do. We'll see the right way to do it in a moment.
The rule of thumb for routers is that they deal only with email addresses. They are blind to how Exim accepted the message.
domainlist virtual_domains = \ dsearch;VDOMS virtual_domains: driver = redirect domains = +virtual_domains local_parts = lsearch;VDOMS/$domain data = $local_part_data |
This is the router for the previous wrong example. The bad example ACL duplicates the logic and must do so accurately - an opportunity for making mistakes when you change something.
local_part_list internal_users = \ lsearch;/etc/mail/internal_users internal_server: driver = manualroute domains = +internal_domain local_parts = +internal_users route_data = internal.mail.example transport = smtp |
A common reason for people to put address logic into ACLs is when the addresses are not local to the Exim server - for example when Exim is being a gateway to an internal mail server. The wrong train of thought is "I want to reject email to invalid addresses, and I reject things using ACLs"; the right train of thought is "address verification is done with the routers, so the routers need to know which addresses are valid". We'll come back to this again in a moment.
In many cases the local part list will be (for example) an LDAP lookup rather than a file on disk.
require verify = recipient |
With this verification check, and the correct address checking logic in the routers, there's no need for any address logic in the ACLs.
require verify = recipient/callout=\ use_sender,defer_ok |
This is useful in the absence of something like the LDAP lookup I mentioned earlier, for example when you don't have co-administration of the internal mail server.
SENDER = sender_address_data require authenticated = * require verify = sender deny message = Attempted forgery condition = ${extract {user}{$SENDER} \ {${if !eq{$value}{$authenticated_id} }} } |
This is probably the most complicated example I'll show. It comes from the ACLs on our message submission server. The routers chase through all the various aliases and redirections, probably ending up with a user's real account. (For example, this maps friendly-name addresses to usernames.) If the account name does not match the authenticated username, then someone is messing around. (If no username is found by the routers - e.g. sending with a shared address or an external vanity address - the extract will yield nothing so the message will be accepted.)
Note the ACL has no knowledge about addresses here, such as how they map on to user names: it's just using the result of the router's logic.
You can do similar things with recipient verification and per-user anti-spam options.
require domains = +local_domains |
The anti-relay check should be a simple check against a domain list. We'll have a closer look at lists in a moment.
SPAMMERS = /etc/mail/spammers deny senders = lsearch;SPAMMERS |
Although you can implement a sender address blacklist using the routers, it's more conventional to do so using an ACL clause like this. (The router version has the side-effect of preventing you from sending email to the blacklisted address as well as receiving email from it.)
So far in the examples I have used a lot of named lists, and most of them have been fairly trivial. You might think this is pointless, but it isn't.
domainlist virtual_domains = \ dsearch;VDOMS domainlist local_domains = \ +virtual_domains : ••• # ••• require domains = +local_domains # ••• virtual_domains: driver = redirect domains = +virtual_domains # ••• |
The more complicated your configuration the more likely you are to have repeats, and the more complicated your configuration the more you need to use techniques to keep it simple. The local_domains list can get complicated if you have several different kinds of domains. You might have different variants of a router for verification and delivery, or both aliases and local user routers, which all increase the number of times a list is checked.
domainlist domain_routes = \ lsearch;/etc/mail/domain_routes domain_routes: driver = manualroute domains = +domain_routes route_data = $domain_data transport = smtp |
The lookup syntax in string expansions is cluttered, so it's nice to be able to avoid it. This example shows that the result of the lookup in a domain list is available in $domain_data, which allows you to avoid both repeating the lookup and an ugly string expansion.
This also shows how you can re-use names to tie parts of the configuration together. Give the list the same name as the table it is based on, and give the router the same name as the list it matches.
I'm now going to talk a bit about router configuration.
|
In my configuration I have lots of types of domains: virtual domains, a mapping from long-form to short-form domains (like quns.cam.ac.uk -> queens.cam.ac.uk), obsolete domains which get a special error message (for when a department changes name), manually routed domains, and special domains like Hermes (with the LMTP router above). These mostly do not overlap, so the routers could appear in any order. The order only really matters when a domain changes from one setup to the other, and we need to manage the transition cleanly.
domain_routes: transport = smtp driver = manualroute route_data = $domain_data domains = +domain_routes |
|
This version of the previous example has the same meaning, but it is written in a misleading order which obscures the way it works.
|
The order is not very obvious, so you'll have to refer to section 3.12 and chapter 15 of the manual. It matters partly because some options have side-effects like setting expansion variables, e.g. $domain_data.
|
These are most of the other options that can make a router decline.
The miscellaneous options are generally un-ordered.
|
These last few options are also ordered.
hermes_lmtp: driver = manualroute local_part_suffix = +* local_part_suffix_optional no_verify domains = hermes.cam.ac.uk local_parts = +hermes_active route_data = $local_part_data byname host_find_failed = defer retry_use_local_part transport = ${if ={0}{$body_zerocount} \ {hermes_lmtp} {hermes_lmtp_filter} }
These last few options are also ordered.
String expansions can easily become completely illegible. Many people get into a muddle as a result, judging by questions on the exim-users list. So here are a couple of tips.
${extract {user}{$SENDER} \ {${if !eq{$value} \ {$authenticated_id} }} } |
The extra white space in this example is not included in the result of the expansion, so you can use it to make the expression more readable. See how the pair of closing braces match the pair of opening braces before if.
You can also omit the {yes} {no} in many cases.
Again see how the paired braces match. Also see how the spaces around the inner conditions keep them from being mixed up with the or's braces.
The condition option is a general-purpose escape route for use when the other conditions can't do what you want. However it relies on string expansions which are ugly, so it should be avoided where possible.
DB = /opt/exim/etc/db SERVER = ${lookup {$interface_address} \ cdb {DB/server_params.cdb} } domainlist special_routes = \ cdb;DB/special_routes.cdb |
SENDER = acl_m0 require verify = sender set SENDER = $sender_address_data |
Exim does macro expansion before doing anything else with the configuration file, so you can use them to make ACL variable names more meaningful. In this example we're saving the sender address data for later use. (ACL variables are preserved longer than the $sender_address_data variable.
There are a lot of configuration options that go in the first part of the configuration file. Chapter 14 of the specification has a series of summary sections which divide the options into groups, which you can use to structure your configuration file.
Different from the default configuration, but it has the advantage that it is the order of evaluation. Note that this is for server authentication: if Exim is authenticating as a client it's probably better to put the authenticators after the transports.