Skip to content

Instantly share code, notes, and snippets.

@alkavan
Last active May 21, 2025 19:15
Show Gist options
  • Save alkavan/05186854298aee5ea886f9662403921c to your computer and use it in GitHub Desktop.
Save alkavan/05186854298aee5ea886f9662403921c to your computer and use it in GitHub Desktop.

Rocky Linux 9 | Mail Server Installation

This is a tutorial how to install your own mail server on a Rocky Linux 9 machine.

Initial System Setup

Update system.

sudo dnf update -y

Set your timezone.

sudo timedatectl set-timezone UTC && date

Set the machine hostname.

sudo hostnamectl set-hostname mx.maildomain.com

Enable EPEL Repository

sudo dnf install -y epel-release

Install nano text editor, a nice monitor, and terminal multiplexer.

sudo dnf install -y nano htop tmux

Reboot system

sudo reboot

Install PostgreSQL server

Used for storing domains, users, and aliases.

Install database server:

sudo dnf install -y postgresql-server postgresql-contrib

Initialize database:

sudo postgresql-setup --initdb

Configure password encryption

Edit the configuration file generated by the previous step nano /var/lib/pgsql/data/postgresql.conf, append the following under the auth section or uncomment password_encryption and change md5 to scram-sha-256:

password_encryption = scram-sha-256

Enable and start postgresql service:

sudo systemctl enable --now postgresql
systemctl status postgresql

Create database and user for mail

Switch to the postgres admin user:

sudo -u postgres psql

Run queries to create maildb database and mailuser user with ALL PRIVILEGES:

CREATE DATABASE maildb;
CREATE USER mailuser WITH ENCRYPTED PASSWORD 'strong_password';
GRANT ALL PRIVILEGES ON DATABASE maildb TO mailuser;

Create the necessary tables. The schema includes:

  • domains: Stores virtual domains.
  • users: Stores mailbox user credentials with SCRAM-SHA-256 verifiers.
  • aliases: Stores email aliases.
\c maildb
CREATE TABLE domains (
    id SERIAL PRIMARY KEY,
    domain VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    domain_id INTEGER REFERENCES domains(id),
    username VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL, -- Should store SCRAM-SHA-256 verifier
    UNIQUE (domain_id, username)
);

CREATE TABLE aliases (
    id SERIAL PRIMARY KEY,
    source_domain_id INTEGER REFERENCES domains(id),
    source_username VARCHAR(255),
    destination_domain_id INTEGER REFERENCES domains(id),
    destination_username VARCHAR(255)
);

ALTER TABLE aliases OWNER TO mailuser;
ALTER TABLE domains OWNER TO mailuser;
ALTER TABLE users OWNER TO mailuser;

\q

\c maildb

CREATE TABLE domains (
    domain VARCHAR(255) NOT NULL PRIMARY KEY
);

CREATE TABLE users (
    email VARCHAR(255) NOT NULL PRIMARY KEY,
    password VARCHAR(255) NOT NULL,
    domain VARCHAR(255) NOT NULL REFERENCES domains(domain)
);

CREATE TABLE aliases (
    source VARCHAR(255) NOT NULL PRIMARY KEY,
    destination VARCHAR(255) NOT NULL,
    domain VARCHAR(255) NOT NULL REFERENCES domains(domain)
);

\q

It should look like this:

\dt
          List of relations
 Schema |  Name   | Type  |  Owner
--------+---------+-------+----------
 public | aliases | table | mailuser
 public | domains | table | mailuser
 public | users   | table | mailuser
(3 rows)

Note: For SCRAM-SHA-256, passwords should ideally be stored as salted hashes, but Cyrus SASL with auxprop can use plaintext passwords and compute the necessary SCRAM attributes during authentication. For simplicity, we'll assume plaintext storage here, but in production, consider generating SCRAM-specific hashes (e.g., via a script) and adjust the SQL queries accordingly.

Configure access control

Edit pg_hba.conf database access configuration file:

sudo nano /var/lib/pgsql/data/pg_hba.conf

Allow mailuser connect to maildb using scram-sha-256 password:

host    maildb          mailuser        127.0.0.1/32            scram-sha-256

Ensure this line comes before any ident, md5, or peer rules for 127.0.0.1, as PostgreSQL processes rules in order. Example:

# IPv4 local connections:
host    maildb          mailuser        127.0.0.1/32            scram-sha-256
host    all             all             127.0.0.1/32            ident

Restart database service and confirm connection

Restart database:

sudo systemctl restart postgresql

You should be prompted for a password.

psql -h 127.0.0.1 -U mailuser -d maildb

Install Postfix SMTP Server

sudo dnf -y install postfix postfix-pgsql

Create postfix configurations for domains, uesrs and aliases:

