This is a tutorial how to install your own mail server on a Rocky Linux 9 machine.
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
sudo dnf install -y epel-release
Install nano text editor, a nice monitor, and terminal multiplexer.
sudo dnf install -y nano htop tmux
sudo reboot
Used for storing domains, users, and aliases.
Install database server:
sudo dnf install -y postgresql-server postgresql-contrib
Initialize database:
sudo postgresql-setup --initdb
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
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.
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:
sudo systemctl restart postgresql
You should be prompted for a password.
psql -h 127.0.0.1 -U mailuser -d maildb
sudo dnf -y install postfix postfix-pgsql
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'
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');
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 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
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
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
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
sudo dnf install -y cyrus-imapd cyrus-sasl cyrus-sasl-lib cyrus-sasl-sql
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