[exim] Authenticated User Ratelimiting

Top Page
Delete this message
Reply to this message
Author: Grant Peel
Date:  
To: exim-users
Subject: [exim] Authenticated User Ratelimiting
Hi all,

Recent events have left us wanting to add rate limiting to our exim
configuration.

I have seen several examples in various places on the web and everything I
can find in the exim specification, and I must say I am a little more than
confused.

All I really want to do is ratelimit everyone (locally authenticated) to
250/hour – as a start anyways.

A while back, we implemented authentication checking with includes some
ratelimiting, via ‘Lena’s” post:
https://lists.exim.org/lurker/message/20121117.144211.c96f81fc.pl.html

Any primers or suggestion(s) would be appreciated.

-G

Here is our configuration file as it stands now.

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################
primary_hostname = server.ourdomain.ext
domainlist relay_to_domains =
domainlist local_domains = /etc/virtual/domains
domainlist filtered_domains = /etc/virtual/filtered_domains
hostlist filtering_hosts = /etc/virtual/filtering_hosts
hostlist relay_from_hosts = /etc/virtual/domains
hostlist blacklisted_domains = /etc/virtual/blacklist
hostlist spf_bypass = /etc/virtual/spf_bypass
hostlist whitelist = /etc/virtual/whitelist
trusted_users = mailnull:root:webmail:www
exim_user = mailnull
exim_group = mail
never_users =
host_lookup = *
rfc1413_hosts = *
rfc1413_query_timeout = 5s
ignore_bounce_errors_after = 0s
timeout_frozen_after = 0s
auto_thaw = 6h
return_path_remove
untrusted_set_sender = *
helo_allow_chars = _
daemon_smtp_ports = 25 : 109 : 587
bounce_message_file = /usr/local/etc/exim/bounce_message_file
warn_message_file = /usr/local/etc/exim/warn_message_file
return_size_limit = 10000
bounce_return_message = true
bounce_return_size_limit = 1000
delay_warning = 72h
smtp_accept_max = 100
smtp_accept_max_per_host = 10
smtp_return_error_details = yes
log_selector = +incoming_interface +deliver_time +delivery_size
+received_sender \
+received_recipients +sender_on_delivery +subject +address_rewrite
+all_parents
# log_selector = +all
message_logs = true

#######################################################################
#                  Added for Authentication Limiting.                 #
#######################################################################
acl_smtp_rcpt = acl_check_rcpt
acl_smtp_connect = acl_check_connect
acl_smtp_auth = acl_check_auth
acl_smtp_mail = acl_check_mail
acl_smtp_quit = acl_check_quit
acl_smtp_notquit = acl_check_notquit
LIM = 100
PERIOD = 1h
WARNTO = me@???
EXIMBINARY = /usr/local/sbin/exim -f root
SHELL = /bin/sh
#######################################################################
#                  End of additions.                                  #
#######################################################################

#
#  An Attempt at greylisting
#

hide mysql_servers = localhost/exim_db/exim/<passwd>:

GREYLIST_TEST = SELECT IF(NOW() > block_expires, 2, 1) \
                FROM exim_greylist \
                WHERE relay_ip = '${quote_mysql:$sender_host_address}' \
                AND from_domain = '${quote_mysql:$sender_address_domain}' \
                AND record_expires > NOW()

GREYLIST_ADD  = INSERT INTO exim_greylist \
                SET relay_ip = '${quote_mysql:$sender_host_address}', \
                from_domain = '${quote_mysql:$sender_address_domain}', \
                block_expires = DATE_ADD(NOW(), INTERVAL 1 MINUTE), \
                record_expires = DATE_ADD(NOW(), INTERVAL 14 DAY), \
                origin_type = 'AUTO', \
                create_time = NOW()

GREYLIST_UPDATE = UPDATE exim_greylist \
                SET record_expires = DATE_ADD(now(), INTERVAL 14 DAY) \
                WHERE relay_ip = '${quote_mysql:$sender_host_address}' \
                AND from_domain = '${quote_mysql:$sender_address_domain}' \
                AND record_expires > NOW()

######################################################################
#                       ACL CONFIGURATION                            #
#         Specifies access control lists for incoming SMTP mail      #
######################################################################

begin acl

######################################################################
#                 Added for the Authentication Limiting              #
######################################################################

acl_check_auth:
  drop  message = authentication is allowed only once per message in order \
        to slow down bruteforce cracking
        set acl_m_auth = ${eval10:0$acl_m_auth+1}
        condition = ${if >{$acl_m_auth}{2}}
        delay = 22s

  drop  message = blacklisted for bruteforce cracking attempt
        set acl_c_authnomail = ${eval10:0$acl_c_authnomail+1}
        condition = ${if >{$acl_c_authnomail}{4}}
        continue = ${run{SHELL -c "echo $sender_host_address \
           >>$spool_directory/blocked_IPs; \
           \N{\N echo Subject: $sender_host_address blocked; echo; echo \
           for bruteforce auth cracking attempt.; \
           \N}\N | EXIMBINARY WARNTO"}}

accept

acl_check_quit:

  warn  condition = ${if def:authentication_failed}
        condition = $authentication_failed
        logwrite = :reject: quit after authentication failed: \
                            ${sg{$sender_rcvhost}{\N[\n\t]+\N}{\040}}
        ratelimit = 7 / 5m / strict / per_conn
        continue = ${run{SHELL -c "echo $sender_host_address \
           >>$spool_directory/blocked_IPs; \
           \N{\N echo Subject: $sender_host_address blocked; echo; echo \
           for bruteforce auth cracking attempt.; \
           \N}\N | EXIMBINARY WARNTO"}}

acl_check_notquit:

  warn  condition = ${if def:authentication_failed}
        condition = $authentication_failed
        logwrite = :reject: $smtp_notquit_reason after authentication
failed: \
                            ${sg{$sender_rcvhost}{\N[\n\t]+\N}{\040}}
        condition = ${if eq{$smtp_notquit_reason}{connection-lost}}
        ratelimit = 7 / 5m / strict / per_conn
        continue = ${run{SHELL -c "echo $sender_host_address \
           >>$spool_directory/blocked_IPs; \
           \N{\N echo Subject: $sender_host_address blocked; echo; echo \
           for bruteforce auth cracking attempt.; \
           \N}\N | EXIMBINARY WARNTO"}}

acl_check_mail:

         accept set acl_c_authnomail = 0

acl_check_connect:
  drop  message = $sender_host_address locally blacklisted for a bruteforce
\
                  auth (username+password) cracking attempt
        condition = ${if exists{$spool_directory/blocked_IPs}}
        condition = ${lookup{$sender_host_address}lsearch\
                    {$spool_directory/blocked_IPs}{1}{0}}

accept

############################################################################
##
#                 End of the Authentication Limit additions.                
#
############################################################################
##

acl_check_rcpt:

       accept  hosts           = :
                logwrite        = ACL - ACCEPTED (EMPTY LIST)
        deny    local_parts     = ^.*[@%!/|] : ^\\.
                log_message     = ACL - DENIED (LOCAL PART SYNTAX)

############################################################################
#
#       This block replaced with the three paragraphs below                
#
#       for authentication limiting.                                       
#
#        accept  authenticated   = *                        
               #