Create virtual_mailbox_domains.cf and put contents inside, and update the database password:

sudo nano /etc/postfix/virtual_mailbox_domains.cf
user = mailuser
password = securepassword
hosts = 127.0.0.1
dbname = maildb
query = SELECT domain FROM domains WHERE domain='%s'

Create virtual_mailbox_maps.cf and put contents inside, and update the database password:

sudo nano /etc/postfix/virtual_mailbox_maps.cf
user = mailuser
password = securepassword
hosts = 127.0.0.1
dbname = maildb
query = SELECT email FROM users WHERE email='%s'

Create virtual_alias_maps.cf and put contents inside, and update the database password:

sudo nano /etc/postfix/virtual_alias_maps.cf
user = mailuser
password = securepassword
hosts = 127.0.0.1
dbname = maildb
query = SELECT destination FROM aliases WHERE source='%s'

Add test domain, user, and alias

Connect maildb database as mailuser:

psql -h 127.0.0.1 -U mailuser -d maildb
-- Add the new domain 'alkontek.com'
INSERT INTO domains (domain) VALUES ('alkontek.com');

-- Add a mailbox for '[email protected]' with a plain text password
-- Replace 'securepassword' with a strong password
INSERT INTO users (email, password, domain)
VALUES ('[email protected]', 'securepassword', 'alkontek.com');

-- Or optionally, use an encrypted password format like '{SHA512-CRYPT}hashedpassword'
INSERT INTO users (email, password, domain)
VALUES ('[email protected]', '{SHA512-CRYPT}$6$Q7yX...[rest of hash]', 'alkontek.com');

-- Add an alias '[email protected]' pointing to '[email protected]'
INSERT INTO aliases (source, destination, domain) 
VALUES ('[email protected]', '[email protected]', 'alkontek.com');

Test with postmap

postmap -q alkontek.com pgsql:/etc/postfix/virtual_mailbox_domains.cf

Expected output: alkontek.com (indicating the domain exists).

postmap -q [email protected] pgsql:/etc/postfix/virtual_mailbox_maps.cf

Expected output: [email protected] (indicating the user mailbox exists).

postmap -q [email protected] pgsql:/etc/postfix/virtual_alias_maps.cf

Expected output: [email protected] (indicating the mail alias exists).


Edit postfix server configurations

Edit master.cf config and enable submission service:

sudo nano /etc/postfix/master.cf
submission inet n       -       y       -       -       smtpd
     -o syslog_name=postfix/submission
     -o smtpd_tls_security_level=encrypt
     -o smtpd_sasl_auth_enable=yes
     -o smtpd_reject_unlisted_recipient=no
     -o smtpd_client_restrictions=permit_sasl_authenticated,reject
     -o smtpd_helo_restrictions=
     -o smtpd_sender_restrictions=
     -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
     -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
     -o milter_macro_daemon_name=ORIGINATING

Edit main.cf and configure postfix:
nano /etc/postfix/main.cf

myhostname = mx.alkontek.com
mydomain = alkontek.com
myorigin = $mydomain
inet_interfaces = all
mydestination = localhost
virtual_mailbox_domains = pgsql:/etc/postfix/pgsql-virtual-domains.cf
virtual_mailbox_maps = pgsql:/etc/postfix/pgsql-virtual-mailboxes.cf
virtual_alias_maps = pgsql:/etc/postfix/pgsql-virtual-aliases.cf
virtual_mailbox_base = /var/spool/mail/vhosts
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
smtpd_tls_cert_file = /etc/pki/tls/certs/postfix.pem
smtpd_tls_key_file = /etc/pki/tls/private/postfix.key
smtpd_use_tls = yes
smtpd_tls_auth_only = yes
smtpd_sasl_type = cyrus
smtpd_sasl_path = smtpd
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination

Or run the following command to amend configuration file:

postconf -e 'myhostname = mx.alkontek.com'
postconf -e 'mydomain = alkontek.com'
postconf -e 'myorigin = $mydomain'
postconf -e 'inet_interfaces = all'
postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost'

postconf -e 'virtual_mailbox_domains = pgsql:/etc/postfix/virtual_mailbox_domains.cf'
postconf -e 'virtual_mailbox_maps = pgsql:/etc/postfix/virtual_mailbox_maps.cf'
postconf -e 'virtual_alias_maps = pgsql:/etc/postfix/virtual_alias_maps.cf'
postconf -e 'virtual_mailbox_base = /var/spool/mail/vhosts'
postconf -e 'virtual_uid_maps = static:5000'
postconf -e 'virtual_gid_maps = static:5000'

