Summary

This guide shows how to setup a full mail system using Postfix, including how to setup DNS records, SPF, DKIM, and Sieve filtering. Installing Dovecot for IMAP/POP and webmail packages are beyond the scope of this document.

The major settings changed here are the smtpd_*_restrictions which define the rejection rules. At this point, I've noticed that 90% of spam is blocked; some still gets through. I prefer not to use DNSBLs for several reasons, but mainly due to the lack of control and a high rate of false positives. I also recommend using fail2ban for added security.

MONITOR YOUR MAIL LOG FOR A FEW WEEKS TO CONFIRM THINGS ARE WORKING THE WAY YOU WANT.

Main.cf

The main configuration for Postfix is typically located at /etc/postfix/main.cf.

General Options

On Debian, the smtpd_banner is modified to include the string (Debian/GNU). Other systems/packages may add the version number of Postfix here. Since the banner is shown to all connecting MTAs and clients, it should be set to exclude this information.

smtpd_banner = $myhostname ESMTP $mail_name

Set a per-message size limit. The size is specified in bytes. This limits the size of each message to 25 MB (to allow for attachments). The default size in Postfix is a bit small. Don't set this to 0 (unlimited).

message_size_limit = 25000000

It's not too important to specify the IP interfaces if your system only has one IP, but it is good practice and could have security implications on a system with more than one address. Obviously, use your own IPs.

inet_interfaces = 167.114.96.243 2607:5300:100:200::fce

After a client has one SMTP error, delay responses for 8 seconds for each subsequent error. At 3 errors, disconnect them. Postfix's defaults are a bit liberal here; I tend not to be tolerant of clients that don't follow the rules.

smtpd_error_sleep_time = 8s
smtpd_soft_error_limit = 1
smtpd_hard_error_limit = 3

SSL Options

The first setting enables STARTTLS on all available ports (typically smtp on 25 and submission on 587). This should always be set to allow both plaintext and encrypted connections for connecting MTAs and clients. The second setting requires the use of TLS when using AUTH (for the submission port). This way, AUTH logins are never over plaintext.

smtpd_tls_security_level = may
smtpd_tls_auth_only = yes

Generate unique SSL keys and specify them here. I use 4096 RSA keys.

smtpd_tls_cert_file = /etc/ssl/private/postfix.cacert.pem
smtpd_tls_key_file = /etc/ssl/private/postfix.privkey.pem

Weak encryption is more secure than plaintext. Please read the Postfix mailing list thread on disabling anonymous Diffie-Hellman for more information. I strongly recommend that the smtp_tls_ciphers and smtp_tls_protocols be left at default. Use GnuPG if you require end-to-end encryption.

SMTP Restrictions

This is the fun part: setting up restrictions for connecting MTAs/clients. I recommend you understand the options instead of copying/pasting.

Change all 5xx (permanent failure) codes to 4xx (temporary failure; try again later). This is great for testing a new configuration; remember to remove it. Also, receive notifications to postmaster@ for all errors, including connecting MTA/client errors. NOTE: THIS CAN DIVULGE CONFIDENTIAL INFORMATION. Use this to monitor your new configuration, then when you're sufficiently satisfied, remove it to return to the default.

soft_bounce = yes
notify_classes = bounce, 2bounce, data, delay, policy, protocol, resource, software

Wait until RCPT TO before evaluating the *_restrictions. This permits for some old/broken clients and allows Postfix to log the destination of rejected requests. In addition, disable the ETRN and VRFY commands, require clients to follow the RFC, and don't show details of the user lookup table in error messages to connecting MTAs/clients.

smtpd_delay_reject = yes
smtpd_etrn_restrictions = reject
fast_flush_domains =
disable_vrfy_command = yes
strict_rfc821_envelopes = yes
show_user_unknown_table_name = no

This rejects remote MTAs whose IP address does not have valid DNS records. This requires valid ip1->name, name->ip2, and ip1==ip2. If a remote MTA doesn't have all this, the sysadmin isn't doing a good job and the mail should be rejected anyway. Note: This is overridden for AUTH'ed clients on the submission port in master.cf below.

smtpd_client_restrictions = reject_unknown_client_hostname

Require all clients to HELO/EHLO (if not required, they can bypass the entire smtpd_helo_restrictions setting). Additionally, reject malformed HELO hostnames and non-fully qualified hostnames. The reject_unknown_helo_hostname option blocks too many legitimate emails, so I do not include it here.

smtpd_helo_required = yes
smtpd_helo_restrictions = reject_invalid_helo_hostname reject_non_fqdn_helo_hostname

This restricts the MAIL FROM command. Reject email from domains that are not fully-qualified and do not have valid DNS A or MX records. Also, create a map, controlled_auth_senders for use in master.cf below.

smtpd_sender_restrictions = reject_non_fqdn_sender reject_unknown_sender_domain
smtpd_sender_login_maps = hash:/etc/postfix/controlled_auth_senders

File: /etc/postfix/controlled_auth_senders
# MAIL FROM        SASL AUTH User
bob@example.com    bob
alice@example.com  alice

Reject mail to domains that are not fully-qualified or do not have valid DNS A or MX records.

smtpd_recipient_restrictions = reject_non_fqdn_recipient reject_unknown_recipient_domain

Who can send outgoing mail to other domains? Anyone in $mynetworks (typically localhost for cli mailers like mailx) and anyone AUTH'ed (submission port). Reject any mail of which Postfix is not the final destination.

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated reject_unauth_destination

Pipelining is sending commands without waiting for a response for each one. Don't allow remote MTAs/clients to pipeline without saying they're going to do it first.

smtpd_data_restrictions = reject_unauth_pipelining

Master.cf

Typically located at /etc/postfix/master.cf, the master.cf file defines what services Postfix will run and what ports those services are going to listen on. Clients (like Claws-Mail or K-9Mail) should never use port 25. Port 25 is only for MTAs to communicate with each other. By default, Postfix only listens on port 25, so lets open up a submission service (port 587) for clients to use.

Name the submission port differently for logging. Enable AUTH here. If you enable AUTH in main.cf instead of master.cf, AUTH will be available on port 25 as well, which is unnecessary and bad practice. Only permit valid AUTH'ed users to relay mail here, no one else. Lastly, remove the reject_unknown_client_hostname restriction from smtpd_client_restrictions (we set this above in main.cf) because cell phones and/or laptops may not have valid DNS records for their hostname. But if they AUTH, it doesn't matter anyway.

submission inet n - - - - smtpd
-o syslog_name=postfix/submission
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_client_restrictions=

Add an option to the beginning of the sender restrictions we defined earlier in main.cf. If a user is AUTH'ed (submission), require them to use the same MAIL FROM address as the one they AUTH'ed with. This requires a file be created and mapped using postmap (see above).

-o smtpd_sender_restrictions=reject_authenticated_sender_login_mismatch,reject_non_fqdn_sender,reject_unknown_sender_domain

DNS

DNS records are important for a mail server. Due to the smtpd_client_restrictions above, Postfix will check for valid DNS records and reject a client if the records are not correct. Most servers [should] do this. Setting up proper DNS records will decrease the likelihood that your outgoing mail will be rejected or marked as spam.

When sending outgoing mail, your MTA (Postfix) will connect to a remote MTA and issue a HELO hostname or EHLO hostname command. The hostname will be the actual hostname of your MTA. In my case, even though mail is being sent from @guglielmo.us, the actual hostname of the server that runs Postfix is athena.guglielmo.us.

The remote server should check that the IP your MTA is connecting from resolves to the HELO/EHLO hostname. As in, 167.114.96.243 (Athena's IP) resolves to athena.guglielmo.us (and vice versa, of course). This applies to IPv4 and IPv6. You can confirm this by using the host command.

$ host guglielmo.us
guglielmo.us has address 167.114.96.243
guglielmo.us has IPv6 address 2607:5300:100:200::fce
guglielmo.us mail is handled by 10 athena.guglielmo.us.

$ host athena.guglielmo.us
athena.guglielmo.us has address 167.114.96.243
athena.guglielmo.us has IPv6 address 2607:5300:100:200::fce

$ host 167.114.96.243
243.96.114.167.in-addr.arpa domain name pointer athena.guglielmo.us.

$ host 2607:5300:100:200::fce
e.c.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.0.1.0.0.0.3.5.7.0.6.2.ip6.arpa domain name pointer athena.guglielmo.us.

SPF

Sender Policy Framework is a technique that uses DNS records to reduce email spoofing. There is two steps here. First, you must add SPF to your domain so remote MTAs can validate your outgoing mail (if configured to do so). Then, you have to configure Postfix to check SPF on incoming mail.

As you can see, there is a TXT record on my domain that holds the information. This says that outgoing mail will only be sent from the IPv4 and IPv6 address listed. Read about the SPF syntax and testing tools.

$ host -t TXT guglielmo.us
guglielmo.us descriptive text "v=spf1 ip4:167.114.96.243 ip6:2607:5300:100:200::fce -all"

To configure Postfix to check SPF on incoming mail, I use the postfix-policyd-spf-python Debian package. After installing it, you must configure Postfix to use it. In master.cf, add the following.

policyd-spf unix - n n - 0 spawn
user=policyd-spf argv=/usr/bin/policyd-spf /etc/postfix-policyd-spf-python/policyd-spf.conf

In main.cf, change/add the following:

smtpd_recipient_restrictions = reject_non_fqdn_recipient reject_unknown_recipient_domain check_policy_service unix:private/policyd-spf
policyd-spf_time_limit = 3600

To prevent checking of SPF records for an AUTH'ed client's IP (laptop, cell phone, etc) on outgoing mail through the submission port, add the following to your master.cf. The check_policy_service is removed here.

-o smtpd_recipient_restrictions=reject_non_fqdn_recipient,reject_unknown_recipient_domain

The configuration for policyd-spf is located in /etc/postfix-policyd-spf-python/policyd-spf.conf. More information can be found with man 1 policyd-spf and in /usr/share/doc/postfix-policyd-spf-python. The configuration defaults are sane, however you should read the man pages anyway. Review your mail log to confirm things are working the way you want.

When mail is rejected due to a SPF failure, you'll see a message in your maillog similar to this: Recipient address rejected: Message rejected due to: SPF fail - not authorized.

DKIM

DKIM, or DomainKeys Identified Mail is a method of preventing forged sender addresses and modification of email in transit. It uses cryptographic signatures and a DNS record to do this. I used OpenDKIM to implement this in Postfix. OpenDKIM will sign outgoing mail and verify signatures on incoming mail.

Install OpenDKIM, create a config directory, and generate a 2048-bit key pair. Replace guglielmo.us with your own domain, of course.

sudo apt-get install opendkim opendkim-tools
sudo mkdir -m 700 /etc/opendkim
sudo opendkim-genkey -b 2048 -d guglielmo.us -D /etc/opendkim/ -r -s mail -v

You will now have two files in /etc/opendkim/, mail.private and mail.txt. The former is the private key and the latter contains a DNS zone file with the public key. You must add a TXT record as defined in the zone file.

Next, edit OpenDKIM's configuration file, /etc/opendkim.conf to contain the following.

# Always add a "Authentication-Results:" header to incoming mail.
AlwaysAddARHeader T

# The mail may be modified by the MTA at some point, possibly
# invalidating the signature. "Relaxed" allows whitespace changes,
# "simple" allows no changes. In the format "header/body".
Canonicalization relaxed/relaxed

DisableADSP T # ADSP is not recommended by the IETF anymore.

Domain guglielmo.us # Sign mail from this domain (your domain)

# Use this private key to sign outgoing mail
KeyFile /etc/opendkim/mail.private

Selector mail # The DNS selector to use

# Postfix is chroot'ed to /var/spool/postfix/
# You must create /var/spool/postfix/opendkim and set the permissions
# so that the postfix user/group) can write to it.
Socket local:/var/spool/postfix/opendkim/opendkim.sock

Syslog yes # Log to syslog

# Required to use a local socket with MTAs that access the socket
# as an unprivileged user (e.g. Postfix)
UMask 000

UserID opendkim:postfix

# Sign using the actual FROM plus a null FROM to prevent the addition
# of malicious header fields between the signer and the verifier.
OversignHeaders From

Finally, add the following lines in /etc/postfix/main.cf to tell Postfix to use OpenDKIM, then restart both daemons.

milter_default_action = accept
smtpd_milters = unix:/opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters

You can use Port25's autoresponder to confirm both SPF and DKIM are working properly. To do this, send an empty email to check-auth@verifier.port25.com.

Sieve Filtering

This assumes you have Dovecot up-and-running already. Installing Dovecot is beyond the scope of this document.

Mail can be filtered into folders using a simple programming language called Sieve. Sieve is designed specifically for filtering mail and is limited to prevent exploitation. After accepting the mail, Postfix delivers it to Dovecot using LMTP. Dovecot then does the actual final delivery to the user's homedir, applying the sieve filtering as well.

Install support for sieve and LMTP in Dovecot.

apt-get install dovecot-sieve dovecot-lmtpd

NOTE: Debian automatically enables installed protocols in /usr/share/dovecot/protocols.d/. Other distributions/OSs may not.

Configure Dovecot to listen on a unix socket for LMTP connections from Postfix in /etc/dovecot/conf.d/10-master.conf.

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

Tell Dovecot where we want mail delivered to in /etc/dovecot/conf.d/10-mail.conf. I prefer the Maildir format with delivery to ~/Maildir.

mail_location = maildir:~/Maildir

Tell Dovecot that we want to enable the sieve plugin on lmtp connections (/etc/dovecot/conf.d/20-lmtp.conf):

protocol lmtp {
  mail_plugins = sieve
}

Configure Dovecot to use the user part of the user@domain.tld for the user/pass lookups in PAM (/etc/dovecot/conf.d/10-auth.conf).

auth_username_format = %Ln

Finally, configure Postfix to use Dovecot (via lmtp) for final delivery (/etc/postfix/main.cf).

mailbox_transport = lmtp:unix:private/dovecot-lmtp

There are options that can be configured for sieve in /etc/dovecot/conf.d/90-sieve.conf. Sieve scripts are placed in ~/.dovecot.sieve by default. Below is an example sieve script that filters Postfix's smtp errors into a "System" imap folder.

require [
  "fileinto", # Put mail in dirs
  "mailbox"]; # Create dirs

if allof (
  address :matches :all "from" "MAILER-DAEMON@athena.guglielmo.us",
  header :matches "subject" "Postfix SMTP server: errors from *") {
    fileinto :create "System";
}

See the sieve info website for more information, including script examples and tutorials.