Config File Literacy: Postfix

How to read main.cf — every key directive explained, with relay, TLS, and queue management.

Postfix config files

Postfix lives in /etc/postfix/. The key files:

/etc/postfix/
├── main.cf          # main configuration file (most settings live here)
├── master.cf        # service definitions (which daemons run, on what ports)
├── virtual          # virtual alias maps (if using virtual_alias_maps)
├── transport        # routing exceptions (if using transport_maps)
└── sasl_passwd      # relay credentials (if using smtp_sasl_password_maps)

main.cf is where you spend most of your time. master.cf defines the process model — enabling submission (port 587), smtps (port 465), or tweaking per-daemon settings are routine master.cf tasks. Leave it at defaults otherwise.

main.cf structure

Each line is a parameter = value pair. Comments start with #. Whitespace continuation (indent next line) is supported for long values. Parameter values can reference other parameters using $parameter_name.

# This is a comment
myhostname = mail.example.com
mydomain = example.com

# Reference another parameter
myorigin = $mydomain    # equivalent to myorigin = example.com

To see the current effective value of any parameter:

postconf myhostname
postconf mydestination
postconf -d relayhost   # -d shows the compiled default

Identity directives

# The hostname this machine will use to introduce itself in SMTP
myhostname = mail.example.com

# The domain appended to unqualified addresses (user → user@mydomain)
mydomain = example.com

# What domain appears in From/envelope addresses originated here
myorigin = $mydomain

# What domains this server will accept mail FOR (deliver locally)
# IMPORTANT: list only the domains you actually host
mydestination = $myhostname, localhost.$mydomain, localhost
mydestination is critical. If your server is a relay-only host (forwarding all mail to another server), set mydestination = (empty) or only include localhost. If you list your real domain here, Postfix will try to deliver mail locally instead of forwarding it.

Network directives

# Which interfaces to listen on for incoming SMTP connections
inet_interfaces = all          # all interfaces
inet_interfaces = loopback-only  # localhost only (relay-from-app server)
inet_interfaces = 10.0.0.5     # specific IP

# Which networks are trusted to relay mail through this server
# (trusted = can send mail without authentication)
mynetworks = 127.0.0.0/8, 10.0.0.0/8

# Or auto-detect from attached networks (less secure)
mynetworks_style = host         # only localhost
mynetworks_style = subnet       # all attached subnets

mynetworks determines who can use this server as a relay without authentication. Be restrictive — only list subnets you control. An open relay (0.0.0.0/0) will be blacklisted within hours of going live.

Relay configuration

# Forward all outbound mail to this smarthost instead of delivering directly
relayhost = [smtp.example.com]:587

# [] brackets mean: do not do MX lookup on this hostname
# Postfix connects directly to smtp.example.com:587
# Without brackets, Postfix would look up MX records for smtp.example.com

# Common relay formats:
relayhost = [smtp.gmail.com]:587           # Gmail relay
relayhost = [10.0.0.2]:25                  # Internal relay by IP
relayhost = smtp.example.com               # MX lookup — less common

TLS directives

TLS for incoming connections (when Postfix receives mail):

# Incoming TLS (smtpd = server)
smtpd_tls_cert_file = /etc/ssl/certs/mail.example.com.crt
smtpd_tls_key_file  = /etc/ssl/private/mail.example.com.key
smtpd_tls_security_level = may   # offer TLS but don't require it
smtpd_tls_loglevel = 1           # log TLS connection info

TLS for outgoing connections (when Postfix sends mail or relays):

# Outgoing TLS (smtp = client)
smtp_tls_security_level = may    # try TLS, fall back to plaintext if unavailable
smtp_tls_security_level = encrypt  # require TLS (for relay to a specific smarthost)
smtp_tls_loglevel = 1
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt

smtpd_ prefixed directives control Postfix acting as a server (receiving mail). smtp_ prefixed directives control Postfix acting as a client (sending mail). This naming distinction is consistent throughout Postfix.

SASL authentication (relayhost with auth)

When relaying through a smarthost that requires authentication:

# Enable SASL auth for outgoing connections
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt   # require TLS for smarthost relay
# smtp_tls_security_level = may     # opportunistic TLS (falls back to plaintext)

Create the credentials file:

# /etc/postfix/sasl_passwd
# Format: [host]:port  username:password
[smtp.example.com]:587  relayuser@example.com:secretpassword
# Hash the file (Postfix uses the .db file, not the plaintext)
postmap /etc/postfix/sasl_passwd

# Secure the plaintext file (it contains a password)
chmod 600 /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd.db

# Reload Postfix
systemctl reload postfix

Local delivery and mailbox

# Local delivery: Maildir format (one file per message, subdirectories)
home_mailbox = Maildir/

# Local delivery: mbox format (single file, less safe)
mail_spool_directory = /var/spool/mail

# If this server is a relay-only host and should not deliver locally:
# Leave home_mailbox unset and set mydestination = empty

Lookup tables — maps

Postfix uses lookup tables (maps) for addresses, routes, and credentials. Most maps need to be compiled with postmap after editing.

# Virtual aliases — rewrite address before delivery
virtual_alias_maps = hash:/etc/postfix/virtual
# /etc/postfix/virtual:
# admin@example.com    alice@example.com
# info@example.com     alice@example.com, bob@example.com

# Canonical maps — rewrite sender/recipient addresses
canonical_maps = hash:/etc/postfix/canonical

# Transport maps — route specific domains differently
transport_maps = hash:/etc/postfix/transport
# /etc/postfix/transport:
# partner.com    smtp:[mail.partner.com]

# After editing any map file:
postmap /etc/postfix/virtual
systemctl reload postfix

Queue management

# View the mail queue
postqueue -p
mailq                # alias for the above

# Flush the queue (attempt immediate delivery of all queued mail)
postqueue -f

# Delete all mail from the queue (use carefully!)
postsuper -d ALL

# Delete deferred mail only
postsuper -d ALL deferred

# Show the content of a queued message
postcat -vq QUEUEID

When mail is stuck in the queue, read the reason in postqueue -p. Common reasons: connection refused (smarthost down), authentication failure (wrong credentials), DNS failure (relayhost hostname not resolving).

Testing configuration

# Syntax check main.cf
postfix check

# View effective config (all parameters, not just changed ones)
postconf -d            # defaults
postconf              # current effective values
postconf relayhost    # one parameter

# Send a test email
echo "test body" | mail -s "test subject" recipient@example.com

# Watch what happens in real time
tail -f /var/log/maillog
# or
journalctl -u postfix -f

Annotated main.cf — relay server

A typical relay server that receives mail from internal apps and forwards to a smarthost:

# /etc/postfix/main.cf — internal relay server

# --- Identity ---
myhostname = relay01.example.com
mydomain = example.com
myorigin = $mydomain

# Do NOT deliver locally — this server is relay-only
mydestination =
local_recipient_maps =
local_transport = error:local delivery disabled

# --- Network ---
# Accept connections only from internal networks
inet_interfaces = all
mynetworks = 127.0.0.0/8, 10.0.0.0/8

# --- Relay ---
# Forward all mail to the corporate mail server
relayhost = [mail.example.com]:25

# --- Outgoing TLS ---
smtp_tls_security_level = may
smtp_tls_loglevel = 1

# --- Queue ---
# Retry more aggressively for transient failures
maximal_queue_lifetime = 1d     # give up after 1 day (default 5d)
bounce_queue_lifetime = 1d

# --- Limits ---
message_size_limit = 52428800   # 50 MB max message size

smtpd_relay_restrictions

smtpd_relay_restrictions is the single most important anti-open-relay setting in Postfix. An open relay will forward mail for anyone, making your server a spam source. The default safe pattern:

# /etc/postfix/main.cf
smtpd_relay_restrictions =
    permit_mynetworks,           # allow hosts in mynetworks (your internal servers)
    permit_sasl_authenticated,   # allow clients that authenticated with SASL
    reject_unauth_destination    # reject everything else (prevents open relay)

# These three lines together mean: only relay mail from trusted networks
# or authenticated clients. All other relay attempts are rejected.
Open relay check: After any change to relay restrictions, test with an external tool: telnet mail.example.com 25 and try to relay mail to an external domain from an untrusted IP. The server must respond with 554 Relay access denied.

Related restriction lists

# smtpd_recipient_restrictions — controls who can receive mail
smtpd_recipient_restrictions =
    permit_mynetworks,
    permit_sasl_authenticated,
    reject_unauth_destination,
    reject_unknown_recipient_domain   # reject mail to domains we don't host

# smtpd_sender_restrictions — controls who can send mail (envelope From)
smtpd_sender_restrictions =
    permit_mynetworks,
    reject_unknown_sender_domain

Aliases and newaliases

The aliases database maps local email addresses to other addresses, users, files, or programs. This is how root@server mail gets forwarded to a real inbox.

/etc/aliases format

# /etc/aliases
# Format: alias: destination

# Redirect root mail to the ops team mailbox
root:           ops-team@example.com

# Multiple destinations (comma-separated)
postmaster:     alice@example.com, bob@example.com

# Redirect to a local user
webmaster:      alice

# Forward to a file (appended)
archive:        /var/mail/archive

