Postfix

[TOC]

Written for CentOS 6.4, Postfix 2.6.6.

  • Server is example.com.

  • Mail users map to local accounts (i.e., in /etc/passwd).

  • DNS looks like this:

    [me@example.com ~]$ dig example.com -t MX +short  
    10 mail.example.com.
    
  • Made sure that reverse DNS is set up properly.

Overview

Things were easier to set up after understanding these things, even
cursorily[^1].

Mail Transfer - Basic Idea

Lots of formal abbreviations! They are, luckily enough, quite
sensible
. Here’s the basic
flow:

Sender > Server > Server > ... > Server > Receiver  
(MUA)    (MTA)    (MTA)          (MTA)     (MUA)

You can be a bit more granular:

   Sender   >  Server > Server > ... >   Server     | Delivery |  >   Receiver  
(MUA > MSA)    (MTA)    (MTA)          (MTA > MDA)  | complete |     (MRA > MUA)

(MDAs, when local, are also called LDAs.)

This separation of purpose is good since you can use a variety of
applications and
topologies

at each stage. Lot of possibilities. E.g.

Sender > Postfix > Procmail > Clam Anti-Virus > SpamAssassin > Procmail > Fetchmail > Receiver  
(MUA)    (MTA)     (                       MDA                        )     (MRA)       (MUA)

Open Relays

Where it is not required to (1) authenticate to your server, and/or (2)
be in the same network as the server to send email. This is very bad for
public-facing mail servers. From a simpler time when there were very few
email servers and everybody was nice to each other.

Mailbox Formats

There are quite a few, each
with its own pros and cons. I personally like Maildir.

Installation

yum install postfix cyrus-sasl  
ln -s /usr/sbin/sendmail.postfix /etc/alternatives/mta --force

Configuration

The Basics

Postfix ships with sane and secure defaults. Here’s stuff I changed in
/etc/postfix/main.cf

First set the hostname and domain

myhostname = example.com  
mydomain = $myhostname

Mail from this server will come from this domain.

myorigin = $mydomain

Accept mail on specified interface[^2] and all protocols (IPv4 and
IPv6[^3])

inet_interfaces = all  
inet_protocols = all

This server will think itself the final MTA (in the chain above) for
these domains:

mydestination = $mydomain, localhost

The server will only trust itself[^4]

mynetworks_style = host

Use the Maildir format for message delivery

home_mailbox = Maildir/

Change the banner for fun (and no version information)

smtpd_banner = $myhostname ESMTP Why, hello there!

