the djb way

qmail with attitude


mailfront and recipient validation

As I write this article, qmail-1.03 is approaching its tenth birthday, a rarely achieved milestone of longevity within the software universe.

Admittedly, though, the project is showing its age. Especially around the edges -- those parts that come into contact with the outside world -- qmail has become a bit yellowed and crusty.

In particular, the weakest link in any qmail installation today is its smtp server, qmail-smtpd. It was designed during a time that pre-dates the relentless onslaught of spam that now pounds away at every port 25 service, 24/7. These days qmail-smtpd is under strain to do the job right, and really can't, at least not without some kind of outside help.

You can find a truckload of patchsets that will try to fixup qmail-smtpd, a variety of efforts to make it more robust and featureful in the face of this spam onslaught. In fact most of the cottage industry of qmail patches is directed at qmail-smtpd, and more lines of patch files are committed to qmail-smtpd than any other qmail component. These patches may work well, or they may not. Patching is a brittle and error-prone strategy of software engineering.

On the other hand, you don't have to patch qmail-smtpd at all. Simply replace it with an alternative, engineered from the ground up to provide a modern and reliable smtp service, and designed to function flexibly and effectively in today's hostile spam environments.

This is the mailfront proposition. It starts from the premise that the qmail core is still pretty damn good. That is, all the activity within the queue, the processing logic, the stability and reliability, are still viable and effective to this day. (In our own analysis, a mere 60 lines of code -- less than 0.5% of the 16,000+ line total codebase -- may be patched to make for a very competent and portable qmail core installation.)

mailfront then builds upon this solid qmail core foundation by offering a full set of integrated replacements for all the interface services, including smtp, qmtp, qmqp, and pop3. These are provided as standalone programs included in the mailfront package, and you don't need to patch anything else in qmail to use them.

We have already reviewed a couple examples of mailfront smtp services, focusing on sender authentication and used primarily as a message submission service. This time we want to reconsider our standard port 25 smtp service, and focus particularly on the issue of recipient validation.

Of all the weaknesses in the original qmail-smtpd, perhaps the most significant is the issue of backscatter. This happens when qmail accepts mail from a forged sender address, for recipients who do not exist on the system. When qmail then bounces these message as undeliverable, they may be received by innocent third parties who had nothing to do with sending the original message. Our friendly qmail system has thus been exploited as a spam amplifier.

The original qmail-smtpd doesn't check to see if a recipient address is truly deliverable during the smtp dialog. As long as the hostname part of the recipient address passes the control/locals and control/virtualdomains checks, qmail will accept the message into its queue. Only later will qmail figure out if a recipient isn't deliverable, at which point the message becomes a bounce, rather than a reject at the source.

It would be far better if the smtp service could check the validity of a recipient address for each RCPT TO: command during the smtp dialog itself. The combination of cvm and mailfront provides the means to implement just this behavior.

One of the most interesting modules provided by the cvm package is called cvm-qmail. This credential validation module actually replicates all the standard qmail processing logic to find a deliverable recipient on a qmail system. That is, it uses all the standard qmail control/* files, adjusts the address as necessary for virtualdomains, checks users/cdb, /etc/passwd, and ~/aliases, and probes all the way into home directories for .qmail and .qmail-* files, just like qmail would. If the search is successful, cvm-qmail validates the recipient.

Here's a cvm-rcptto runscript that uses cvm-qmail to provide a recipient validation service:


#!/bin/rc
# cvm-rcptto/run
# runscript for cvm-rcptto service supporting mailfront-smtpd
# wcm, 2007.12.19 - 2007.12.19
# ===

## This CVM service uses the cvm-qmail module to see if
## RCPT TO: recipient addresses are valid recipients in
## the installed qmail environment.

## That is, the cvm-qmail module mimics all the qmail-local
## processing logic to find a user on the system.

## Environment:
##
## Where to find qmail configuration (standard installation):
##   QMAIL_ROOT=/var/qmail
##
## The cvm-qmail module requires a shared secret here
## and with the cvm-validate plugin used by mailfront-smtpd:
##   CVM_LOOKUP_SECRET='secret'
##

## listening on this local domain socket:
MY_SOCKET=/var/run/cvm/cvm-rcptto

## other config:
MY_QMAIL=/var/qmail
MY_SECRET='m00nb33m'

exec >[2=1]
echo '*** Starting cvm-rcptto ...'
echo '*** >> listening on local domain socket: '$MY_SOCKET

## setup envdir:
if(test ! -d ./env){
    mkdir ./env
}
if(test ! -r ./env/QMAIL_ROOT){
    echo $MY_QMAIL >./env/QMAIL_ROOT
}
if(test ! -r ./env/CVM_LOOKUP_SECRET){
    echo $MY_SECRET >./env/CVM_LOOKUP_SECRET
}


## let's go:
exec \
    /sbin/chpst \
    -e ./env \
       /usr/sbin/cvm-qmail cvm-local:$MY_SOCKET

### EOF

(Here again the standard djb way stuff for a service, showing the use of the plan9 rc shell installed in /bin/rc as the command processor, and the runit package in place of daemontools.)

NOTE: There is a small bug in the cvm-qmail with cvm-0.82, that requires control/virtualdomains to exist. If you don't have a virtualdomains file on your system, go ahead and touch(1) one:

# touch /var/qmail/control/virtualdomains

Then setup a logging service in the usual way. When everything is ready (run files executable?; logging directory/perms okay?), link into the service activation directory to get it started. You can then test this new CVM service with the cvm-testclient utility:

$ CVM_LOOKUP_SECRET=m00nb33m cvm-testclient cvm-local:/var/run/cvm/cvm-rcptto amy localdomain
user name:        amy
user ID:          4001
group ID:         4001
real name:        (null)
directory:        /home/amy
shell:            (null)
group name:       (null)
system user name: amy
system directory: /home/amy
domain:           localdomain
mailbox path:     /home/amy

$ CVM_LOOKUP_SECRET=m00nb33m cvm-testclient cvm-local:/var/run/cvm/cvm-rcptto tim localdomain
cvm-testclient: Fatal: Authentication failed, error #100 (Credentials rejected)

(Note here that, since cvm-qmail doesn't trade in passwords, we need to set the environment CVM_LOOKUP_SECRET to share a passphrase between the client and server.)

Voila! The cvm-rcptto service is working just as we would like, using full qmail logic to verify recipient addresses within our qmail installation. Moreover, it uses a Unix domain socket to provide an effective privilege partition between the smtp service (which runs as unprivileged user qmaild), and the cvm service, which runs with root privilege.

We can now use the new recipient validation service with our new mailfront-smtpd service:


#!/bin/rc
# mailfront-smtpd/run
# runscript for mailfront-smtpd service
# wcm, 2007.12.19 - 2007.12.20
# ===

## This service is intended to run as a public mail server on
## the MX host for the domain(s) being served.  We try to lock
## it down as well as possible to limit UCE (spam).

IP=0
PORT=25
CONLIMIT=19

## standard mailfront plugins in standard order:
MY_PLUGINS=(
    check-fqdn   counters     mailrules
    relayclient  cvm-validate qmail-validate
    add-received patterns     accept-sender
)

## recipient validation in concert with cvm-rcptto service:
MY_CVM_RCPTTO=cvm-local:/var/run/cvm/cvm-rcptto

## Environment:
##
## Base directory of qmail (qmail backend) installation:
##   QMAILHOME=/var/qmail
##
## Banner provided on connection:
##   SMTPGREETING=
##
## Settings for 'counters' plugin:
##   MAXRCPTS=
##   MAXHOPS=
##   DATABYTES=
##
## Settings for 'mailrules' plugin:
##   MAILRULES=/path/to/mailrules
##
## Settings for 'cvm-validate' plugin (validates recipient addresses):
##   CVM_LOOKUP=$MY_CVM_RCPTTO   # see cvm-rcptto service; uses cvm-qmail
##   CVM_LOOKUP_SECRET=          # must be *shared* with cvm-rcptto service!
##
## Settings for 'qmail-validate' plugin:
##   QMAILHOME=/var/qmail
##
## Settings for 'add-received' plugin:
##   FIXUP_* (many!)
##
## Patterns to reject with 'patterns' plugin:
## (note: copy the ./badmimes in this service into /var/qmail/control)
##   PATTERNS=/var/qmail/control/badmimes
##   PATTERNS_LINEMAX=
##   PATTERNS_RESP='message contains prohibited content (#5.3.4)
##


exec >[2=1]
echo '*** Starting mailfront-smtpd ...'
echo '*** >> using plugins: '$^MY_PLUGINS

## setup envdir:
fn setup_envdir {
    ## $1=VAR $2=VALUE
    if(test ! -d ./env){
        mkdir ./env
    }
    if(test ! -r ./env/$1){
        echo $2 >./env/$1
    }
}

setup_envdir QMAILHOME '/var/qmail'
setup_envdir DATABYTES '10000000'
setup_envdir MAXRCPTS  '20'
setup_envdir CVM_LOOKUP $MY_CVM_RCPTTO
setup_envdir CVM_LOOKUP_SECRET 'm00nb33m'
setup_envdir PATTERNS '/var/qmail/control/badmimes'
## ...

## setup iprules.cdb:
iprules-make || {\
    echo '*** !! failure creating iprules.cdb !!'
    exit 1
}


## let's go:
exec \
  chpst \
  -e ./env \
      tcpsvd -vv  \
      -c $CONLIMIT \
      -l 0 \
      -u qmaild \
      -x ./iprules.cdb \
      $IP $PORT \
          /usr/bin/rblsmtpd -B -t 300 \
          -r cbl.abuseat.org \
          -r list.dsbl.org \
          -r zen.spamhaus.org \
              /usr/sbin/mailfront smtp qmail $MY_PLUGINS


## insert before mailfront to provide greetdelay spam discourager:
# /usr/bin/greetwait.rc 

### EOF 

This runscript documents only some of the mailfront knobs and tunables; see the package documentation for complete information. This runscript also shows the use of rblsmtpd ahead of the smtp service, as we have previously described here.

The magic of recipient validation in this service is controlled by the following:

  1. Using the cvm-validate plugin.
  2. Setting CVM_LOOKUP to point to our cvm-rcptto service.
  3. Setting CVM_LOOKUP_SECRET with the shared secret.

When everything is configured to your requirements, setup a logging service in the usual way. Then shutdown and remove any existing port 25 service, and symlink this new service into your service activation directory.

Enjoy the result: a hardened smtp service on a solid qmail core, and no more backscatter!


Copyright © 2003 - 2007, Wayne Marshall.
All rights reserved.

Last edit 2007.12.27, wcm.