The Postfix Mail Server

(August 2019)

These notes cover Postfix, and using it as a mail forwarding and filtering smarthost.

Postfix is an MTA designed to replace Sendmail. Sendmail uses a monolithic architecture. Postfix breaks its functionality into modular, semi-resident, mutually-cooperative processes that each do specific tasks.

The official Postfix documentation is generally good.

Postfix has two primary configuration files: sets options for the various Postfix components using key = value directives, like:

mydomain =
myorigin = $mydomain
mydestination = $myhostname localhost.$mydomain localhost
mynetworks =
relay_domains = $mydomain
relayhost =
proxy_interfaces =

Each line in defines how a client program connects to a service, and what program the Postfix master daemon runs when the service is requested. Most of the programs controlled by the master daemon are short-lived, and exit after serving max_use clients or after max_idle periods of inactivity.

Daemons specified in must speak the Postfix-internal protocol. Non-Postfix software that does not must use the local(8), pipe(8) or spawn(8) services.

Run postfix reload after changing

Postfix uses database files (SQL, LDAP, Berkeley DB) for access control, address rewriting and other purposes. Postfix also calls these “lookup tables”. postconf -m shows what types of databases the installed Postfix understands.

Postfix mail flow diagram

How Postfix receives mail

Network → smtpd     rewrite
                ↘     ↓ ↑
Network → qmqpd  → cleanup → incoming
              pickup ← maildrop
   Local → sendmail → postdrop
  1. Mail enters from outside via smtpd (or qmqpd). The smtpd removes the SMTP encapsulation, enforces a few sanity checks, and hands the sender, recipients, and message content to the cleanup server.
  2. The cleanup server adds any missing headers, applies any address rewriting rules, and possibly does light content inspection. Then, cleanup writes a message file to the incoming queue, and tells the queue manager about the new message.

(The trivial-rewrite server rewrites addresses to the standard “user@fully.qualified.domain” form. See

How Postfix delivers mail

                              +---→ local --→ file, command
                  trivial-    |
                  rewrite     |---→ lmtp ---→ network
                    ↑ ↓       |
incoming → active → qmgr ---------→ smtp ---→ network
             ↑ ↓              |
           deferred           |---→ virtual → file
                              +---→ pipe ---→ command

The incoming and deferred queues might hold a large number of messages. The active queue holds only the small number of messages that qmgr has opened for immediate handling.

  1. The queue manager moves messages from incoming to active.
  2. The queue manager has trivial-rewrite resolve the address to a destination, handle any mail transport overrides, and handle any relocated users/domains. (See
  3. The queue manager sends out the message to a destination via (for example) smtp or lmtp.

Mail Programs

The follow sections describe some of the key Postfix programs, with partial excerpts from their manual pages.


network → (smtpd) → (cleanup) → [incoming] → [active] → (qmgr) → …

cleanup(8postfix) 🔗

The cleanup(8) daemon processes inbound mail, inserts it into the incoming mail queue, and informs the queue manager of its arrival.

The cleanup(8) daemon always performs the following transformations:

The following address transformations are optional:

The cleanup(8) daemon performs sanity checks on the content of each message. When it finds a problem, by default it returns a diagnostic status to the client, and leaves it up to the client to deal with the problem. Alternatively, the client can request the cleanup(8) daemon to bounce the message back to the sender in case of trouble.


Postfix built-in content filtering is meant to stop a flood of worms or viruses. It is not a general content filter.


master(8) 🔗

The master(8) daemon is the resident process that runs Postfix daemons on demand.

The behavior of the master(8) daemon is controlled by the configuration file, as described in master(5).


postconf(1) 🔗

By default, the postconf(1) command displays the values of configuration parameters, and warns about possible mis-typed parameter names (Postfix 2.9 and later). The command can also change configuration parameter values, or display other configuration information about the Postfix mail system.

🐚 ~ # postconf -c /tmp/posfix-test-config/
🐚 ~ # postconf 1>/dev/null

Also see postconf(5) 🔗.


postfix(1) 🔗

The postfix(1) command controls the operation of the Postfix mail system: start or stop the master(8) daemon, do a health check, and other maintenance.

🐚 ~ # postfix stop
🐚 ~ # postfix start
🐚 ~ # postfix status
🐚 ~ # postfix reload
🐚 ~ # postfix flush
🐚 ~ # postfix check
🐚 ~ # postfix logrotate


postscreen(8) 🔗

The Postfix postscreen(8) server provides additional protection against mail server overload. One postscreen(8) process handles multiple inbound SMTP connections, and decides which clients may talk to a Postfix SMTP server process. By keeping spambots away, postscreen(8) leaves more SMTP server processes available for legitimate clients, and delays the onset of server overload conditions.

This program should not be used on SMTP ports that receive mail from end-user clients (MUAs). In a typical deployment, postscreen(8) handles the MX service on TCP port 25, and smtpd(8) receives mail from MUAs on the submission service (TCP port 587) which requires client authentication. Alternatively, a site could set up a dedicated, non-postscreen, “port 25” server that provides submission service and client authentication, but no MX service.

postscreen(8) maintains a temporary whitelist for clients that have passed a number of tests. When an SMTP client IP address is whitelisted, postscreen(8) hands off the connection immediately to a Postfix SMTP server process. This minimizes the overhead for legitimate mail.

By default, postscreen(8) logs statistics and hands off each connection to a Postfix SMTP server process, while excluding clients in mynetworks from all tests (primarily, to avoid problems with non-standard SMTP implementations in network appliances). This default mode blocks no clients, and is useful for non-destructive testing.

In a typical production setting, postscreen(8) is configured to reject mail from clients that fail one or more tests. postscreen(8) logs rejected mail with the client address, helo, sender and recipient information.

Changes to are not picked up automatically, as postscreen(8) processes may run for several hours. Use the command “postfix reload” after a configuration change.


… → [active] → (qmgr) → (smtp) → {network}

qmgr(8) 🔗

The qmgr(8) daemon awaits the arrival of incoming mail and arranges for its delivery via Postfix delivery processes. The actual mail routing strategy is delegated to the trivial-rewrite(8) daemon. This program expects to be run from the master(8) process manager.

The qmgr(8) daemon maintains the following queues:

Changes to are not picked up automatically as qmgr(8) is a persistent process. Use the “postfix reload” command after a configuration change.


{local} → (sendmail) → [maildrop] → (pickup) → (cleanup) → …

sendmail(1) 🔗

The Postfix sendmail(1) command implements the Postfix to Sendmail compatibility interface.

By default, Postfix sendmail(1) reads a message from standard input until EOF or until it reads a line with only a . character, and arranges for delivery. Postfix sendmail(1) relies on the postdrop(1) command to create a queue file in the maildrop directory.

Specific command aliases are provided for other common modes of operation:

mailq List the mail queue. Each entry shows the queue file ID, message size, arrival time, sender, and the recipients that still need to be delivered. If mail could not be delivered upon the last attempt, the reason for failure is shown.

newaliases Initialize the alias database.

For reading and sending mail from the command line, mail(1) (from either the mailutils or bsd-mailx package) is handier.


… → (qmgr) → (smtp) → {network}

smtp(8) 🔗

The Postfix SMTP+LMTP client implements the SMTP and LMTP mail delivery protocols. It processes message delivery requests from the queue manager. Each request specifies a queue file, a sender address, a domain or host to deliver to, and recipient information. This program expects to be run from the master(8) process manager.

The SMTP+LMTP client updates the queue file and marks recipients as finished, or it informs the queue manager that delivery should be tried again at a later time.

The SMTP+LMTP client looks up a list of mail exchanger addresses for the destination host, sorts the list by preference, and connects to each listed address until it finds a server that responds.

Most smtp_xxx configuration parameters have an lmtp_xxx “mirror” parameter for the equivalent LMTP feature. This [man page] describes only those LMTP-related parameters that aren’t simply “mirror” parameters. See postconf(5) for more details including examples.

Changes to are picked up automatically, as smtp(8) processes run for only a limited amount of time. Use the command “postfix reload” to speed up a change.


{network} → (smtpd) → (cleanup) → incoming → …

smtpd(8) 🔗

The SMTP server accepts network connection requests and performs zero or more SMTP transactions per connection. Each received message is piped through the cleanup(8) daemon, and is placed into the incoming queue as one single queue file. For this mode of operation, the program expects to be run from the master(8) process manager.

The SMTP server implements a variety of policies for connection requests, and for parameters given to HELO, ETRN, MAIL FROM, VRFY and RCPT TO commands. They are detailed below and in the configuration file.

Changes to are picked up automatically, as smtpd(8) processes run for only a limited amount of time. Use the command “postfix reload” to speed up a change.

smtpd has a large number of configuration options described in the manual and detailed in postconf(5).


(smtpd) ⭤ (tlsmgr)

(smtp) ⭤ (tlsmgr)

tlsmgr(8) 🔗

The tlsproxy(8) server implements a two-way TLS proxy. It is used by the postscreen(8) server to talk SMTP-over-TLS with remote SMTP clients that are not whitelisted (including clients whose whitelist status has expired), and by the smtp(8) client to support TLS connection reuse, but it should also work for non-SMTP protocols.

After receiving a valid remote SMTP client STARTTLS command, the postscreen(8) server sends the remote SMTP client endpoint string, the requested role (server), and the requested timeout to tlsproxy(8). postscreen(8) then receives a “TLS available” indication from tlsproxy(8). If the TLS service is available, postscreen(8) sends the remote SMTP client file descriptor to tlsproxy(8), and sends the plain-text 220 greeting to the remote SMTP client. This triggers TLS negotiations between the remote SMTP client and tlsproxy(8). Upon completion of the TLS-level handshake, tlsproxy(8) translates between plaintext from/to postscreen(8) and ciphertext to/from the remote SMTP client.

network → smtpd ⭤ tlsmgr ⭤ smtp → network
           /        |        \
  smtpd session    PRNG    smtp session
    key cache   state file   key cache


smtpd_tls_cert_file = /etc/postfix/server.pem
smtpd_tls_key_file = $smtpd_tls_cert_file
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1

Enable tlsmgr in /etc/postfix/ It’s enabled by default in the Debian package.

tlsmgr    unix  -       -       y       1000?   1       tlsmg


(cleanup) ↔ (trivial-rewrite)

(qmgr) ↔ (trivial-rewrite)

trivial-rewrite(8) 🔗

Postfix address rewriting and resolving daemon

The trivial-rewrite(8) daemon processes three types of client service requests:

The trivial-rewrite(8) servers run under control by the Postfix master server.

On busy mail systems a long time may pass before a change affecting trivial-rewrite(8) is picked up. Use the command “postfix reload” to speed up a change.

Mail Queues

The follow sections describe some of the key Postfix queues, with partial excerpts from the qmgr(8) manual page.

For queue management, see:

The -p flag of postqueue implements the traditional mailq function of printing the queue contents.

🐚 ~ $ /usr/sbin/postqueue -p
Mail queue is empty


… → (cleanup) → [incoming] → [active] → (qmgr) → …

qmgr(8) 🔗

The “active” queue holds mail the queue manager has open for delivery.


… → [active] ↔ [deferred]

qmgr(8) 🔗

The “deferred” queue holds mail that couldn’t be delivered on the latest try. The queue manager implements exponential backoff, doubling the time between delivery attempts.


… → [active] → [hold]

Messages relegated to the “hold” queue stay there until manually freed.


… (smtpd) → (cleanup) → [incoming] → [active] → …

qmgr(8) 🔗

The “incoming” queue hosts mail inbound from the network (or picked up ty the local pickup(8) daemon).

Lookup tables

The follow sections describe some of the key Postfix databases, with partial excerpts from their manual pages.


[access] → (smtpd) → (cleanup) → …

access(5) 🔗

This document describes access control on remote SMTP client information: host names, network addresses, and envelope sender or recipient addresses; it is implemented by the Postfix SMTP server. See header_checks(5) or body_checks(5) for access control on the content of email messages.

Normally, the access(5) table is specified as a text file that serves as input to the postmap(1) command. The result, an indexed file in dbm or db format, is used for fast searching by the mail system. Execute the command “postmap /etc/postfix/access” to rebuild an indexed file after changing the corresponding text file.


The input format for the postmap(1) command is as follows:

pattern action: When pattern matches a mail address, domain or host address, perform the corresponding action.




Execute the command “postmap /etc/postfix/access” after editing the file.


[canonical] → (cleanup) → [incoming] → …

canonical(5) 🔗

The optional canonical(5) table specifies an address mapping for local and non-local addresses. The mapping is used by the cleanup(8) daemon, before mail is stored into the queue. The address mapping is recursive.

Normally, the canonical(5) table is specified as a text file that serves as input to the postmap(1) command. The result, an indexed file in dbm or db format, is used for fast searching by the mail system. Execute the command “postmap /etc/postfix/canonical” to rebuild an indexed file after changing the corresponding text file.

By default the canonical(5) mapping affects both message header addresses (i.e. addresses that appear inside messages) and message envelope addresses (for example, the addresses that are used in SMTP protocol commands). This is controlled with the canonical_classes parameter.


[relocated] → (qmgr) → …

relocted(5) 🔗

The optional relocated(5) table provides the information that is used in “user has moved to new_location” bounce messages.

Normally, the relocated(5) table is specified as a text file that serves as input to the postmap(1) command. The result, an indexed file in dbm or db format, is used for fast searching by the mail system. Execute the command “postmap /etc/postfix/relocated” to rebuild an indexed file after changing the corresponding relocated table.

An entry has one of the following form:

pattern new_location

Where new_location specifies contact information such as an email address, or perhaps a street address or telephone number.


[virtual] → (cleanup) → [incoming] → …

virtual(5) 🔗

The optional virtual(5) alias table rewrites recipient addresses for all local, all virtual, and all remote mail destinations. This is unlike the aliases(5) table which is used only for local(8) delivery. Virtual aliasing is recursive, and is implemented by the Postfix cleanup(8) daemon be‐ fore mail is queued.

The main applications of virtual aliasing are:

Virtual alias domains are not to be confused with the virtual mailbox domains that are implemented with the Postfix virtual(8) mail delivery agent. With virtual mailbox domains, each recipient address can have its own mailbox.

Virtual aliasing is applied only to recipient envelope addresses, and does not affect message headers. Use canonical(5) mapping to rewrite header and envelope addresses in general.

Normally, the virtual(5) alias table is specified as a text file that serves as input to the postmap(1) command. The result, an indexed file in dbm or db format, is used for fast searching by the mail system. Execute the command “postmap /etc/postfix/virtual” to rebuild an indexed file after changing the corresponding text file.


How do we force Postfix to re-read a newly-edited lookup table?

See maintaining Postfix lookup table files.

In short, for regexp, pcre, cidr or texthash files, there’s little need to do postfix reload for files used by short-running processes like smtpd, cleanup, or local. For files used by longer-running processes, like trivial-rewrite, do postfix reload.

For a local file-based database, like DBM or Berkeley DB, there’s no need to do “postfix reload”. The Postfix daemon will notice changed files, and terminate before handling the next client request, so as to start with the new database.

How do we update Berkely DB files safely?

Because of the aggressive caching used by Berkely DB files, going something like postmap access might possibly break Postfix. Instead, move the updated database file into place, like postmap && mv access.db.

See updating Berkeley DB files safely.

Debugging Postfix from inside

Produce two types of mail delivery reports: “what if” and “what happened”.

What if? Report what would happen, but don’t actually deliver mail:

% /usr/sbin/sendmail -bv address...
Mail Delivery Status Report will be mailed to <your login name>.

What happened? Deliver mail and report successes or failures, including replies from remote SMTP servers:

% /usr/sbin/sendmail -v address...
Mail Delivery Status Report will be mailed to <your login name>.

To turn on verbose logging for a particular remote peer:

🐚 ~ $ sudo postconf -e 'debug_peer_list ='
🐚 ~ $ sudo postfix reload

How do we reduce spam?

In /etc/postfix/, comment the old smtp … smptd line, and uncomment the smtp … postscreen and smtpd pass … lines:

#smtp      inet  n       -       y       -       -       smtpd
smtp      inet  n       -       y       -       1       postscreen
smtpd     pass  -       -       y       -       -       smtpd
tlsproxy  unix  -       -       y       -       0       tlsproxy

In /etc/postfix/, add:

postscreen_access_list = permit_mynetworks,
postscreen_dnsbl_action = enforce


🐚 ~ $ sudo touch /etc/postfix/postscreen_access.cidr
🐚 ~ $ sudo postfix reload

Enable RBL support:

postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites =*2

(Note that requires registration.)

In /etc/postfix/, uncomment this line:

dnsblog   unix  -       -       y       -       0       dnsblog

How do we log mail To/From/Subject?

🐚 ~ $ sudo vi /etc/postfix/header_checks
/^Subject:/      WARN
🐚 ~ $ sudo vi /etc/postfix/
header_checks = regexp:/etc/postfix/header_checks
🐚 ~ $ sudo postfix reload

(According to header_checks(5), by default, Postfix treats regexp: and pcre: patterns as case insensitive.)

How can Postfix block certain attachment types?

🐚 ~ $ sudo vi /etc/postfix/
header_checks = pcre:/etc/postfix/header_checks.pcre
🐚 ~ $ sudo vi /etc/postfix/header_checks.pcre
	REJECT ".$4" attachments, like "$2" are blocked.
🐚 ~ $ sudo postfix reload

What’s LMTP?

Local Mail Transfer Protocol is a restricted, SMTP-like protocol, designed to be used to talk to daemons that don’t implement mail queues.

Basically: let the MTA queue manager handle the queues, and don’t require mail delivery agents to also manage queues.

Postfix acts as an LMTP client. Postfix can speak to amavisd, for example, via LMTP.

The design of the SMTP protocol effectively requires the server to manage a mail delivery queue. This is because a single mail transaction may specify multiple recipients and the final “.” of the DATA command may return only one reply code, to indicate the status of the entire transaction. If, for example, a server is given a transaction for two recipients, delivery to the first succeeds, and delivery to the second encounters a temporary failure condition, there is no mechanism to inform the client of the situation. The server must queue the message and later attempt to deliver it to the second recipient.

This queuing requirement is beneficial in the situation for which SMTP was originally designed: store-and-forward relay of mail between networked hosts. In some limited situations, it is desirable to have a server which does not manage a queue, instead relying on the client to perform queue management.


It would be desirable to use SMTP over a local inter-process communication channel to transfer messages from the queue manager to the delivery agents. It would, however, significantly increase the complexity of the delivery agents to require them to manage their own mail queues.

The common practice of invoking a delivery agent with the envelope address(es) as command-line arguments, then having the delivery agent communicate status with an exit code has three serious problems: the agent can only return one exit code to be applied to all recipients, it is difficult to extend the interface to deal with ESMTP extensions such as DSN [DSN] and ENHANCEDSTATUSCODES [ENHANCEDSTATUSCODES], and exits performed by system libraries due to temporary conditions frequently get interpreted as permanent errors.

The LMTP protocol causes the server to return, after the final “.” of the DATA command, one reply for each recipient. Therefore, if the queue manager is configured to use LMTP instead of SMTP when transferring messages to the delivery agents, then the delivery agents may attempt delivery to each recipient after the final “.” and individually report the status for each recipient. Connections which should use the LMTP protocol are drawn in the diagram above using asterisks.

Note that it is not beneficial to use the LMTP protocol when transferring messages to the queue manager, either from the network or from a delivery agent. The queue manager does implement a mail queue, so it may store the message and take responsibility for later delivering it.


The LMTP protocol SHOULD NOT be used over wide area networks.

What’s Milter?

Milter is a portmanteau of “mail filter”. “Milter” is an API developed by the Sendmail MTA that lets external filters, such as anti-spam or anti-virus programs, evaluate mail at various stages of mail processing. “A milter” is such a filter program.

Postfix can apply milters before-queue. Specify milters with the smtpd_milters config parameter.

           SMTP-only   non-SMTP
            filters     filters
             ^  |        ^  |
             |  v        |  v
Network  ->  smtpd  ->  cleanup  ->  incoming

What’s the deal with SPF?

A receiving mail server checks a DNS SPF record to verify that the sending mail server’s IP address is allowed to send mail for that domain. This only applies to the Sender listed in the message envelope, not the From domain in the body headers.

Create TXT DNS records, like:

@                   IN  TXT  "v=spf1 mx -all"   IN  TXT  "v=spf1 a -all"  IN  TXT  "v=spf1 a -all"

Enable SPF checking for Postfix:

🐚 ~ $ sudo apt install postfix-policyd-spf-python
🐚 ~ $ sudo vi /etc/postfix/
policy-spf_time_limit = 3600s
smtpd_recipient_restrictions = permit_mynetworks
                               check_policy_service unix:private/policy-spf
🐚 ~ $ sudo vim /etc/postfix/
policy-spf unix  -       n       n       -       -       spawn
    user=nobody argv=/usr/bin/policyd-spf
🐚 ~ $ sudo postfix reload

N.B. — put the policy services for smtpd_recipient_restrictions after reject_unauth_destination to prevent unexpected responses from the policy service from making the system an open relay. Also, put policy services after permit_mynetworks to avoid SPF checks on outbound mail from local users.

What’s the deal with DKIM?

DKIM uses a public/private key pair to cryptographically sign some or all of a message body, typically including the From header. Receiving mail servers can check the signature to verify From headers originated at that domain. (This is not absolutely fool-proof; e.g., some spammers have been known to resend signed message headers, but append bad contents to the body.)

Use Amavisd for outbound message signing and inbound verification.

Generate a key pair (or install your existing one):

🐚 ~ $ sudo mkdir -p /etc/amavis/dkim
🐚 ~ $ sudo chmod 700 /etc/amavis/dkim
🐚 ~ $ sudo openssl genrsa -out /etc/amavis/dkim/ 2048
🐚 ~ $ sudo openssl rsa -in /etc/ssl/amavis/dkim/dkim.key -pubout -out /etc/amavis/dkim/
🐚 ~ $ sudo chown -R amavis:amavis /etc/amavis/dkim

As with SPF, we publish a TXT DNS record containing the public half of our DKIM key. opendkim-genkey outputs two files:

selector201909._domainkey       IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; "
          "gx6VZe/pqUo7QTivRaqvLKGdPVrsAa4A+XHbGqamcWPwsd/HfL5MCaqtVFc75oIsbazqbDlj2TN8Cy9Cjlf47Q5+rMt4ZfaH747pWCMmk/ng9KLA365Z5fxWn2bjCDPAh88WLvUwIDAQAB" )  ;

Note that TXT records are limited to 255 characters in a single string, so long keys must be split for some DNS server. Route 53 requires a split value, while Windows DNS requires a whole, unsplit value.

After adding a record for the public key, check it like:

🐚 ~ $ dig +short TXT
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqCxxZduA47anRLt+SLDiNFK6Vh4OQhyfp/OvHqVQioE27T1cO723Jjth0sevmk6ufIFQ2wYBk5uzfr1FmvI5UUVWcDd/yT47xOKW9Y852BWFpkIIovY4dvb3Px7VKSZhijyjifEC1ewQb" "SM12QDeL854scXyd48EW803Njsn98vvPi1uOKMMOPbfIrAzD7a9vN/4QNdF/X9g5AUaytjsXx+p/V1jTgcySJbJ72SVwhTaL2bKISUZ8Uj7oayfel/i1PkNhsFHoSczujt+pyxbi80mjRWj6yX6jzPTKzG8KjcvSaofNoUFeHyl6JplyvG48Z+gIQm6STSJsokr9UA+fQIDAQ8G"

Add to `/etc/amavis/conf.d/50-user`:

$enable_dkim_verification = 1; # enable DKIM signatures verification $enable_dkim_signing = 1; # load DKIM signing code, keys defined by dkim_key dkim_key( ‘’, ‘selector201909’, ‘/etc/amavis/dkim/’ ); @dkim_signature_options_bysender_maps = ( { “” => { d => ‘’, a => ‘rsa-sha256’, ttl => 10*24*3600 } } );

--------<!-- Maybe no good DKIM config below -->

Use OpenDKIM as a Postfix milter for outbound message signing and inbound verification.

Generate a key pair (or install your existing one):

🐚 ~ $ opendkim-genkey -s myselector -d -b 2048 🐚 ~ $ sudo mkdir -p /etc/dkimkeys 🐚 ~ $ sudo chmod 700 /etc/dkimkeys 🐚 ~ $ sudo mv ./myselector.private /etc/dkimkeys/ 🐚 ~ $ sudo mv ./myselector.txt /etc/dkimkeys/ 🐚 ~ $ sudo ln -s /etc/dkimkeys/myselector.private /etc/dkimkeys/dkim.key 🐚 ~ $ sudo chown opendkim:opendkim /etc/dkimkeys/* 🐚 ~ $ sudo chmod 600 /etc/dkimkeys/*

Alternatively, generate the key pair using `openssl`:

🐚 ~ $ sudo openssl genrsa -out /etc/ssl/dkimkeys/dkim.key 2048 🐚 ~ $ sudo openssl rsa -in /etc/ssl/dkimkeys/dkim.key -pubout -out /etc/dkimkeys/

As with SPF, we publish a TXT DNS record containing the public half of our DKIM key.
`opendkim-genkey` outputs two files:

- `myselector.private` contains the private key
- `myselector.txt` contains a DNS record with the public key, like:

myselector._domainkey IN TXT ( “v=DKIM1; h=sha256; k=rsa; ” “p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArTgyBL9cTXnJjnpDq9ZrjAixiq7YQoR/XE83X29jbD/QxJjcxX8nYqmKmxua3TbtamKYhhaVGPiUt+Hfj6tR5hV7FXYlt89IBSDJMTuFSxMIFiPFG9V+rCv6qjcLUWDo8T8I8P6KFHYTGoZFJ6FPgeqMJVEJGut3mnweJGG2Fhc609tupuNeEbRF0UUafpRBksweFh6bNNWlGZ” “gx6VZe/pqUo7QTivRaqvLKGdPVrsAa4A+XHbGqamcWPwsd/HfL5MCaqtVFc75oIsbazqbDlj2TN8Cy9Cjlf47Q5+rMt4ZfaH747pWCMmk/ng9KLA365Z5fxWn2bjCDPAh88WLvUwIDAQAB” ) ; —– DKIM key myselector for

Note that TXT records are limited to 255 characters in a single string, so long keys must be split for some DNS server.
Route 53 requires a split value, while Windows DNS requires a whole, unsplit value.

After adding a record for the public key, check it like:

🐚 ~ $ dig +short TXT “v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqCxxZduA47anRLt+SLDiNFK6Vh4OQhyfp/OvHqVQioE27T1cO723Jjth0sevmk6ufIFQ2wYBk5uzfr1FmvI5UUVWcDd/yT47xOKW9Y852BWFpkIIovY4dvb3Px7VKSZhijyjifEC1ewQb” “SM12QDeL854scXyd48EW803Njsn98vvPi1uOKMMOPbfIrAzD7a9vN/4QNdF/X9g5AUaytjsXx+p/V1jTgcySJbJ72SVwhTaL2bKISUZ8Uj7oayfel/i1PkNhsFHoSczujt+pyxbi80mjRWj6yX6jzPTKzG8KjcvSaofNoUFeHyl6JplyvG48Z+gIQm6STSJsokr9UA+fQIDAQ8G”

Add to /etc/postfix/

non_smtpd_milters = unix:/var/run/opendkim/opendkim.sock

That takes care of signing outbound messages. For verifying inbound messages…

How do we fix logging of this warning: “dict_nis_init: NIS domain name not set - NIS lookups disabled”

Add to /etc/postfix/

alias_maps = hash:/etc/aliases

Test a new mail server by relaying a subdomain?

If our normal domain is, we can add a subdomain to test a new mail server.

relay_domains =
canonical_classes = envelope_recipient, header_recipient
canonical_maps = regexp:/etc/postfix/canonical

The map file /etc/postfix/canonical looks like:

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


Mailgraph is an RRDtool-based CGI that produces periodic graphs of mail activity.


🐚 ~ $ sudo cp /usr/share/doc/mailgraph/examples/mailgraph.conf /etc/apache2/conf-available/
🐚 ~ $ sudo a2enmod cgi
🐚 ~ $ sudo a2enconf mailgraph
🐚 ~ $ sudo systemctl reload apache2



Usually, SMTP servers accept mail to remote destinations when the client’s IP address is in the “same network” as the server’s IP address.

SMTP clients outside the SMTP server’s network need a different way to get “same network” privileges. To address this need, Postfix supports SASL authentication (RFC 4954, formerly RFC 2554).

Postfix lacks its own full SASL implementation. Dovecot SASL may be used.

Dovecot is a POP/IMAP server that has its own configuration to authenticate POP/IMAP clients. When the Postfix SMTP server uses Dovecot SASL, it reuses parts of this configuration. Consult the Dovecot documentation for how to configure and operate the Dovecot authentication server.


🐚 ~ $ cat /etc/amavis/conf.d/50-user
use strict;

# Place your configuration directives here.  They will override those in
# earlier files.
# See /usr/share/doc/amavisd-new/ for documentation and examples of
# the directives you can use in this file

@bypass_virus_checks_maps = (
        \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
@bypass_spam_checks_maps = (
        \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);

#@mynetworks = qw( [FEC0::]/10 [fd80:c96e:7a06::]/64 );
$log_level = 1;              # verbosity 0..5, -d
$log_recip_templ = $log_verbose_templ;    # disable by-recipient level-0 log entries
$nanny_details_level = 2;    # nanny verbosity: 1: traditional, 2: detailed
$enable_dkim_verification = 1;  # enable DKIM signatures verification
$enable_dkim_signing = 1;    # load DKIM signing code, keys defined by dkim_key
$sa_tag_level_deflt  = -999.0;  # add spam info headers if at, or above that level
$sa_tag2_level_deflt = 3.0;  # add 'spam detected' headers at that level
$sa_kill_level_deflt = 6.9;  # triggers spam evasive actions (e.g. blocks mail)
$sa_dsn_cutoff_level = 10;   # spam level beyond which a DSN is not sent
$sa_crediblefrom_dsn_cutoff_level = 18; # likewise, but for a likely valid From
$penpals_bonus_score = 8;    # (no effect without a @storage_sql_dsn database)
$penpals_threshold_high = $sa_kill_level_deflt;  # don't waste time on hi spam
$bounce_killer_score = 100;  # spam score points to add for joe-jobbed bounces
$sa_mail_body_size_limit = 400*1024; # don't waste time on SA if mail is larger
$sa_spam_subject_tag = '';
$defang_virus  = 1;  # MIME-wrap passed infected mail
$defang_banned = 1;  # MIME-wrap passed mail containing banned name
$defang_by_ccat{CC_BADH.",3"} = 1;  # NUL or CR character in header
$defang_by_ccat{CC_BADH.",5"} = 1;  # header line longer than 998 characters
$defang_by_ccat{CC_BADH.",6"} = 1;  # header field syntax error
push @{$score_sender_maps[0]{'.'}}, read_hash("/etc/amavis/sender_scores_sitewide");

#------------ Do not modify anything below this line -------------
1;  # ensure a defined return

Add two services to /etc/postfix/ — one for Postfix to send messages to Amavis, the other to reinject Amavis-scanned messages into Postfix:

amavisfeed unix    -       -       n        -      2     lmtp
    -o lmtp_data_done_timeout=1200
    -o lmtp_send_xforward_command=yes
    -o disable_dns_lookups=yes
    -o max_use=20 inet n    -       n       -       -     smtpd
    -o content_filter=
    -o smtpd_delay_reject=no
    -o smtpd_client_restrictions=permit_mynetworks,reject
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=reject_unauth_pipelining
    -o smtpd_end_of_data_restrictions=
    -o smtpd_restriction_classes=
    -o mynetworks=
    -o smtpd_error_sleep_time=0
    -o smtpd_soft_error_limit=1001
    -o smtpd_hard_error_limit=1000
    -o smtpd_client_connection_count_limit=0
    -o smtpd_client_connection_rate_limit=0
    -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters
    -o local_header_rewrite_clients=

Add to /etc/postfix/


Run (at least on Debian Buster):

🐚 ~ $ sudo usermod -a -G amavis clamav
🐚 ~ $ sudo systemctl restart clamav-daemon.service
🐚 ~ $ sudo systemctl restart amavis.service

Example Postfix on a Workstation

We want to send mail from a workstation to the LAN’s smarthost. Here’s a minimal config:

🐚 ~ $ sudo newaliases
🐚 ~ $ cat /etc/postfix/
compatibility_level = 2
mydomain =
myorigin = $myhostname
mydestination = $myhostname localhost.$mydomain localhost
mynetworks = [::1]/128
relay_domains =
relayhost = smarthost.$mydomain
🐚 ~ $ sudo postconf 1>/dev/null
🐚 ~ $ sudo postfix reload
postfix/postfix-script: refreshing the Postfix mail system

To receive mail, of course, an MX record must exist for

Example Postfix Server

We want a smarthost to do graylisting, spam scoring, and AV scanning, before forwarding mail to our internal mail server. Things we want:

🐚 ~ $ sudo apt install amavisd-new clamav clamav-daemon postfix postfix-doc postfix-pcre postfix-policyd-spf-python postfix-sqlite spamassassin unrar arj lhasa lzop lrzip lz4 mailgraph opendkim opendkim-tools

🐚 ~ $ sudo newaliases

We run rsyslog for logging, though Postfix offers its own logging service (see postlog(1) and postlogd(8postfix)). Our /etc/rsyslog.conf contains:

mail.*       -/var/log/mail.log    -/var/log/
mail.warn    -/var/log/mail.warn
mail.err      /var/log/mail.err

(The minus sign (“-”) disables syncing the log after each write. See rsynclog.conf.)

Remember to configure /etc/logrotate.d/rsyslog as desired.

compatibility_level = 2
mydomain =
myorigin = $myhostname
mydestination = $myhostname localhost.$mydomain localhost
mynetworks = [::1]/128 [fe80::]/10
relay_domains = $mydestination, $mydomain
#relayhost = smarthost.$mydomain
debug_peer_list = 
smtpd_tls_cert_file = /etc/ssl/private/
smtpd_tls_key_file = $smtpd_tls_cert_file
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1
alias_maps = hash:/etc/aliases
header_checks = regexp:/etc/postfix/header_checks
postscreen_access_list = permit_mynetworks,
postscreen_dnsbl_action = enforce
postscreen_dnsbl_threshold = 2
postscreen_dnsbl_sites =*2
policy-spf_time_limit = 3600s
#smtpd_helo_restrictions = reject_unknown_helo_hostname
smtpd_sender_restrictions = reject_unknown_sender_domain
smtpd_data_restrictions = reject_unauth_pipelining
smtpd_recipient_restrictions = permit_mynetworks
                               check_policy_service unix:private/policy-spf