Now edit [http://www.postfix.org/master.5.html /etc/postfix/master.cf]
to enable the submission port[^5].

submission inet n       -       n       -       -       smtpd

Uncomment any options (‚Äú-o‚ÄĚ); we‚Äôll take care of these in later.

Testing

Restart the postfix service. Then from another computer,

orangebox:~$ telnet example.com 25  
Trying 96.126.123.32...  
Connected to example.com.  
Escape character is '^]'.  
EHLO example.com  
250-example.com  
250-PIPELINING  
250-SIZE 10240000  
250-VRFY  
250-ETRN  
250-ENHANCEDSTATUSCODES  
250-8BITMIME  
250 DSN  
MAIL FROM: testuser@internet.com  
250 2.1.0 Ok  
RCPT TO: me@example.com  
250 2.1.5 Ok  
DATA  
354 End data with <CR><LF>.<CR><LF>  
Subject: Hello!  
How have you been?  
.  
250 2.0.0 Ok: queued as 7602C72C2  
QUIT

This should:

  • Work for a valid user

    MAIL FROM: postmaster@example.com  
    250 2.1.0 Ok
    
  • Not work an invalid user

    RCPT TO: nonexistent@example.com  
    550 5.1.1 <nonexistent@example.com>: Recipient address rejected: User unknown in local recipient table
    
  • Deliver a mail to your home folder! The ‚ÄúMaildir‚ÄĚ folder will be
    created automagically.

    ~/Maildir  
    ¬†‚Ēú‚ĒÄ‚ĒĬ†cur  
    ¬†‚Ēú‚ĒÄ‚ĒĬ†new  
    ¬†‚Ēā¬†¬†¬†‚ĒĒ‚ĒÄ‚ĒĬ†1377029606.Vca00I4025fM640219.example.com  
    ¬†‚ĒĒ‚ĒÄ‚ĒĬ†tmp
    

But this is done insecurely. Let’s fix that.

Doing things securely

Generate a self-signed certificate[^6]

openssl req -new -x509 \  
            -newkey rsa:4096 \  
            -days 3650 \  
            -nodes \  
            -out /etc/pki/tls/certs/dovecot.crt \  
            -keyout /etc/pki/tls/private/dovecot.key

chmod o= /etc/pki/tls/private/dovecot.pem

Now configure Postfix to use these certificates for TLS

smtp_tls_security_level = may  
smtpd_tls_security_level = may  
smtpd_tls_cert_file = /etc/pki/tls/certs/postfix.crt  
smtpd_tls_key_file = /etc/pki/tls/private/postfix.key  
smtpd_tls_auth_only = yes  
smtpd_tls_loglevel = 3  
smtpd_tls_received_header = yes  
smtpd_tls_session_cache_timeout = 3600s  
tls_random_source = dev:/dev/urandom

Restart Postfix. As always, see /var/log/maillog for any errors.

Now test.

Important Stuff

  • You have to use the OpenSSL client instead of telnet from this point on!

  • Watch out for non-zero ‚ÄúVerify return codes‚ÄĚ. Avoid these by providing a full path to the system root certs. This is normally done via openssl version -d. On OS X, use ‚ÄúKeychain Access‚ÄĚ to export all the stuff under ‚ÄúSystem Roots‚ÄĚ into a single PEM file.

  • Keep the former telnet commands lowercase! Else, the client will renegotiate every time you type RCPT TO. OpenSSL can waste your time like that!

    openssl s_client -starttls smtp \
                     -CAfile /path/to/roots.pem \
                     -connect example.com:25

Some restrictions

Stepping throught the telnet output in the previous section, start
adding some restrictions to the client connection^7:

smtpd_client_restrictions = reject_unknown_client_hostname, permit

Then the HELO command

smtpd_helo_required = yes  
smtpd_helo_restrictions = reject_unknown_helo_hostname, reject_non_fqdn_helo_hostname, reject_invalid_helo_hostname, permit

MAIL FROM

smtpd_sender_login_maps = pcre:/etc/postfix/login_maps.pcre  
smtpd_sender_restrictions = reject_non_fqdn_sender, reject_sender_login_mismatch, reject_unknown_sender_domain, permit

And finally, RCPT TO
This will allow you to relay messages (i.e. send email to other
domains) if you’re SASL-authenticated.

smtpd_recipient_restrictions = permit_sasl_authenticated, reject_unauth_destination, permit

Add this to /etc/postfix/login_maps.pcre[^8].

/^(.*)@example.com$/   ${1}

Test away! You should see good errors like:

450 4.1.8 <askljdas@lksjdklfsdjf.com>: Sender address rejected: Domain not found  
450 4.7.1 <blarghhh>: Helo command rejected: Host not found  
553 5.7.1 <me@example.com>: Sender address rejected: not logged in

Now add a way in which you an log in to the server remotely to send
messages through it.

SASL Authentication

Will use Cyrus.
Postfix uses it by default.
You can see what other libraries Postfix was compiled with support for as well:

[root@example ~]# postconf -a
cyrus  
dovecot

Install Cyrus

yum install cyrus-sasl

You can then see what authentication methods Cyrus supports:

[root@example !]# saslauthd -v
saslauthd 2.1.23  
authentication mechanisms: getpwent kerberos5 pam rimap shadow ldap

Install the appropriate package. Since I’m using plain auth,

yum install cyrus-sasl-plain

Since we’re dealing with local accounts, let’s tell Cyrus to use
/etc/shadow. Open /etc/sysconfig/saslauthd:

SOCKETDIR=/var/run/saslauthd  
MECH=shadow
FLAGS=

Start the service

service saslauthd start

Make sure it starts when you reboot your server

chkconfig saslauthd on

Test!

[root@example !]# testsaslauthd -u testuser -p secretpassword
0: OK "Success."

Now tell Postfix to use Cyrus in /etc/postfix/main.cf

smtpd_sasl_auth_enable = yes  
smtpd_sasl_type = cyrus

Set some security
options

smtpd_sasl_security_options = noanonymous

Restart the Postfix service. Test:

[root@toolkit ~]# openssl s_client -starttls smtp -CAfile /path/to/postfix.crt -connect example.com:25  
(Certificate, connection info)  
---  
250 DSN  
helo example.com**  
250 example.com  
auth plain An8o0tjsHojfDausWtzblk4bnZA
235 2.7.0 Authentication successful

Generate the funky MD5 output with your username and password:

echo -ne '\000user\000password' | base64

Preventing Spam, Bad Email, and DOS Attacks

Using Blocklists

Change smtpd_recipient_restrictions to add some blocklists[^9] and
other stringent policies:

smtpd_recipient_restrictions =   
  permit_sasl_authenticated,   
  reject_invalid_hostname,   
  reject_non_fqdn_sender,  
  reject_non_fqdn_recipient,  
  reject_unauth_destination,  
  reject_rbl_client zen.spamhaus.org,  
  reject_rbl_client psbl.surriel.com,  
  reject_rbl_client bl.spamcop.net,  
  permit

Most residential IPs are banned by blocklists, so keep that in mind when
testing your setup:

554 5.7.1 Service unavailable; Client host [173.29.77.33] blocked using zen.spamhaus.org;
http://www.spamhaus.org/query/bl?ip=173.29.77.33  

Using SPF

Sender Policy Framework
prevents fake sender addresses from your domain. It’s a great idea and
is something everyone should do[^10].

To empower Postfix with SPF, first install some required packages from
EPEL:

yum install perl-core perl-Mail-SPF --enablerepo=epel

I’m going to try the Perl implementation of
SPF
. [^11] Download,
extract, move to a good place:

tar -xvzf postfix-policyd-spf-perl-2.010.tar.gz  
cd postfix-policyd-spf-perl-2.010  
mv postfix-policyd-spf-perl /usr/local/bin/

Now set up /etc/postfix/main.cf. Add to smtpd_recipient_restrictions

# Other options not shown for brevity  
smtpd_recipient_restrictions =   
  check_policy_service unix:private/policy-spf,  
  policy_time_limit = 3600 # Default is 1000; too short[^12]

Then add to /etc/postfix/master.cf

policy-spf unix  -       n       n       -       0       spawn  
    user=nobody  argv=/usr/bin/perl /usr/local/bin/postfix-policyd-spf-perl

Restart the service. Send yourself an email from another service Gmail,
and look for SPF output in /var/log/maillog

Some Limits on Interaction

The first setting is the denominator for the "limit"s

# Connection limits  
anvil_rate_time_unit = 120s  
smtpd_client_connection_rate_limit = 2400  
smtpd_client_message_rate_limit = 12000  
smtpd_error_sleep_time = 60

Prevent Abuse

Greylisting is a great
approach to fighting spam. The idea is that spammy mail servers do not
respect the RFC spec that, if an email couldn’t be delivered initially,
they are to re-attempt delivery later.

Postgrey works well for this. By
default, it asks MTAs to attempt redelivery in 5 minutes.

yum install postgrey  
service postgrey start  
chkconfig postgrey on

This will run on a Unix socket. The next step is to get Postfix to use
it. Edit /etc/postfix/main.cf.

# Other options not shown for brevity  
smtpd_recipient_restrictions =  
    check_policy_service unix:postgrey/socket

I lowered the default wait time to a minute by creating
/etc/sysconfig/postgrey and adding this:

OPTIONS=$OPTIONS" --delay=60"

Restart Postfix and Postgrey. You’ll see something like this in
maillog to make sure it’s working:

postgrey[12582]: action=pass, reason=client whitelist, client_name=mail-qc0-f169.google.com,   
    client_address=209.85.216.169, sender=foo@bar.com, recipient=test@example.com

Miscellaneous

‚Äúwarning: dict_nis_init:‚ÄĚ

Disable NIS lookups

alias_maps = hash:/etc/aliases

‚ÄúRelay Access Denied‚ÄĚ

Usually something
quite simple.

References

Footnotes

[^1]: The Linode page on mail
servers is also a great overview

[^2]: Can specify IP address also:

    inet_interfaces=all
    inet_interfaces=eth0,eth1
    inet_interfaces=38.9.127.1,10.0.1.23
    inet_interfaces=mail.tux.com

[^3]: Default is IPv4

[^4]: Can trust network classes or subnets and specific IP addresses

[^5]: I was a little confused about this but think I understand. Port 25
is the standard SMTP port that used for MTA-to-MTA communication. So
if you have a user who is behind an ISP connection that blocks port
25 (for spam or other reasons like bad proxying), they can still
send/submit mail to your server, even if it’s not the final
destination on the message envelope, on port 587.

[^6]: Can also use StartSSL or CACert.

[^8]: From ServerFault. Postfix can
use a lot more formats for controlled envelopes. See the output of
postconf -m. For instance, I initally used this file (Specified
with hash:/path/to/file):

    # Envelope sender Owner
    me@example.com    me

[^9]: Of which there are a lot
available

[^10]: To get started, read about the syntax
or use a wizard, then the validation tool.

[^11]: Tried to make the Python version work but ran into issues with
Python3 and the ipaddr module.