O AliasFile=null: and Lenny’s sendmailconfig

As is described in the bat book, when you set:

O AliasFile=null:

no aliases will ever be found. However, if you put in your sendmail.mc:

define(`ALIAS_FILE’, `null:’)dnl

and the run sendmailconfig on a Debian Lenny, update_db will complain because it cannot handle the null: declaration. If you are bothered by this warning message, the solution is simple. Do not declare null: and instead:

# cp /dev/null /etc/mail/aliases
# newaliases

sendmail: implementing a “catch all” address

You may find yourself in a situation where you may need to implement a “catch all” email address, i.e. every email that is directed to your domain regardless whether the user exits or not, not to be rejected but instead directed to a single mailbox. There are various approaches to the problem, and we will see some here:

First, the easy way:

Using FEATURE(`virtusertable’) one can do that in a single line:

@example.com          catch-all@delivery.host.name

You can even exclude some addresses and have email delivered to their own mailbox instead of catch-all:

user1@example.com          user1@delivery.host.name
user2@example.com          user2@delivery.host.name
@example.com                 catch-all@delivery.host.name

The sendmail.mc way:

Normally the above trick which is adequately described in cf/README and the bat book, should be enough. But there may be cases that it is not the solution that you want, or simply because it-is-not-invented-here. For example you may want to redirect to catch-all all email directed to existing users of the system, as opposed to the virtusertable trick which does this unconditionally:

LOCAL_CONFIG
Kuser user -m -a.FOUND

LOCAL_RULE_0
R$- < @ $=w . > $*        $: $(user $1 $) < @ $2 . > $3
R$- . FOUND < @ $=w . > $*          $@ catch-all < @ $2 . > $3

Or, you may want to redirect to the catch-all address all email directed to non-existing users of the system:

MODIFY_MAILER_FLAGS(`LOCAL', `-w')dnl
FEATURE(`local_procmail')dnl
MAILER(`smtp')dnl

LOCAL_CONFIG
Kuser user -m -a.FOUND

LOCAL_RULE_0
R$- < @ $=w . > $*        $(user $1 $)
R$- . FOUND          $#local $: $1
R$-                    $#local $: bit-bucket

In fact (with bit-bucket aliased to /dev/null) the above example silently discards every email not directed to an existing user.

sendmail: when local users are not users of the system – part 2

Continuing from the previous post in this series, let’s see how one can deal with incoming email that must be delivered both to physical users of the system and users not visible via /etc/passwd:

LOCAL_CONFIG
Kuser user -m -a.FOUND

LOCAL_RULE_0
# Unconditionally redirect email to abuse and Postmaster
RPostmaster  < @ $=w . > $*        $: Postmaster  < @example.com. > $3
Rabuse  < @ $=w . > $*        $: abuse  < @example.com. > $3

# Deliver email to yiorgos locally
Ryiorgos  < @ $=w . > $*        $# local $: $1

# Delete email directed to all other users in /etc/passwd
R$-  < @ $=w . > $*        $(user $1 $)
R$- . FOUND        $#local $: bit-bucket

# The following is valid only if sendmail is instructed to not check /etc/passwd.
# This is achieved with MODIFY_MAILER_FLAGS(`LOCAL', `-w')dnl
R$- < @ $=w . > $*        $#custom.local $: $1

What does the above snippet do? The first set of rules accepts all incoming email addressed to Postmaster and abuse and redirects it to Postmaster@example.com and abuse@example.com.

The second set of rules accepts and delivers locally all incoming email addressed to user yiorgos.

The third set deletes all incoming email for all other users listed in /etc/passwd. One may refine that using a (sendmail) class definition and decide to do so for incoming email addressed to users like man, daemon, lp etc. Remember that in Ruleset 0 you cannot call $#discard.

Assuming that you have written a special delivery agent (to save email in a database for example) for “local” users not found in /etc/passwd, the last rule calls that delivery agent for the given username.

Of course if you are in a certain mood of BOFHiness, you can add similar rules that return random error codes to the sender. The expressiveness of sendmail’s modem-noise is unlimited…

(part 1)

sendmail: when local users are not users of the system

Suppose that you are running a sendmail server which is the final delivery server and that the users of the mail system are not physical users on the server (ie. they do not exist in /etc/passwd). What choices do you have in order to accept valid local email?

  1. Use LDAP.
  2. Edit mbdb.c and add a map. You can add your custom map and the relevant hooks to support the external directory of your choice. Read the source on how to do that.
  3. Edit mbdb.c and wrap getpwnam(3). Similar to the above but it may seem easier in some cases, especially if the users are kept in /etc/passwd like file. The first time I saw such a trick was when I was reading TACACS+ code.
  4. Use MAILER(`local’) without the w flag, which means that /etc/passwd is not consulted prior to forking the mail delivery agent. This is accomplished by:

    MODIFY_MAILER_FLAGS(`LOCAL’, `-w’)dnl

    That way the local mailer and not sendmail decides whether the user exists or not. You have to write your own delivery agent.

Of the above choices I rely heavily on #3 (although I am not using flat files) and lately I used #4. LDAP is always my last choice. I am sure there are other choices though.

(part 2)

HFrom: $>ruleset and some sendmail ranting

Due to a spam burst last night, I was forced to write a ruleset in the spirit of my previous post:

LOCAL_CONFIG:
HFrom: $>BlockFrom

LOCAL_RULESETS
R$* "NEWS Sensation" $*        $#discard $: discard
R$*        $#OK

Again since this is going straight into your .mc file, it is not advised to use this method frequently. Nice five-liner to stop a spam burst, right?

No! As I was applying this filter I thought that although most people think that sendmail‘s most serious trouble is its bug / security track record, its most serious problem is that decision rules on routing and filtering messages come from all over the place: text files like mailertable, virtusertable, genericstable, relay-domains, access, the sendmail.cf rulesets themselves like the example above and more importantly the milters, their defaults and their configuration files (if any).

Say for example that you do not want email directed to abuse@ to be filtered at all. Depending on your rules and how they are written, you may have to upgrade your rulesets, edit access and probably write an exception for every single milter that you are applying. One exception might need to be declared in five different places. Talk about complication and management nightmare! That is why I like running MIMEDefang: It gives me a Perl interpreter and the ability to implement most of the functionality of any other milter I wish to apply. Have I reached that point of being able to run only one milter? No, but I have set this as a goal. MIMEDefang is not my only choice to this path, j-chkmail seems like a good alternative.

However, I am not the only one who manages our global filters, and I want to make it easier for the other admins to add their own global filters in my absence. Oh how much easier it could have been if the libmilter API offered as a return value a variant of SMFIS_ACCEPT that would instruct sendmail to accept the message with no more milters applied to it. Currently SMFIS_ACCEPT instructs the current milter to accept the message. Which is why if you want to write a global whitelist exception, you may have to write it for all milters that are enabled.

Oh well, I think all I want and wish for is to minimize the number of files I have to edit in order to implement a certain policy (or have a tool that can read the policy from one place and edit n-files for me) and a change to the libmilter API (which I do not know whether it is trivial or not). Suggestions to switch to alternatives like Postfix, Qmail or even Exim are outside the scope of this rant. I prefer to take the “Sendmail Theory and Practice” route and write the whole .mc file by hand instead.

HSubject: and rulesets

This is a variation of the bat book‘s subject header checking trick. Assuming that you want to block messages based on the content of the Subject: header of an incoming message, you can place the following rules into your .mc file:

LOCAL_CONFIG
HSubject: $>BlockSubject

The above basically instructs sendmail to call ruleset BlockSubject with the value of the subject. On with the ruleset now:

LOCAL_RULESETS
SBlockSubject
# The next rule is broken in two for readability!
R$* test - block this message $*
        $#error $: "553 message blocked due to Subject: " $&{currHeader}
R$* Your new password $*        $#discard $: discard
R$* Casinoo $*        $#discard $: discard
R$*        $#OK

(You may want to change the $> operator with $>+. Read paragraph 25.5.1 of the fourth edition of the bat book for a discussion on the matter.)

The bat book prefers to put all the unacceptable subjects in an external database file (which is maintained much like aliases and virtusertable). I prefer keeping the list of the unacceptable subjects inside the .mc file for two reasons:

First, keeping them in a file outside the .mc makes the list grow faster. Editing the .mc to add yet another unacceptable subject makes one think whether to do so or not.

Second, although a subject that contains a certain phrase may be considered unacceptable, you might want to make an exception. For example one may decide to block all the Your new password messages except ISP name – Your new password message that your MIS sends to your users when they reset their password. This can easily be maintained in one place in the .mc file and is also self documented modem noise code:

R$* ISP - Your new password $*        $#OK
R$* Your new password $*        $#discard $: discard

Remember, do not copy-paste sendmail.mc code. The LHS and the RHS are tab separated. Copy-pasting converts tabs to spaces and your ruleset will not work.

$#discard and ruleset 0

Note to self: The Sendmail Installation and Operation Guide (at least version 1.25 of doc/op.ps) clearly states:

“The mailer with the special name “discard” causes any mail sent to it to be discarded but otherwise treated as though it were successfully delivered. This mailer cannot be used in ruleset 0, only in the various address checking rulesets.”

So, instead of writing something like this:

LOCAL_RULE_0
R$={NIL}  < @ $=w . > $*        $#discard $: $1

you need to write something like this:

LOCAL_RULE_0
R$={NIL}  < @ $=w . > $*        $#local $: bit-bucket

otherwise you get beaten by “buildaddr: unknown mailer discard” errors, even though $#discard is very well known (to you; not to ruleset 0).

The above example assumes that $={NIL} is a class that contains usernames for which we do not want to accept any email and bit-bucket is an entry in /etc/mail/aliases:

bit-bucket: /dev/null

Yes, it is possible to achieve the same thing using FEATURE(`virtusertable’), but you can use this hack as a guide when you have more complex situations, where you may need to decide programmagically on whether to discard the email or not.

$#random – a local mailer that returns a random EX_*

I found myself in a situation where I needed a local mailer that was required to return random exit values0 for email directed to certain users. Luckily, most of the job can be done via your .mc file:

MAILER_DEFINITIONS
Mrandom,        P=/opt/bin/random,
                F=lsDFMqbE,
                S=ruleset_noop, R=ruleset_random,
                T=DNS/RFC822/X-Unix,
                A=random $u

LOCAL_CONFIG
C{Random} adamo yiorgos admin system operator
Kcomp arith

LOCAL_RULESETS
Sruleset_noop
# This ruleset does nothing

Sruleset_random
# EX__BASE == 64 and EX__MAX == 78. EX_OK == 0, so we mask it to 79.
R$*             $: $1 . $(comp r $@ 64 $@ 79 $)
R$* . 79        $@ $1 . 0

LOCAL_RULE_0
R$={Random}  < $=w . > $*        $#random $: $1

When $#random is called, ruleset_random is called with $1 (the username) as an argument and returns $1.number, where number is a random number between EX__BASE (64) and EX__MAX (78) or zero. Therefore, the $#random binary is executed with $1.number as an argument.

/opt/bin/random can be as simple as this shell script1:

#!/bin/sh
# This is an example (non) delivery agent and is not considered safe to run on a 
# production server.
status=`echo $1 | sed -e 's/.*\.\(.*\)$/\1/'`

while read x
do
        # noop
done

exit $status

A random EX_* value is returned to the sender of the email, assuming s/he has emailed a local user contained in class $={Random}.


[0] – Valid sendmail exit codes (EX_*) are defined in sysexits.h.
[1] – In my case I wrote the mail delivery agent in C, as I needed to do a few more stuff prior to calling exit().

milter-dnsbl

Sendmail administrators using FEATURE(dnsbl) may have noticed that ruleset check_rcpt is executed after all connected milters have executed the corresponding xxfi_*() routines.

Wouldn’t it be better if a milter (in fact the first in order) could block a connection based on a list of DNSBLs?

That is why I wrote my first milter, milter-dnsbl (download). milter-dnsbl has no configuration file; on startup it takes a number of arguments that allow you to specify a number of DNSBLs, plus whitelists published via DNS, or based on the domain name of the connecting host. It requires a running lwresd(8) which it uses as a caching server. Read the manpage that comes with the source code distribution.

milter-dnsbl is distributed with an OpenBSD-style license and has been tested on an Ubuntu 6.06 i386 server.