# Pipe to a program
majordomo:      "|/usr/lib/majordomo/wrapper majordomo"

# Catch-all (handle all unknown addresses for a virtual domain)
@example.com:   catchall@example.com

Rebuild the aliases database

# After editing /etc/aliases, rebuild the binary database
newaliases

# Equivalent to:
postmap /etc/aliases

Postfix main.cf directives for aliases

# Point Postfix at the aliases files
alias_maps     = hash:/etc/aliases
alias_database = hash:/etc/aliases

# For virtual alias maps (multiple domains)
virtual_alias_maps = hash:/etc/postfix/virtual
# After editing /etc/postfix/virtual:
postmap /etc/postfix/virtual

Always run newaliases after editing /etc/aliases. Changes to the text file have no effect until the binary database is rebuilt.

master.cf — submission port (587)

master.cf defines which services Postfix runs and how they listen. Port 587 (submission) is the standard port for authenticated mail clients. Unlike port 25 (SMTP), port 587 requires SASL authentication.

# /etc/postfix/master.cf
# Format: service type private unpriv chroot wakeup maxproc command

# Port 25 — SMTP between servers (active by default)
smtp      inet  n       -       n       -       -       smtpd

# Port 587 — Submission from mail clients (uncomment to enable)
submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt      # require TLS (STARTTLS)
  -o smtpd_sasl_auth_enable=yes            # require SASL auth
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

# Port 465 — SMTPS (SSL/TLS wrapper, legacy but still used)
smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
# After editing master.cf, reload Postfix
postfix reload

# Verify the port is listening
ss -tlnp | grep ':587'

Port 587 lines in master.cf are usually present but commented out. Uncomment and configure them rather than creating new entries from scratch.

smtpd_recipient_restrictions primer

smtpd_recipient_restrictions is evaluated once per recipient during the SMTP RCPT TO phase and is the usual hook for both anti-relay and content/policy checks (greylisting, DNSBLs, policy daemons). Order matters — each check is a vote of PERMIT, REJECT, or DUNNO, and evaluation stops at the first non-DUNNO answer.

# /etc/postfix/main.cf — a sane, ordered recipient restriction list
smtpd_recipient_restrictions =
    permit_mynetworks,                 # 1. trust internal hosts first (cheapest check)
    permit_sasl_authenticated,         # 2. trust authenticated submission clients
    reject_unauth_destination,         # 3. anti-open-relay gate — mandatory
    reject_non_fqdn_recipient,         # 4. reject RCPT TO:<user@host> without TLD
    reject_unknown_recipient_domain,   # 5. reject domains with no MX/A records
    reject_rbl_client zen.spamhaus.org,# 6. DNS blocklist (optional, use cautiously)
    check_policy_service unix:private/policyd-spf, # 7. SPF policy daemon
    check_policy_service inet:127.0.0.1:10023,     # 8. postgrey / custom policy
    permit                             # 9. fallthrough — everything else allowed

The guiding rules:

Test your ruleset with postmap -q 'alice@example.com' unix:postfix/local-recipients-regex and a real swaks --to … --from … --server … before you reload. A misplaced permit or reject can turn your MTA into a spam source or silently drop legitimate mail — both take hours to notice.

DKIM / DMARC via milter

Signing outbound mail with DKIM and enforcing DMARC on inbound mail are usually delegated to OpenDKIM and OpenDMARC running as milters (mail filter daemons) that Postfix talks to over a local socket. Postfix itself stays ignorant of the signing algorithm; the milter reads the message, adds/validates headers, and returns the result.

# /etc/postfix/main.cf
# Chain Postfix → OpenDKIM (8891) → OpenDMARC (8893). Milters run in list order.
smtpd_milters    = inet:localhost:8891, inet:localhost:8893
non_smtpd_milters = $smtpd_milters   # also sign locally-submitted mail (sendmail, php, cron)

# Don't reject mail if a milter is unreachable — queue for retry instead
milter_default_action = accept

# Version 6 protocol — required for modern opendkim/opendmarc
milter_protocol = 6

Then on the OpenDKIM side (/etc/opendkim.conf): listen on inet:8891@localhost, point KeyTable/SigningTable at your per-domain selectors, publish the matching TXT selector._domainkey.example.com record in DNS. OpenDMARC similarly listens on 8893 and queries the recipient domain's _dmarc.example.com record during inbound SMTP to enforce the published policy.

See Postfix TLS & DKIM for the full OpenDKIM key-generation, DNS-publication, and OpenDMARC report-aggregation workflow. Keep milter_default_action = accept in production — reject means a single crashed milter takes down mail delivery.