postconf -e 'smtpd_banner = $myhostname ESMTP'
postconf -e 'smtpd_tls_cert_file = /etc/pki/tls/certs/postfix.pem'
postconf -e 'smtpd_tls_key_file = /etc/pki/tls/private/postfix.key'
postconf -e 'smtpd_use_tls = yes'
postconf -e 'smtpd_tls_auth_only = yes'
postconf -e 'smtpd_sasl_type = cyrus'
postconf -e 'smtpd_sasl_path = smtpd'
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination'

postconf -e 'mua_client_restrictions = permit_sasl_authenticated, reject'
postconf -e 'mua_helo_restrictions = permit_sasl_authenticated, reject'
postconf -e 'mua_sender_restrictions = permit_sasl_authenticated, reject'

OLD:

# uncomment and specify hostname
myhostname = mail.srv.world

# uncomment and specify domain name
mydomain = srv.world

# uncomment
myorigin = $mydomain

# listen on all network interfaces
inet_interfaces = all

# change it for ipv4 only
inet_protocols = ipv4

# uncomment and add more domains
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain

# uncomment and specify your local network
mynetworks = 127.0.0.0/8, 10.0.0.0/24

# or, set to only trust local host
mynetworks_style = host

# uncomment home mailbox location (use Maildir)
home_mailbox = Maildir/

# uncomment this line (without version)
smtpd_banner = $myhostname ESMTP $mail_name

# add next setting in the end of the file

# disable SMTP VRFY command
disable_vrfy_command = yes

# require HELO command to sender hosts
smtpd_helo_required = yes

# limit an email size
# example below means 10M bytes limit
message_size_limit = 10240000

# SMTP-Auth settings
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
smtpd_recipient_restrictions = permit_mynetworks, permit_auth_destination, permit_sasl_authenticated, reject

Create user, group, and home for virtual inbox:

sudo mkdir -p /var/spool/mail/vhosts
sudo groupadd -g 5000 vmail
sudo useradd -u 5000 -g vmail -s /sbin/nologin -d /var/spool/mail/vhosts vmail
sudo chown -R vmail:vmail /var/spool/mail/vhosts
sudo chmod -R 750 /var/spool/mail/vhosts

Enable and start postfix server and check status:

systemctl enable --now postfix
systemctl status postfix

Extra Security and Anti-Spam Settings

Edit the postfix configuarion file:

nano /etc/postfix/main.cf

Consider carfually before adding the following settings, they might reject messages that you do not intened.

Add the following settings:

# reject unknown clients that forward lookup and reverse lookup of their hostnames on DNS do not match
smtpd_client_restrictions = permit_mynetworks, reject_unknown_client_hostname, permit

# rejects senders that domain name set in FROM are not registered in DNS or 
# not registered with FQDN
smtpd_sender_restrictions = permit_mynetworks, reject_unknown_sender_domain,reject_non_fqdn_sender

# reject hosts that domain name set in FROM are not registered in DNS or 
# not registered with FQDN when your SMTP server receives HELO command
smtpd_helo_restrictions = permit_mynetworks, reject_unknown_hostname,reject_non_fqdn_hostname, reject_invalid_hostname, permit

Restart postfix server:

systemctl restart postfix

Configure SMTP Authentication

Install Cyrus SASL:

dnf install -y cyrus-sasl cyrus-sasl-plain

Enable and Start SASL Service:

systemctl enable saslauthd --now

Edit /etc/postfix/master.cf and change the smtpd_recipient_restrictions option to permit_sasl_authenticated,reject for both submission and smtps:

submission inet n	-	n	-	-	smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=$mua_client_restrictions
  -o smtpd_helo_restrictions=$mua_helo_restrictions
  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
smtps     inet  n	-	n	-	-	smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=$mua_client_restrictions
  -o smtpd_helo_restrictions=$mua_helo_restrictions
  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Cyrus IMAP Server Installation

sudo dnf install -y cyrus-imapd cyrus-sasl cyrus-sasl-lib cyrus-sasl-sql

Firewall Settings

Install firewalld:

sudo dnf install -y firewalld

Run firewall service:

sudo systemctl enable firewalld --now

Add mail ports to firewall and ispect changes:

sudo firewall-cmd --add-service=smtp
sudo firewall-cmd --add-service=imap
sudo firewall-cmd --add-service=imaps
sudo firewall-cmd --add-port=25/tcp
sudo firewall-cmd --add-port=143/tcp
sudo firewall-cmd --add-port=993/tcp
sudo firewall-cmd --add-port=587/tcp
sudo firewall-cmd --add-port=465/tcp
sudo firewall-cmd --list-all
sudo firewall-cmd --runtime-to-permanent

When confired everything works, remove non-secure ports and services:

sudo firewall-cmd --remove-port=143/tcp
sudo firewall-cmd --remove-service=imap
sudo firewall-cmd --list-all
sudo firewall-cmd --runtime-to-permanent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment