Configure Postfix and Dovecot on RHEL, AlmaLinux, Rocky and Oracle Linux

Running your own mail server on the Enterprise Linux family is still one of the most useful skills a sysadmin can have, whether you need an internal relay for application alerts or a full mailbox server for a small domain. The standard stack on Red Hat Enterprise Linux and its rebuilds is Postfix as the SMTP server (MTA) and Dovecot as the IMAP server and SASL authentication provider. Both ship in the base AppStream repository, so you need nothing from third-party repos for the core setup.

This guide builds a single-host mail server for one domain with local system users, TLS everywhere, authenticated submission on port 587 for mail clients, IMAPS on port 993, and LMTP delivery from Postfix into Dovecot. The commands were checked against the Red Hat Enterprise Linux 10 Deploying mail servers documentation and the Dovecot 2.3 docs, and they apply to RHEL 9 and 10, AlmaLinux, Rocky Linux, Oracle Linux and CentOS Stream. Where EL9 and EL10 differ, the difference is called out.

What you will build

  • Mail host: mail.example.com at 192.168.1.40, mail domain example.com
  • Postfix receives mail for example.com on port 25 and hands it to Dovecot over LMTP
  • Users send mail through Postfix on port 587 (STARTTLS + SASL authentication handled by Dovecot)
  • Users read mail with IMAPS on port 993, stored as Maildir in each user home directory
  • SELinux enforcing and firewalld enabled throughout

Replace the hostname, IP address and domain with your own everywhere they appear.

Package versions by release

ReleasePostfixDovecotDefault Postfix map type
RHEL 9.x, AlmaLinux 9, Rocky Linux 9, Oracle Linux 93.5.252.3.16hash (Berkeley DB); lmdb available via postfix-lmdb
RHEL 10.x, AlmaLinux 10, Rocky Linux 10, Oracle Linux 103.8.52.3.21lmdb (Berkeley DB removed)
CentOS Stream 9 / 10Tracks the next 9.x / 10.x minorTracks the next minorSame as EL9 / EL10
Postfix and Dovecot in AppStream at the time of writing (EL 9.8 and 10.2). Check yours with rpm -q postfix dovecot.

The biggest practical difference is the lookup table format. RHEL 10 no longer ships the Berkeley DB library, so Postfix cannot open hash: or btree: tables and uses LMDB by default. This guide uses lmdb: on both releases so the same configuration works on EL9 and EL10 and survives a future in-place upgrade.

Prerequisites

  • A minimal EL9 or EL10 install with a static IP and root or sudo access.
  • DNS records: an A record for mail.example.com, an MX record for example.com pointing to mail.example.com, and a matching reverse (PTR) record for the public IP. Many receiving servers reject mail from hosts without forward-confirmed reverse DNS.
  • An SPF record such as v=spf1 mx -all as a TXT record on example.com.
  • Accurate time (chrony is enabled by default) so TLS certificate validation works.
  • Outbound TCP 25 allowed by your network or cloud provider. Many clouds block it; if yours does, you will need a relayhost (covered in troubleshooting).
  • A TLS certificate for mail.example.com from a public CA (Let’s Encrypt is shown below) or your internal CA.

Set the hostname first, because Postfix derives its defaults from it:

sudo hostnamectl set-hostname mail.example.com
hostname -f

Step 1: Install Postfix and Dovecot

On every release the packages come from AppStream. s-nail provides the mail command used for testing.

# EL9 (RHEL 9, AlmaLinux 9, Rocky Linux 9, Oracle Linux 9, CentOS Stream 9)
sudo dnf -y install postfix postfix-lmdb dovecot s-nail

# EL10 (postfix-lmdb is pulled in automatically as a dependency)
sudo dnf -y install postfix dovecot s-nail

If Sendmail is installed (possible on EL9; it was removed from RHEL 10), remove it so Postfix owns the /usr/sbin/sendmail alternative:

sudo dnf -y remove sendmail
sudo alternatives --display mta

Oracle Linux users: the packages are in the ol9_appstream / ol10_appstream repositories, which are enabled by default. The kernel choice (UEK or RHCK) makes no difference to this setup.