#                logwrite        = ACL - ACCEPTED                          
#
#                endpass                                                   
#
############################################################################
#

    accept authenticated = *
        set acl_m_user = $authenticated_id
        condition = ${if
exists{$spool_directory/blocked_authenticated_users}}
        condition = ${lookup{$acl_m_user}lsearch\
                    {$spool_directory/blocked_authenticated_users}{1}{0}}
        control = freeze/no_tell
        control = submission/domain=
        add_header = X-Authenticated-As: $acl_m_user

    accept authenticated = *
        !verify = recipient/defer_ok/callout=10s,defer_ok,use_sender
        ratelimit = LIM / PERIOD / per_rcpt / user-$acl_m_user
        continue = ${run{SHELL -c "echo $acl_m_user \
           >>$spool_directory/blocked_authenticated_users; \
           \N{\N echo Subject: user $acl_m_user blocked; echo; echo because
\
           has sent mail to LIM invalid recipients during PERIOD.; \
           \N}\N | EXIMBINARY WARNTO"}}
        control = freeze/no_tell
        control = submission/domain=
        add_header = X-Authenticated-As: $acl_m_user

    accept authenticated = *
        control = submission/domain=
        logwrite        = ACL - ACCEPTED (authenticated block)
########################################################################
#            END of authentication replacement.                        #
########################################################################

########################################################################
#      This block Replaced with the following                         #
#           for Authentication Limitiing:                              #
#        accept  hosts           = +relay_from_hosts                   #
#                logwrite        = ACL - ACCEPTED                      #
#                endpass                                               #
########################################################################
accept hosts = !@[] : +relay_from_hosts
        set acl_m_user = $sender_host_address
                         # or username from RADIUS
        condition = ${if exists{$spool_directory/blocked_relay_users}}
        condition = ${lookup{$acl_m_user}lsearch\
                    {$spool_directory/blocked_relay_users}{1}{0}}
        control = freeze/no_tell
        control = submission/domain=
        add_header = X-Relayed-From: $acl_m_user

  accept hosts = !@[] : +relay_from_hosts
        !verify = recipient/defer_ok/callout=10s,defer_ok,use_sender
        ratelimit = LIM / PERIOD / per_rcpt / relayuser-$acl_m_user
        continue = ${run{SHELL -c "echo $acl_m_user \
           >>$spool_directory/blocked_relay_users; \
           \N{\N echo Subject: relay user $acl_m_user blocked; echo; echo \
           because has sent mail to LIM invalid recipients during PERIOD.; \
           \N}\N | EXIMBINARY WARNTO"}}
        control = freeze/no_tell
        control = submission/domain=
        add_header = X-Relayed-From: $acl_m_user

  accept hosts = +relay_from_hosts
        control = submission/domain=
        logwrite        = ACL - ACCEPTED (hosts block)
######################################################################
#            END of authentication Replacement.                      #
######################################################################

######################################################################
# Hello checks added November 1 2009.
######################################################################

# If the remote host greets with an IP address, then reject the mail.
        deny    message         = RATWARE - IP address.
                log_message     = ACL - DENIED - RATWARE remote host used IP
address in HELO/EHLO greeting
                condition       = ${if isip
{$sender_helo_name}{true}{false}}

# Likewise if the peer greets with one of our own names
        deny    message         = RATWARE - Fake HELO Domain
                log_message     = ACL - DENIED - RATWARE remote host used
our name in HELO/EHLO greeting.
                condition       = ${if match_domain{$sender_helo_name}\
                               
{$primary_hostname:+local_domains:+relay_to_domains}\
                                {true}{false}}
        deny    message         = RATWARE - No HELO
                log_message     = ACL - DENIED - RATWARE remote host did not
present HELO/EHLO greeting.
                condition       = ${if def:sender_helo_name {false}{true}}

# If HELO verification fails, we add a X-HELO-Warning: header in the
message.
        warn    message         = X-HELO-Warning: Remote host
$sender_host_address \
                                ${if def:sender_host_name
{($sender_host_name) }}\
                                incorrectly presented itself as
$sender_helo_name
                log_message     = ACL - WARN - UNVERIFIABLE HELO/EHLO
greeting.
                !verify         = helo

# An attempt to block using list of subjects
###########################################
#        deny    message         = X-Blackhole: Yes
#                log_message     = REJECTED - Subject in blocksubject list -
$h_Subject
#                condition       = ${if
exists{/etc/virtual/blocksubject.txt}\
#                                 
{${lookup{$h_Subject:}wildlsearch{/etc/virtual/blocksubject.txt}{yes}{no}}}\
#                                  {no}}
############################################################################
#############

#######################################################################
# Mail is being rejected on some hosts because the mail MX is only set to
SMARTHOST
# and our server is rejecting it because some mail servers see the lesser
priorty
# MX and try to oour server directly instead of going though SMARTHOST.
        accept  domains         = +filtered_domains
                hosts           = +filtering_hosts
                verify          = recipient
                log_message     = ACL - ACCEPTED - MXTEXT
        deny    message         = Please use the public MX server for the
domain $domain
                domains         = +filtered_domains
                ! hosts         = +filtering_hosts
                log_message     = ACL - DENIED - MXTEST
######################################################################

######################################################################
# DNS checks
######################################################################
# The results of these checks are cached, so multiple recipients
# does not translate into multiple DNS lookups.
#
# If the connecting host is in one of a select few DNSbls, then
# reject the message.  Be careful when selecting these lists; many
# would cause a large number of false postives, and/or have no
# clear removal policy.
#
#       deny    dnslists        = dnsbl.sorbs.net : \
#               dnsbl.njabl.org : \
#               cbl.abuseat.org : \
#               bl.spamcop.net
#               message         = RBL - $sender_host_address is listed in
$dnslist_domain\
#                               ${if def:dnslist_text { ($dnslist_text)}}
#####################################################################

        deny    senders         = :
                condition       = ${if > {$recipients_count}{2}{1}}
                message         = Bounces must have only a single recipient
                log_message     = ACL - DENIED - BACKSCATTER - RECIPIENTS
$recipients_count
        deny    message         = rejected because $sender_host_address was
\
                                found in our blacklist
                hosts           = +blacklisted_domains
                log_message     = ACL - DENIED - BLACKLISTED DOMAIN FOUND IN
$blacklisted_domains
        deny
                ! condition    = ${lookup
dnsdb{defer_never,ptr=$sender_host_address}{yes}}
                log_message    = ACL - DENIED - NO PTR [rDNS] FOUND FOR
$sender_host_address
                message        = We do not accept mail from hosts with
missing \
                                or incorrect rDNS.
        deny    senders         = :
                ! hosts         = +whitelist
                ! domains       = +local_domains
                dnslists        = ips.backscatterer.org
                message         = This message looks like a bounce, and your
server is listed at \
                                ips.backscatterer.org, so I assume that this
is "backscatter". \
                                Please configure your mail server to not
send "backscatter spam". \
                                For advice, try
http://www.dontbouncespam.org/
                log_message     = ACL - DENIED - BACKSCATTER - INCOMING
        warn    set acl_m2      = ${lookup mysql{GREYLIST_TEST}{$value}{0}}
        defer   ! hosts         = +whitelist
                ! hosts         = +relay_from_hosts
                ! authenticated = *
                condition       = ${if eq{$acl_m2}{0}{yes}}
                condition       = ${lookup mysql{GREYLIST_ADD}{yes}{no}}
                message         = Now greylisted - please try again in 1
minute.
                log_message     = ACL - DEFERED - ADDING TO GREYLIST
        defer   ! hosts         = +whitelist
                ! hosts         = +relay_from_hosts
                ! authenticated = *
                condition       = ${if eq{$acl_m2}{1}{yes}}
                message         = Still greylisted - please try again in 1
minute.
                log_message     = ACL - DEFERED - STILL GREYLISTED
        defer
                ! hosts         = +whitelist
                ! hosts         = +relay_from_hosts
                ! authenticated = *
                condition       = ${lookup mysql{GREYLIST_UPDATE}{no}{no}}
                message         = Greylist update failed
                log_message     = ACL - DEFERED - GREYLIST UPDATE FAILED
        require verify          = sender
        accept  hosts           = +spf_bypass
                verify          = recipient
                spf             = fail
                logwrite        = SPF - REFLEXION $sender_host_address is OK
for \
                                $sender_address_domain
        deny    message         = SPF - INCOMING $sender_host_address \
                                is not allowed to send mail from
$sender_address_domain
                spf             = fail
                log_message     = ACL - DENIED - SPF Mismatch
        accept  domains         = +local_domains
                endpass
                message         = unknown user
                verify          = recipient
        accept  domains         = +relay_to_domains
                endpass
                message         = unrouteable address
                verify          = recipient

######################################################################
#                      ROUTERS CONFIGURATION                         #
#               Specifies how addresses are handled                  #
######################################################################
#     THE ORDER IN WHICH THE ROUTERS ARE DEFINED IS IMPORTANT!       #
# An address is passed to each router in turn until it is accepted.  #
######################################################################

begin routers

dnslookup_owm_www:
  driver = dnslookup
  domains = ! +local_domains
  condition = ${if eq {$sender_host_address}{127.0.0.1} {yes}{no}}
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more

dnslookup_local:
  driver = dnslookup
  domains = ! +local_domains
  condition = ${lookup {$sender_address_domain} lsearch
{/etc/virtual/domains} {yes}{no}}
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more

dnslookup_bounce:
  driver = dnslookup
  domains = ! +local_domains
  condition = ${if eq {$sender_address_local_part}{} {yes}{no}}
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more

dnslookup_alias:
  driver = dnslookup
  domains = ! +local_domains
  transport = remote_smtp
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
  no_more

spamcheck_router:
   driver = accept
   no_verify
   condition = "${if and { {!def:h_X-Spam-Flag:} {!eq {$received_protocol}\
   {spam-scanned}}} {1}{0}}"
   transport = spamcheck

virtual_alias:
  driver = redirect
  allow_defer
  allow_fail
  data = ${lookup {$local_part} lsearch {/home/$domain/mail/aliases}}
  domains = /etc/virtual/domains
  require_files = /home/$domain/mail/aliases
  condition = ${lookup {$local_part} lsearch {/home/$domain/mail/aliases}
{yes}{no}}
  qualify_preserve_domain
  retry_use_local_part
  check_ancestor
  one_time
  file_transport = address_file
  pipe_transport = address_pipe
  reply_transport = address_reply

autoreply_router:
  driver = accept
  require_files = /home/$domain/mail/auto-replies/$local_part
  transport = autoreply_transport
  no_verify
  unseen

virtual_localuser:
  driver = accept
  require_files = /etc/virtual/$domain/passwd
  domains = /etc/virtual/domains
  condition = ${lookup {$local_part} lsearch
{/etc/virtual/$domain/passwd}{$value}}
  transport = virtual_localdelivery

virtual_catchall:
  driver = redirect
  allow_defer
  allow_fail
  data = ${lookup {catchall} lsearch {/home/$domain/mail/aliases}}
  domains = /etc/virtual/domains
  require_files = /home/$domain/mail/aliases
  condition = ${lookup {catchall}lsearch{/home/$domain/mail/aliases}
{yes}{no}}
  qualify_preserve_domain
  retry_use_local_part
  check_ancestor
  one_time
  file_transport = address_file
  pipe_transport = address_pipe
  reply_transport = address_reply

localuser:
  driver = accept
  check_local_user
  condition = ${lookup {$sender_helo_name} lsearch
{/etc/virtual/domains}{YES}{NO}}
  transport = local_delivery

######################################################################
#                      TRANSPORTS CONFIGURATION                      #
######################################################################
#                       ORDER DOES NOT MATTER                        #
#     Only one appropriate transport is called for each delivery.    #
######################################################################

begin transports

remote_smtp:
        driver = smtp
        return_path_add = true

autoreply_transport:
  driver = pipe
  command = /usr/local/bin/autoreply.pl
/home/$domain/mail/auto-replies/$local_part

spamcheck:
  driver = pipe
  command = /usr/local/sbin/exim -oMr spam-scanned -bS
  use_bsmtp = true
  transport_filter = /usr/local/bin/spamc -u
${lookup{$domain}lsearch{/etc/virtual/domains_users}}
  home_directory = "/tmp"
  current_directory = "/tmp"
  # must use a privileged user to set $received_protocol on the way back in!
  user = mailnull
  group = mailnull
  log_output = true
  return_fail_output = false
  return_path_add
  message_prefix =
  message_suffix =

virtual_localdelivery:
  driver = appendfile
  create_directory = true
  directory_mode = 700
  file = /var/spool/virtual/${domain}/${local_part}
  headers_remove = "Bcc"
  return_path_add
  user = ${lookup{$domain}lsearch{/etc/virtual/domains_users}}
  group = mail
  mode = 660

local_delivery:
   driver = appendfile
   file = /$home/mail/$local_part
   delivery_date_add
   envelope_to_add
   return_path_add
   user = mailnull
   group = mail
   mode = 0660

address_pipe:
  driver = pipe
  return_output
  user = <anotheruser>

address_file:
  driver = appendfile
  delivery_date_add
  envelope_to_add
  return_path_add

address_reply:
  driver = autoreply

######################################################################
#                      RETRY CONFIGURATION                           #
######################################################################

begin retry

# Domain               Error       Retries
# ------               -----       -------
*                      quota_7d
*                      quota       F,72h,1h;
*                      *           F,60m,10m; F,11h,30m; F,12h,1h;
F,144h,2h;

######################################################################
#                      REWRITE CONFIGURATION                         #
######################################################################

# There are no rewriting specifications in this default configuration file.

begin rewrite

######################################################################
#                   AUTHENTICATION CONFIGURATION                     #
######################################################################

# There are no authenticator specifications in this default configuration
file.

begin authenticators

# For Netscape/Mozilla
plain:
  driver = plaintext
  public_name = PLAIN
  server_condition = "${if and{ {!eq{$2}{}}{!eq{$3}{}} \
   {crypteq {$3} {${lookup {${local_part:$2}} lsearch \
                            {/etc/virtual/${domain:$2}/passwd}\
                            {$value} {*:*}}}} } {1}{0}}"
  server_set_id = $2

# For Outlook/Outlook Express
login:
  driver = plaintext
  public_name = LOGIN
  server_prompts = "Username:: : Password::"
  server_condition = "${if and{ {!eq{$1}{}}{!eq{$2}{}} \
   {crypteq {$2} {${lookup {${local_part:$1}} lsearch \
                            {/etc/virtual/${domain:$1}/passwd}\
                            {$value} {*:*}}}} } {1}{0}}"
  server_set_id = $1

# End of Exim configuration file