Sendmail relay configuration without losing your mind

I have spent the last week reconfiguring Sendmail on a small mail relay I run for a couple of friends. The job took longer than I expected. The reason it took longer than expected is that Sendmail's configuration is famously dense — the sendmail.cf file is several thousand lines of cryptic line-noise — and the relay-related parts of it are the densest.

This post is the notes I wish someone had written for me before I started.

The problem the configuration is solving

Sendmail is a mail transport agent: it accepts mail from senders, forwards it to receivers. The fundamental security question for a mail relay is who is allowed to use the relay to send mail to other people.

In the early days of SMTP, the answer was "anyone". Open relays were the norm. They worked because the population of SMTP-speaking hosts was small and well-behaved.

That era is over. Open relays are now exploited within hours of being put online — used as bounce-points for spam, primarily. The cost to the operator is bandwidth, reputation, and, increasingly, being added to the RBL blackholes that other mail systems use to reject your traffic.

The modern Sendmail configuration has to express, precisely, who the legitimate users of the relay are, and reject everyone else.

The configuration mechanism, explained

There are two ways to think about who counts as a legitimate user.

Source-based. Anyone connecting from a trusted IP range — the office LAN, perhaps — is allowed to relay. Anyone else is not.

Authentication-based. Anyone who can prove they are a known user, regardless of source, is allowed to relay. This requires SMTP AUTH, which Sendmail supports but which is genuinely not yet standard practice.

For the relay I run, the source-based approach is simpler and matches how the friends use it.

The Sendmail mechanism for source-based relay control is the access database. It is a flat file with lines like:

localhost.localdomain   RELAY
localhost               RELAY
127.0.0.1               RELAY
192.0.2.0/24            RELAY
spammer-domain.example  REJECT

Each line is a token (an IP, a network, a domain) and an action. RELAY means "this source is allowed to send mail through us to anywhere". REJECT means "reject mail from this source unconditionally".

The file is plain text, but Sendmail does not read it directly. You have to compile it into a Berkeley DB file with makemap:

makemap hash /etc/mail/access < /etc/mail/access

Forgetting this step is the single most common mistake. You add a line, you reload Sendmail, the line has no effect, and you are confused for an hour.

What RELAY actually means

This is the part I had to read three times.

A mail relay processes traffic in two directions. Inbound: mail arriving for users on this server. Outbound: mail from users on this server, going elsewhere.

For inbound traffic, no relay permission is needed — Sendmail is the destination. It accepts the mail.

For outbound traffic — mail from elsewhere, going to elsewhere — Sendmail will refuse by default. This is correct.

The RELAY action in the access file says: for traffic where the sender matches this rule, also permit outbound relaying. In other words, treat this source as a legitimate user.

The distinction matters because it changes how you think about the rules. The file is not a list of "who can talk to me". It is a list of "who I will deliver outbound mail on behalf of".

The sane minimum configuration

For a small relay, here is the minimum I think you need.

In /etc/mail/access:

localhost                RELAY
localhost.localdomain    RELAY
127.0.0.1                RELAY
192.0.2.0/24             RELAY

This allows the local machine and a single trusted network to relay. Everyone else gets relayed-traffic refused, with the standard SMTP error.

In /etc/mail/sendmail.mc (the source for sendmail.cf, processed through m4):

FEATURE(`access_db', `hash /etc/mail/access')dnl
FEATURE(`relay_hosts_only')dnl

The first line says "use the access database". The second says "only the explicit hosts in the access database are trusted; nobody else, even local machines you have not listed".

Build and install:

m4 /etc/mail/sendmail.mc > /etc/mail/sendmail.cf
makemap hash /etc/mail/access < /etc/mail/access
kill -HUP `cat /var/run/sendmail.pid`

Verify by trying to relay from an IP not on the list. Sendmail should respond with a 550 Relaying denied.

The test you must do

Before declaring victory, test from outside. Specifically: connect to your relay from an external IP, and try to send mail from external@external.com to recipient@somewhere-else.com.

telnet your-relay.example 25
220 your-relay.example ESMTP Sendmail ...
HELO test.example
250 your-relay.example Hello test.example
MAIL FROM: <external@external.com>
250 2.1.0 <external@external.com>... Sender ok
RCPT TO: <recipient@somewhere-else.com>
550 5.7.1 <recipient@somewhere-else.com>... Relaying denied

If the RCPT TO line comes back with anything other than a 550, your relay is open and you have work to do.

Do this test from a different IP than the relay's own. The most common mistake is testing from localhost, which is on the trust list, which makes the relay look open when it is not.

There is also a public open relay tester at abuse.net which will run a series of checks for you. This is what other operators will use to decide whether to blacklist your relay; it is worth running it on yourself first.

The configuration system itself

The one piece of advice I will end with: do not edit sendmail.cf directly. Edit sendmail.mc and rebuild. The .cf file is essentially compiled output. Hand-editing it is the road to madness.

The m4 macro language is also unfamiliar at first, but it is small enough to learn properly in an afternoon, and the O'Reilly Sendmail book is genuinely good if you can get hold of it. After two weeks of fighting it I now find the .mc format reasonable; the .cf format I will never love.


Back to all writing