Step 2: Get a TLS certificate

Any certificate whose subject or SAN matches mail.example.com works. To use Let’s Encrypt, install certbot from EPEL and request a certificate in standalone mode (temporarily open port 80):

# AlmaLinux / Rocky Linux / CentOS Stream
sudo dnf -y install epel-release
# Oracle Linux 9 (use oracle-epel-release-el10 on OL10)
sudo dnf -y install oracle-epel-release-el9
# RHEL: enable CodeReady Builder and install the epel-release RPM from the Fedora EPEL docs

sudo dnf -y install certbot
sudo firewall-cmd --add-service=http
sudo certbot certonly --standalone -d mail.example.com
sudo firewall-cmd --remove-service=http

Rather than pointing Postfix and Dovecot into /etc/letsencrypt, copy the files into the standard RHEL PKI directories, which already carry the correct SELinux label (cert_t). A certbot deploy hook keeps the copies current after each renewal:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/mail.sh >/dev/null <<'EOF'
#!/bin/bash
install -m 0644 /etc/letsencrypt/live/mail.example.com/fullchain.pem /etc/pki/tls/certs/mail.example.com.crt
install -m 0600 /etc/letsencrypt/live/mail.example.com/privkey.pem  /etc/pki/tls/private/mail.example.com.key
restorecon /etc/pki/tls/certs/mail.example.com.crt /etc/pki/tls/private/mail.example.com.key
systemctl reload postfix dovecot 2>/dev/null || true
EOF
sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/mail.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/mail.sh
sudo systemctl enable --now certbot-renew.timer

Step 3: Configure Postfix

Use postconf -e to edit /etc/postfix/main.cf so every change is a single, repeatable command:

sudo postconf -e 'myhostname = mail.example.com'
sudo postconf -e 'mydomain = example.com'
sudo postconf -e 'myorigin = $mydomain'
sudo postconf -e 'inet_interfaces = all'
sudo postconf -e 'inet_protocols = all'
sudo postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain'
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'
sudo postconf -e 'message_size_limit = 26214400'

# LMDB lookup tables (default on EL10, needs postfix-lmdb on EL9)
sudo postconf -e 'default_database_type = lmdb'
sudo postconf -e 'alias_maps = lmdb:/etc/aliases'
sudo postconf -e 'alias_database = lmdb:/etc/aliases'

# Hand local mailboxes to Dovecot over LMTP
sudo postconf -e 'mailbox_transport = lmtp:unix:/var/run/dovecot/lmtp'

# TLS
sudo postconf -e 'smtpd_tls_cert_file = /etc/pki/tls/certs/mail.example.com.crt'
sudo postconf -e 'smtpd_tls_key_file = /etc/pki/tls/private/mail.example.com.key'
sudo postconf -e 'smtpd_tls_security_level = may'
sudo postconf -e 'smtp_tls_security_level = may'
sudo postconf -e 'smtpd_tls_auth_only = yes'

# SASL via Dovecot (enabled only on the submission port below)
sudo postconf -e 'smtpd_sasl_type = dovecot'
sudo postconf -e 'smtpd_sasl_path = private/auth'
sudo postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'

Port 25 stays open for server-to-server mail without authentication. Mail clients use the submission service on port 587, which requires TLS and a login. Define it in /etc/postfix/master.cf with postconf -M and postconf -P:

sudo postconf -M submission/inet='submission inet n - n - - smtpd'
sudo postconf -P 'submission/inet/syslog_name=postfix/submission'
sudo postconf -P 'submission/inet/smtpd_tls_security_level=encrypt'
sudo postconf -P 'submission/inet/smtpd_sasl_auth_enable=yes'
sudo postconf -P 'submission/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject'
sudo postconf -P 'submission/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject'
sudo postconf -P 'submission/inet/milter_macro_daemon_name=ORIGINATING'

sudo postconf -M submission/inet

Point the postmaster and root aliases at a real user, then build the alias table. On EL9 this converts /etc/aliases to /etc/aliases.lmdb:

echo 'root: alice' | sudo tee -a /etc/aliases
sudo newaliases
sudo postfix check

Crypto settings: Postfix on RHEL follows the system-wide crypto policy, so TLS protocol versions and ciphers are already sane. Use update-crypto-policies --show to check the policy instead of hand-tuning cipher lists.

Step 4: Configure Dovecot

Dovecot reads /etc/dovecot/dovecot.conf plus everything in /etc/dovecot/conf.d/. Make the following edits in the files named.

/etc/dovecot/dovecot.conf: serve IMAP and LMTP only (no POP3):

protocols = imap lmtp

/etc/dovecot/conf.d/10-mail.conf: store mail as Maildir in each user home:

mail_location = maildir:~/Maildir
first_valid_uid = 1000

/etc/dovecot/conf.d/10-auth.conf: keep plaintext logins disabled outside TLS, allow the LOGIN mechanism for Outlook, and strip the domain so alice@example.com maps to the system user alice (this is also what makes LMTP deliveries find the user):

disable_plaintext_auth = yes
auth_mechanisms = plain login
auth_username_format = %Ln

/etc/dovecot/conf.d/10-ssl.conf: require TLS and use the certificate from Step 2. The leading < tells Dovecot to read the value from a file:

ssl = required
ssl_cert = </etc/pki/tls/certs/mail.example.com.crt
ssl_key = </etc/pki/tls/private/mail.example.com.key
ssl_dh = </etc/dovecot/dh.pem
ssl_min_protocol = TLSv1.2
# Generate DH parameters once (can take a few minutes)
sudo openssl dhparam -out /etc/dovecot/dh.pem 4096

/etc/dovecot/conf.d/10-master.conf: give Postfix access to the LMTP socket and create the SASL socket inside the Postfix queue directory. Edit the existing service lmtp and service auth sections so they contain:

service lmtp {
  unix_listener lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
  unix_listener auth-userdb {
    mode = 0600
  }
}

/etc/dovecot/conf.d/20-lmtp.conf: set the postmaster address used in bounces:

protocol lmtp {
  postmaster_address = postmaster@example.com
}

/etc/dovecot/conf.d/15-mailboxes.conf: auto-create and subscribe the standard folders so every client finds Sent, Drafts, Junk and Trash. Add auto = subscribe to each special-use mailbox inside namespace inbox:

namespace inbox {
  mailbox Drafts {
    special_use = \Drafts
    auto = subscribe
  }
  mailbox Junk {
    special_use = \Junk
    auto = subscribe
  }
  mailbox Trash {
    special_use = \Trash
    auto = subscribe
  }
  mailbox Sent {
    special_use = \Sent
    auto = subscribe
  }
}

Check the merged configuration. doveconf -n prints only non-default settings and fails loudly on syntax errors:

sudo doveconf -n

Step 5: Open the firewall and start the services

sudo firewall-cmd --permanent --add-service=smtp --add-service=smtp-submission --add-service=imaps
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

sudo systemctl enable --now dovecot postfix
sudo systemctl status postfix dovecot --no-pager

No SELinux booleans or port labels are needed for this layout: ports 25, 587 and 993 are already labeled, the SASL socket under /var/spool/postfix/private and the LMTP socket under /run/dovecot are allowed by the targeted policy, and ~/Maildir gets the mail_home_rw_t type automatically.

Step 6: Create a mail user

Mail users are regular system accounts. Give them a nologin shell so the mail server is not also a shell server:

sudo useradd -m -s /sbin/nologin alice
sudo passwd alice

Verify the mail server

Work through these checks in order. Each one isolates a single component.

1. Listening ports

sudo ss -tlnp | grep -E ':(25|587|993)\s'

2. Dovecot authentication (should print passdb: alice auth succeeded):

sudo doveadm auth test alice
sudo doveadm user alice

3. Local delivery through Postfix and LMTP:

echo "Test body" | mail -s "Local test" alice@example.com
sudo tail -n 20 /var/log/maillog
sudo ls /home/alice/Maildir/new/

A good delivery logs a line containing relay=mail.example.com[/var/run/dovecot/lmtp] and status=sent (250 2.0.0 <alice@example.com> ... Saved).

4. TLS on submission and IMAPS (check that the certificate chain verifies and the name matches):

openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null | grep -E 'subject=|Verify return code'
openssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null | grep -E 'subject=|Verify return code'

5. Authenticated submission: install swaks from EPEL and send as alice from another machine:

swaks --server mail.example.com:587 --tls --auth LOGIN \
      --auth-user alice --from alice@example.com --to you@gmail.com

6. No open relay: from a host outside mynetworks, try to relay without logging in. Postfix must answer 554 5.7.1 ... Relay access denied:

swaks --server mail.example.com:25 --from test@outside.example --to someone@gmail.com

7. Mail client: configure Thunderbird or another client with IMAP mail.example.com:993 SSL/TLS and SMTP mail.example.com:587 STARTTLS, username alice (or alice@example.com), normal password.

Troubleshooting

Postfix and Dovecot both log to /var/log/maillog through rsyslog. On minimal systems without rsyslog, use the journal: journalctl -u postfix -u dovecot -f.

Symptom in the logCause and fix
unsupported dictionary type: hashSeen on EL10 or after a leapp upgrade from EL9. Replace every hash: / btree: in main.cf and master.cf with lmdb:, run postmap lmdb:/path for each table and newaliases. postconf -n | grep -E 'hash:|btree:' finds leftovers.
SASL: Connect to private/auth failedDovecot is not running or the service auth listener path or owner is wrong. Check ls -lZ /var/spool/postfix/private/auth; it should be owned by postfix with mode 0660.
User doesn't exist: alice@example.comDovecot is looking up the full address. Set auth_username_format = %Ln in 10-auth.conf and reload Dovecot.
lmtp ... Permission deniedSocket ownership in service lmtp does not match the postfix user. Fix 10-master.conf and systemctl restart dovecot.
SSL_accept() failed / client certificate warningsCertificate name does not match the hostname clients use, the chain is incomplete (use fullchain.pem), or the key file is unreadable. Verify with the openssl s_client commands above.
connect to ...:25: Connection timed outOutbound port 25 is blocked upstream. Relay through your provider or a smart host: postconf -e 'relayhost = [smtp.provider.example]:587' plus SASL client credentials.
Relay access denied for your own usersThe client is sending on port 25 or not authenticating. Use port 587 with authentication.
AVC denialsRun sudo ausearch -m AVC -ts recent. Most denials come from mail stored outside the default paths; label custom paths with semanage fcontext and restorecon -Rv. See also Using sed to configure SELinux.

Differences between RHEL, AlmaLinux, Rocky Linux, Oracle Linux and CentOS Stream

  • RHEL: register the system (subscription-manager register) to get AppStream. EPEL (needed only for certbot and swaks) additionally requires the CodeReady Builder repository.
  • AlmaLinux and Rocky Linux: package names and versions match RHEL for the same minor release; dnf install epel-release enables EPEL directly.
  • Oracle Linux: identical packages from ol9_appstream / ol10_appstream; EPEL is provided as oracle-epel-release-el9 / oracle-epel-release-el10. UEK versus RHCK does not matter for mail services.
  • CentOS Stream: receives updates ahead of the next RHEL minor, so Postfix and Dovecot builds can be newer than on RHEL. The configuration in this guide is the same.
  • EL9 to EL10: Berkeley DB is gone (use lmdb:), Sendmail is no longer shipped, and SpamAssassin moved to EPEL. Dovecot stays on the 2.3 series, so the configuration syntax above is unchanged.

Next steps

  • Add DKIM signing (OpenDKIM from EPEL) and publish a DMARC record so large providers trust your outbound mail.
  • Add spam filtering with Rspamd or SpamAssassin as a milter.
  • Move to virtual users stored in LDAP or MariaDB when you outgrow system accounts; Dovecot and Postfix both support these back ends natively.
  • For quick application alerts from other servers, point them at this host as a relay; see Send Email Script CentOS for a simple shell example.
  • Keep the Postfix and Dovecot Cheat Sheet for RHEL, AlmaLinux and Rocky Linux handy for day-to-day queue, log and doveadm commands.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *