Exim's ACL Configuration

I have already explained a little about ACL's, the section begins with the heading "begin acl". You can have as many ACL's as you wish and the order that they appear does not matter, as they are called individually and are NOT processed each in turn. Once you are inside an ACL it is processed until it is the address is accepted, denied.

ACL section
######################################################################
#                       ACL CONFIGURATION                            #
#         Specifies access control lists for incoming SMTP mail      #
######################################################################
begin acl


#####################################################
### end acl/00_exim-config_header
#####################################################

#####################################################
### acl/20_exim-config_whitelist_local_deny
#####################################################

### acl/20_exim-config_whitelist_local_deny
#################################

# This is used to determine whitelisted senders and hosts.
# It checks for CONFDIR/local_host_whitelist and
# CONFDIR/local_sender_whitelist.
#
# It is meant to be used from some other acl entry.
#
# For example,
# deny
#   message = local blacklist example
#   !acl = acl_whitelist
#   dnslist = some.dns.list.example
# will allow messages with envelope sender listed in local_sender_whitelist
# or messages coming in from hosts listed in local_host_whitelist to be
# accepted even if the delivering host is listed in the dns list.
#
# Whitelisting can also be configured by including negative items in the
# black list. See /usr/share/doc/exim-config/default_acl for details.
#
# If the files do not exist, the white list never matches, which is
# the desired behaviour.

acl_whitelist_local_deny:
  accept
    hosts = ${if exists{CONFDIR/local_host_whitelist}\
                 {CONFDIR/local_host_whitelist}\
                 {}}
  accept
    senders = ${if exists{CONFDIR/local_sender_whitelist}\
                   {CONFDIR/local_sender_whitelist}\
                   {}}

  # This hook allows you to hook in your own ACLs without having to
  # modify this file. If you do it like we suggest, you'll end up with
  # a small performance penalty since there is an additional file being
  # accessed. This doesn't happen if you leave the macro unset.
  .ifdef WHITELIST_LOCAL_DENY_LOCAL_ACL_FILE
  .include WHITELIST_LOCAL_DENY_LOCAL_ACL_FILE
  .endif
#####################################################
### end acl/20_exim-config_whitelist_local_deny
#####################################################
#####################################################
### acl/30_exim-config_check_rcpt
#####################################################

### acl/30_exim-config_check_rcpt
#################################

