the djb way

qmail with attitude

mailfront: smtp authentication revisited

We first looked at using smtp authentication with mailfront in this example. Although that article covered many of the mailfront basics, it primarily concerned itself with the mailrules mechanism, and was written before the plugin architecture had evolved.

In this example, we will revisit the mailfront service with smtp authentication. Only this time, we will require sender authentication for any connection to the service, and we will use plugins to achieve the configuration.

Recall that we want to run an instance of mailfront smtp service on port 587, to be used as a message submission agent for relaying mail from authorized senders who may be somewhere outside our local network. This is sometimes described as a "split-horizon" configuration, where we run one service on port 25 as usual for connections from other MTAs, and another service on port 587 to provide a mail relay for our own users.

But rather than open two ports as potential spam relays, it would be nice to limit connections on port 587 exclusively to authorized senders. That is, we would like to deny service to any sender whose credentials we have not explicitly authorized.

The require-auth plugin is used to enforce this restriction.

Before continuing, however, we need to give a strong warning about the examples below:

WARNING: The examples below are for illustration purposes only and are emphatically not secure as is. Specifically, password credentials are transmitted over the network in the clear, and are subject to eavesdropping. To secure these services properly you will need to:

  • run the connection in a secure channel, such as with stunnel
  • select a password storage mechanism with restricted access and encrypted passwords

Unfortunately, as of this writing, mailfront does not support a couple of features that would help us with the security issues:

  • CRAM-MD5 authentication, used to securely send an encrypted challenge+password over insecure lines.
  • STARTTLS support, providing encrypted communications during the authentication stage.

Until these become available, the only secure way to run the services shown here is by way of additional software:

  • the sslserver substitute for tcpserver in the ucspi-ssl package by William Baxter,
  • the sslsvd substitute for tcpserver in the ipsvd package by Gerrit Pape,
  • stunnel, socat, or other SSL wrapper/proxy methodology

Ok, warnings provided, now on to the example.

First, let's set up a new credential validation service, this one using a separate fgetpwent(3) password file (rather than /etc/passwd), and showing passwords stored in clear-text. A documented runscript follows:

# cvm-authsmtpd/run
# runscript for cvm service supporting mailfront-authsmtpd
# wcm, 2007.12.19 - 2007.12.19
# ===

## This service uses CVM_PWFILE authentication.
## (that is, our own credential file, like /etc/passwd)
## Environment (set up in envdir):
##   CVM_PWFILE_PATH=/path/to/mypwfile
##   CVM_PWFILE_PWCMP="plain"           # password stored in cleartext
##     or
##   CVM_PWFILE_PWCMP="crypt"           # password stored encrypted

## install to listen on this domain socket:

## other config:

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

## setup envdir:
if(test ! -d ./env){
    mkdir ./env
if(test ! -r ./env/CVM_PWFILE_PATH){
    echo $MY_PWFILE >./env/CVM_PWFILE_PATH
if(test ! -r ./env/CVM_PWFILE_PWCMP){
    echo $MY_PWCMP >./env/CVM_PWFILE_PWCMP

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

### EOF

(This is pretty much 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.)

Setup a logging service in the usual way, then create/edit the custom ./passwd file as a colon-delimited, 7-field fgetpwent(3) file using cleartext passwords (only the username and password are necessary for smtp authentication):


Add all the users who need remote submission access. Then set this file chmod 0400 to be readable only by root.

Note that using a separate password file for smtp auth may provide certain advantages over using the system password database in /etc/passwd:

When everything is set (run files executable?; logging directory ready?), link into the service activation directory to get it started. You can then test this new CVM service with the cvm-testclient utility:

$ cvm-testclient cvm-local:/var/run/cvm/cvm-authsmtpd amy '' h0tch1ck
user name:        amy
user ID:          0
group ID:         0
real name:        
group name:       (null)
system user name: (null)
system directory: (null)
domain:           (null)
mailbox path:     (null)

$ cvm-testclient cvm-local:/var/run/cvm/cvm-authsmtpd bob '' badpass
cvm-testclient: Fatal: Authentication failed, error #100 (Credentials rejected)

Good, it looks like the authentication service is working just dandy. Now on to the mailfront-authsmtpd service and back to all the plugin action. The runscript follows:

# mailfront-authsmtpd/run
# runscript for mailfront-authsmtpd service
# wcm, 2007.12.19 - 2007.12.20
# ===

## This service is intended to run as a "message submission agent"
## (ie, msa) to provide relay service to authenticated senders only.


## standard mailfront plugins, less 'cvm-validate', plus 'require-auth':
## (note: 'require-auth' is placed before 'accept-sender')
    check-fqdn counters mailrules
    relayclient qmail-validate
    add-received patterns require-auth accept-sender

## authentication is performed in concert with the cvm-authsmtpd service:

## Environment:
## Base directory of qmail (qmail backend) installation:
##   QMAILHOME=/var/qmail
## Banner provided on connection:
## Authentication is performed in concert with the cvm-authsmtpd service:
## Patterns to reject with 'patterns' plugin:
## (note: copy ./badmimes in this service to /var/qmail/control)
##   PATTERNS=/var/qmail/control/badmimes
## Settings for 'counters' plugin:

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

## setup envdir:
if(test ! -d ./env){
    mkdir ./env
if(test ! -r ./env/QMAILHOME){
    echo '/var/qmail' >./env/QMAILHOME
if(test ! -r ./env/CVM_SASL_PLAIN){
    echo $MY_SASL_CVM  >./env/CVM_SASL_PLAIN
if(test ! -r ./env/PATTERNS){
    if(test ! -r /var/qmail/control/badmimes){
        cp ./badmimes /var/qmail/control/badmimes
    echo '/var/qmail/control/badmimes' >./env/PATTERNS

## 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/sbin/mailfront smtp qmail $MY_PLUGINS
### EOF 

(This script demontrates the use of the plan9 rc shell as the command processor, the runit package in place of daemontools, and the ipsvd package in place of ucspi-tcp.)

All the magic happens in two places:

  1. Setting up the CVM_SASL_AUTH environment points to our cvm-authsmtpd service.
  2. The require-auth plugin makes smtp authentication mandatory.

Note that we still keep the accept-sender plugin as the last item in the plugin list. This is because the require-auth plugin action does not explicitly ACCEPT senders; it only REJECTs senders who fail authentication.

Add a logging service in the usual way, then link the service into the service activation directory. When the service is up and running, you can test it with an interactive smtp session:

$ mconnect somehost 587
220 0 mailfront ESMTP
250 0
250-SIZE 0
mail from:
530 5.7.1 You must authenticate first.
auth login
334 VXNlcm5hbWU6
334 UGFzc3dvcmQ6
235 2.7.0 Authentication succeeded.
221 2.0.0 Good bye.

The session shows that the dialog will not proceed unless and until the client authenticates, exactly as we want. The client passes username and password as base-64 encoded strings; this detail is handled automatically by most mail clients. To conduct a manual test as shown above, you will need to use a base-64 conversion utility such as mmencode or base64 on the username and password:

$ printf 'amy' | mmencode
$ printf 'h0tch1ck' |mmencode

And that's about it. Hmmm, except what is that business about removing the cvm-validate plugin? The answer is: since this service authenticates senders and allows them to relay mail to any recipient, we may not care about the need to validate our local recipients here.

But it is a damn good idea to validate recipients on our public port 25 smtp service, and we will show how in the next example.

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

Last edit 2007.12.27, wcm.