# This access control list is used for every RCPT command in an incoming
# SMTP message. The tests are run in order until the address is either
# accepted or denied.
#
acl_check_rcpt:
  # Accept if the source is local SMTP (i.e. not over TCP/IP). We do this by
  # testing for an empty sending host field.
  accept
    hosts = :


  # Add missing Date and Message-ID header for relayed messages
  warn
    hosts = +relay_from_hosts
    control = submission/sender_retain


  # The following section of the ACL is concerned with local parts that contain
  # certain non-alphanumeric characters. Dots in unusual places are
  # handled by this ACL as well.
  #
  # Non-alphanumeric characters other than dots are rarely found in genuine
  # local parts, but are often tried by people looking to circumvent
  # relaying restrictions. Therefore, although they are valid in local
  # parts, these rules disallow certain non-alphanumeric characters, as
  # a precaution.
  #
  # Empty components (two dots in a row) are not valid in RFC 2822, but Exim
  # allows them because they have been encountered. (Consider local parts
  # constructed as "firstinitial.secondinitial.familyname" when applied to
  # a name without a second initial.) However, a local part starting
  # with a dot or containing /../ can cause trouble if it is used as part of a
  # file name (e.g. for a mailing list). This is also true for local parts that
  # contain slashes. A pipe symbol can also be troublesome if the local part is
  # incorporated unthinkingly into a shell command line.
  #
  # Two different rules are used. The first one has a quite strict
  # default, and is applied to messages that are addressed to one of the
  # local domains handled by this host.
  # If you have local accounts that include strange characters, you can
  # use the macro provided to change the ACL range or to disable the
  # check completely.
  .ifdef CHECK_RCPT_LOCAL_LOCALPARTS
  deny
    domains = +local_domains
    local_parts = CHECK_RCPT_LOCAL_LOCALPARTS
    message = restricted characters in address
  .endif


  # The second rule applies to all other domains, and its default is
  # considerably less strict.
  .ifdef CHECK_RCPT_REMOTE_LOCALPARTS
  deny
    domains = !+local_domains
    local_parts = CHECK_RCPT_REMOTE_LOCALPARTS
    message = restricted characters in address
  .endif


  # Accept mail to postmaster in any local domain, regardless of the source,
  # and without verifying the sender.
  #
  accept
    .ifndef CHECK_RCPT_POSTMASTER
    local_parts = postmaster
    .else
    local_parts = CHECK_RCPT_POSTMASTER
    .endif
    domains = +local_domains : +relay_to_domains


  # deny bad senders (envelope sender)
  # CONFDIR/local_sender_blacklist holds a list of envelope senders that
  # should have their access denied to the local host. Incoming messages
  # with one of these senders are rejected at RCPT time.
  #
  # The explicit white lists are honored as well as negative items in
  # the black list. See /usr/share/doc/exim-config/default_acl for details.
  deny
    message = sender envelope address $sender_address is locally blacklisted here.\ 
              If you think this is wrong, get in touch with postmaster !acl = acl_whitelist_local_deny senders = ${if exists{CONFDIR/local_sender_blacklist}\ {CONFDIR/local_sender_blacklist}\ {}} accept recipients = ${if exists{CONFDIR/local_rcpt_whitelist}\ {CONFDIR/local_rcpt_whitelist}\ {}} # deny bad sites (IP address) # CONFDIR/local_host_blacklist holds a list of host names, IP addresses # and networks (CIDR notation) that should have their access denied to # The local host. Messages coming in from a listed host will have all # RCPT statements rejected. # # The explicit white lists are honored as well as negative items in # the black list. See /usr/share/doc/exim-config/default_acl for details. deny message = sender IP address $sender_host_address is locally blacklisted here.\ If you think this is wrong, get in touch with postmaster !acl = acl_whitelist_local_deny hosts = ${if exists{CONFDIR/local_host_blacklist}\ {CONFDIR/local_host_blacklist}\ {}} # Deny unless the sender address can be verified. # # This is disabled by default so that DNSless systems don't break. If # your system can do DNS lookups without delay or cost, you might want # to enable this feature. .ifdef CHECK_RCPT_VERIFY_SENDER deny message = Sender verification failed !acl = acl_whitelist_local_deny !verify = sender .endif # For some sender domains, we do callout to verify if a sender # exists. deny !acl = acl_whitelist_local_deny senders = ${if exists{CONFDIR/local_sender_callout}\ {CONFDIR/local_sender_callout}\ {}} !verify = sender/callout # For some recipient domains, we do callout to verify if a recipient # exists. This is especially handy for customers that receive a lot of # spam to non-existent addresses. deny !acl = acl_whitelist_local_deny recipients = ${if exists{CONFDIR/local_rcpt_callout}\ {CONFDIR/local_rcpt_callout}\ {}} !verify = recipient/callout # Warn if the sender host does not have valid reverse DNS. # # If your system can do DNS lookups without delay or cost, you might want # to enable this. # If sender_host_address is defined, it's a remote call. If # sender_host_name is not defined, then reverse lookup failed. Use # this instead of !verify = reverse_host_lookup to catch deferrals # as well as outright failures. .ifdef CHECK_RCPT_REVERSE_DNS warn message = X-Host-Lookup-Failed: Reverse DNS lookup failed for $sender_host_address\              (${if eq{$host_lookup_failed}{1}{failed}{deferred}})
    condition = ${if and{{def:sender_host_address}{!def:sender_host_name}}\ {yes}{no}} .endif # Check against classic DNS "black" lists (DNSBLs) which list # sender IP addresses .ifdef CHECK_RCPT_IP_DNSBLS deny message = X-Warning: IP $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text) log_message = IP $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text) !senders = ${if exists{CONFDIR/local_domain_dnsbl_whitelist}\ {CONFDIR/local_domain_dnsbl_whitelist}\ {}} dnslists = CHECK_RCPT_IP_DNSBLS delay = 4m .endif # Check against DNSBLs which list sender domains, with an option to locally # whitelist certain domains that might be blacklisted. If you want one # blacklist per domain, you need to replicate the stanza for each DNSBL. .ifdef CHECK_RCPT_DOMAIN_DNSBLS deny message = X-Warning: Domain $sender_address_domain is listed\
              at $dnslist_domain ($dnslist_value: $dnslist_text) log_message = Domain $sender_address_domain is listed at $dnslist_domain ($dnslist_value: $dnslist_text) !senders = ${if exists{CONFDIR/local_domain_dnsbl_whitelist}\ {CONFDIR/local_domain_dnsbl_whitelist}\ {}} dnslists = CHECK_RCPT_DOMAIN_DNSBLS/$sender_address_domain delay = 4m .endif # This hook allows you to hook in your own ACLs without having to # modify this file. If you do it like we suggest, you'll end up with # a small performance penalty since there is an additional file being # accessed. This doesn't happen if you leave the macro unset. .ifdef CHECK_RCPT_LOCAL_ACL_FILE .include CHECK_RCPT_LOCAL_ACL_FILE .endif # Accept if the address is in a local domain, but only if the recipient can # be verified. Otherwise deny. The "endpass" line is the border between # passing on to the next ACL statement (if tests above it fail) or denying # access (if tests below it fail). # accept domains = +local_domains endpass message = unknown user verify = recipient # Accept if the address is in a domain for which we are relaying, but again, # only if the recipient can be verified. # # If you want to use the more conservative "unknown user" error # message in case of a non-existing local part, you might want to # set CHECK_RCPT_GIVE_UNKNOWN_USER. However, this might reveal # local information, which is the cause for it not being enabled by # default. accept domains = +relay_to_domains endpass .ifdef CHECK_RCPT_GIVE_UNKNOWN_USER message = ${if eq{$acl_verify_message}{Unrouteable address}{unknown user}{$acl_verify_message}} .else message = unrouteable address .endif verify = recipient ############ # If control reaches this point, the domain is neither in +local_domains # nor in +relay_to_domains. ############ # Accept if the message comes from one of the hosts for which we are an # outgoing relay. Recipient verification is omitted here, because in many # cases the clients are dumb MUAs that don't cope well with SMTP error # responses. If you are actually relaying out from MTAs, you should probably # add recipient verification here. # accept hosts = +relay_from_hosts # Accept if the message arrived over an authenticated connection, from # any host. Again, these messages are usually from MUAs, so recipient # verification is omitted. # accept authenticated = * # Reaching the end of the ACL causes a "deny", but we might as well give # an explicit message. # deny message = relay not permitted ##################################################### ### end acl/30_exim-config_check_rcpt ##################################################### ##################################################### ### acl/40_exim-config_check_data ##################################################### ### acl/40_exim-config_check_data ################################# acl_check_data: # Deny unless the address list headers are syntactically correct. # # If you enable this, you might reject legitimate mail. .ifdef CHECK_DATA_VERIFY_HEADER_SYNTAX deny message = Message headers fail syntax check !acl = acl_whitelist_local_deny !verify = header_syntax .endif # require that there is a verifiable sender address in at least # one of the "Sender:", "Reply-To:", or "From:" header lines. .ifdef CHECK_DATA_VERIFY_HEADER_SENDER deny message = No verifiable sender address in message headers !acl = acl_whitelist_local_deny !verify = header_sender .endif # This hook allows you to hook in your own ACLs without having to # modify this file. If you do it like we suggest, you'll end up with # a small performance penalty since there is an additional file being # accessed. This doesn't happen if you leave the macro unset. .ifdef CHECK_DATA_LOCAL_ACL_FILE .include CHECK_DATA_LOCAL_ACL_FILE .endif # accept otherwise accept ##################################################### ### end acl/40_exim-config_check_data #####################################################

ACL Definition

The ACL definition comes first

  begin acl

acl_whitelist_local_deny ACL

The ACL acl_whitelist_local_deny points the hosts to the local_host_whitelist file and senders to the local_sender_whitelist files, if they exists, otherwise they are blank

 
acl_whitelist_local_deny:
  accept
    hosts = ${if exists{CONFDIR/local_host_whitelist}\
                 {CONFDIR/local_host_whitelist}\
                 {}}
  accept
    senders = ${if exists{CONFDIR/local_sender_whitelist}\
                   {CONFDIR/local_sender_whitelist}\
                   {}}

  # This hook allows you to hook in your own ACLs without having to
  # modify this file. If you do it like we suggest, you'll end up with
  # a small performance penalty since there is an additional file being
  # accessed. This doesn't happen if you leave the macro unset.
  .ifdef WHITELIST_LOCAL_DENY_LOCAL_ACL_FILE
  .include WHITELIST_LOCAL_DENY_LOCAL_ACL_FILE
  .endif

Note: the if statement is broken down as below (you can brush on you if skill here).

if the file local_host_whitelist exists the set hosts to local_sender_whitelist otherwise leave blank

acl_check_rcpt ACL

Now comes a large ACL called acl_check_rcpt which I will break down

  acl_check_rcpt:

Accept if the source is local SMTP (i.e. not over TCP/IP). We do this by testing for an empty sending host field. I have a discussed this before in ACL.

  accept hosts = :

Add missing Date and Message-ID header for relayed messages, I have already talked about submission and control mode.

  warn
   hosts = +relay_from_hosts
   control = submission/sender_retain

Two different rules are used. The first one has a quite strict default, and is applied to messages that are addressed to one of the local domains handled by this host. If you have local accounts that include strange characters, you can use the macro provided to change the ACL range or to disable the check completely.

 
.ifdef CHECK_RCPT_LOCAL_LOCALPARTS
  deny
    domains = +local_domains
    local_parts = CHECK_RCPT_LOCAL_LOCALPARTS
    message = restricted characters in address
.endif


# The second rule applies to all other domains, and its default is considerably less strict.
.ifdef CHECK_RCPT_REMOTE_LOCALPARTS
  deny
    domains = !+local_domains
    local_parts = CHECK_RCPT_REMOTE_LOCALPARTS
    message = restricted characters in address
.endif

Note: the marcos CHECK_RCPT_LOCAL_LOCALPARTS and CHECK_RCPT_REMOTE_LOCALPARTS were discussed here but 
for a recap they were set to the following

CHECK_RCPT_LOCAL_LOCALPARTS = ^[.] : ^.*[@%!/|\#&?]
CHECK_RCPT_REMOTE_LOCALPARTS = ^[./|] : ^.*[@%!\#&?] : ^.*/\\.\\./

Accept mail to postmaster in any local domain, regardless of the source, and without verifying the sender. We had nothing about postmaster in the main section but you can have.

  accept
   .ifndef CHECK_RCPT_POSTMASTER
      local_parts = postmaster
   .else
      local_parts = CHECK_RCPT_POSTMASTER
   .endif
   
   domains = +local_domains : +relay_to_domains

Deny bad senders (envelope sender) local_sender_blacklist holds a list of envelope senders that should have their access denied to the local host. Incoming messages with one of these senders are rejected at RCPT time. The explicit white lists are honored as well as negative items in the black list.

 
deny
   message = sender envelope address $sender_address is locally blacklisted here.\ 
              If you think this is wrong, get in touch with postmaster !acl = acl_whitelist_local_deny senders = ${if exists{CONFDIR/local_sender_blacklist} {CONFDIR/local_sender_blacklist} {}} accept recipients = ${if exists{CONFDIR/local_rcpt_whitelist} {CONFDIR/local_rcpt_whitelist} {}}
Note: notice the negate (!) on the acl part

Deny unless the sender address can be verified. This is disabled by default so that DNSless systems don't break. If your system can do DNS lookups without delay or cost, you might want to enable this feature.

 
.ifdef CHECK_RCPT_VERIFY_SENDER
  deny
    message = Sender verification failed
    !acl = acl_whitelist_local_deny
    !verify = sender
.endif Note: this will only run if CHECK_RCPT_VERIFY_SENDER has been defined

For some sender domains, we do callout to verify if a sender exists. I discussed the sender/callout option in address verification.

 
deny
   !acl = acl_whitelist_local_deny
   senders = ${if exists{CONFDIR/local_sender_callout} {CONFDIR/local_sender_callout} {}}
   !verify = sender/callout



Note: notice we are calling another ACL acl_whitelist_local_deny

For some recipient domains, we do callout to verify if a recipient exists. This is especially handy for customers that receive a lot of spam to non-existent addresses.

 
deny
   !acl = acl_whitelist_local_deny
   recipients = ${if exists{CONFDIR/local_rcpt_callout} {CONFDIR/local_rcpt_callout} {}}
   !verify = recipient/callout


Note: notice we are calling another ACL acl_whitelist_local_deny

Warn if the sender host does not have valid reverse DNS. If your system can do DNS lookups without delay or cost, you might want to enable this. If sender_host_address is defined, it's a remote call. If sender_host_name is not defined, then reverse lookup failed. Use this instead of !verify = reverse_host_lookup to catch deferrals as well as outright failures.

The condition statement is a custom condition check, you can read more about the condition statement here.

 
.ifdef CHECK_RCPT_REVERSE_DNS
  warn
    message = X-Host-Lookup-Failed: Reverse DNS lookup failed for $sender_host_address\
             (${if eq{$host_lookup_failed}{1}{failed}{deferred}})
    
    condition = ${if and{{def:sender_host_address}{!def:sender_host_name}} {yes}{no}} .endif Note: the if statement reads as the following if sender_host_address is definied and sender_host_name is not define then condition is YES, anything else then condition is NO (note the and in the if statement) You can read up on if statements here.

Check against classic DNS "black" lists (DNSBLs) which list sender IP addresses also check against DNSBLs which list sender domains, with an option to locally whitelist certain domains that might be blacklisted. If you want one blacklist per domain, you need to replicate the stanza for each DNSBL.

 
.ifdef CHECK_RCPT_IP_DNSBLS
  deny
    message = X-Warning: IP $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text)
    log_message = IP $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text)
    !senders = ${if exists{CONFDIR/local_domain_dnsbl_whitelist} {CONFDIR/local_domain_dnsbl_whitelist} {}}
    dnslists = CHECK_RCPT_IP_DNSBLS
    delay = 4m
.endif


.ifdef CHECK_RCPT_DOMAIN_DNSBLS
  deny
    message = X-Warning: Domain $sender_address_domain is listed\
              at $dnslist_domain ($dnslist_value: $dnslist_text) log_message = Domain $sender_address_domain is listed at $dnslist_domain ($dnslist_value: $dnslist_text) !senders = ${if exists{CONFDIR/local_domain_dnsbl_whitelist} {CONFDIR/local_domain_dnsbl_whitelist} {}} dnslists = CHECK_RCPT_DOMAIN_DNSBLS/$sender_address_domain delay = 4m .endif Note: these will only be checked if CHECK_RCPT_IP_DNSBLS
and CHECK_RCPT_DOMAIN_DNSBLS have been defined

This hook allows you to hook in your own ACLs without having to modify this file. If you do it like we suggest, you'll end up with a small performance penalty since there is an additional file being accessed. This doesn't happen if you leave the macro unset which is the case in my configuration

  .ifdef CHECK_RCPT_LOCAL_ACL_FILE
   .include CHECK_RCPT_LOCAL_ACL_FILE
.endif

Accept if the address is in a local domain, but only if the recipient can be verified. Otherwise deny. The "endpass" line is the border between passing on to the next ACL statement (if tests above it fail) or denying access (if tests below it fail).

 

accept
   domains = +local_domains
   endpass
   message = unknown user
   verify = recipient

Note: remember endpass is like a if statement, in this instance if the address is not in the local_domains list then move on

Accept if the address is in a domain for which we are relaying, but again, only if the recipient can be verified. If you want to use the more conservative "unknown user" error message in case of a non-existing local part, you might want to set CHECK_RCPT_GIVE_UNKNOWN_USER. However, this might reveal local information, which is the cause for it not being enabled by default.

  accept
   domains = +relay_to_domains
   endpass

   .ifdef CHECK_RCPT_GIVE_UNKNOWN_USER
      message = ${if eq{$acl_verify_message}{Unrouteable address}{unknown user}{$acl_verify_message}}    .else
      message = unrouteable address
   .endif
   verify = recipient

Accept if the message comes from one of the hosts for which we are an outgoing relay. Recipient verification is omitted here, because in many cases the clients are dumb MUAs that don't cope well with SMTP error responses. If you are actually relaying out from MTAs, you should probably add recipient verification here.

  accept hosts = +relay_from_hosts

Accept if the message arrived over an authenticated connection, from any host. Again, these messages are usually from MUAs, so recipient verification is omitted.

  accept authenticated = *

Reaching the end of the ACL causes a "deny", but we might as well give an explicit message.

  deny message = relay not permitted

acl_check_data ACL

Deny unless the address list headers are syntactically correct. If you enable this, you might reject legitimate mail.

  .ifdef CHECK_DATA_VERIFY_HEADER_SYNTAX
   deny
      message = Message headers fail syntax check
      !acl = acl_whitelist_local_deny
      !verify = header_syntax
.endif

Require that there is a verifiable sender address in at least one of the "Sender:", "Reply-To:", or "From:" header lines.

  .ifdef CHECK_DATA_VERIFY_HEADER_SENDER
   deny
      message = No verifiable sender address in message headers
      !acl = acl_whitelist_local_deny
      !verify = header_sender
.endif

This hook allows you to hook in your own ACLs without having to modify this file. If you do it like we suggest, you'll end up with a small performance penalty since there is an additional file being accessed. This doesn't happen if you leave the macro unset.

  .ifdef CHECK_DATA_LOCAL_ACL_FILE
   .include CHECK_DATA_LOCAL_ACL_FILE
.endif

Accept otherwise accept

  accept