ISPMail on RHEL (Part Three)

Early configuration in Postfix

In this article we begin performing some early configuration in Postfix.

We basically have three queries to perform on our DB:

  • Check if a domain is handled by our server: Is example.com handled? Or do we handle only example.net?
  • Check if a user is present on our server: Is franck@example.com a valid user?
  • Check if an email alias exists on our server: Is family@example.com a valid alias? And what are its expanded addresses? For example, let’s assume we have two email on our server: franck@example.com and lara@example.com; both are assigned to family@example.com so this query should return: franck@example.com and lara@example.com;
  • Check if the logged-in user is allowed to send emails using the address they want: This is a forged email protection that is effective when a correctly logged in user is trying to send an email with a forged email address; if this feature is not implemented, nothing stops franck@example.com from sending emails in the name of lara@example.com or even worse!

About email address forging

In the early days of internet, it was 1999, I sent an email to a friend of mine setting the From field as: Bill Clinton <bill.clinton@whitehouse.gov>. The email was delivered… Of course, my provider didn’t implement forged email protection at that time!

Virtual mailbox domains

On this first step we configure Postfix to check if it handle a specific domain; we create a file under /etc/postfix named sqlite-virtual-mailbox-domains.cf :

dbpath = /etc/dovecot/mailserver.db
query = SELECT 1 FROM virtual_domains WHERE name='%s'

The meaning of these two lines is straightforward: First line is the path of the SQLite file we created on Part Two; the second line is the query that has to be run against the database and should return 1 if the domain is handled, nothing otherwise.

Virtual mailbox maps

The second step is to check whether the provided email address is valid; again, we create another file under /etc/postfix named sqlite-virtual-mailbox-maps.cf :

dbpath = /etc/dovecot/mailserver.db
query = SELECT 1 FROM virtual_users WHERE email='%s'

The query changes; we don’t check against virtual_domains anymore but against virtual_users. The user franck@example.com exists? It returns 1, nothing otherwise.

Virtual alias maps

The last step is to handle aliases! The last file to create is sqlite-virtual-alias-maps.cf , always under /etc/postfix :

dbpath = /etc/dovecot/mailserver.db
query = SELECT destination FROM virtual_aliases WHERE source='%s'

Something different here: We don’t just want a simple 1 or nothing; we need the destinations for a source email address: family@example.com must return franck@example.com and lara@example.com.

Sender login maps

This is to prevent sending emails with a forged email address; same file location as above, filename is sqlite-email2email.cf and the content is:

dbpath = /etc/dovecot/mailserver.db
query = SELECT email FROM virtual_users WHERE email='%s'

When this query runs, if the address is valid, it should return the email address itself.

Safety features

Let’s keep an eye on the mess of my directory /etc/postfix and let’s think together about:

# pwd
/etc/postfix
# ls -l
total 236
-rw-r--r--. 1 root root 21111 Sep  8  2019 access
-rw-r--r--. 1 root root 13194 Jun  3  2018 canonical
-rw-r--r--. 1 root root    60 Oct  3 00:42 dynamicmaps.cf
drwxr-xr-x. 2 root root    20 Jan 12 17:04 dynamicmaps.cf.d
-rw-r--r--. 1 root root 10221 Sep 17  2016 generic
-rw-r--r--. 1 root root 23802 Oct  9  2016 header_checks
-rw-r--r--. 1 root root 30442 Feb  4 14:18 main.cf
-rw-r--r--. 1 root root 29130 Oct  3 00:42 main.cf.proto
-rw-r--r--. 1 root root  6906 Jan 31 14:55 master.cf
-rw-r--r--. 1 root root  6372 Oct  3 00:42 master.cf.proto
-rw-r--r--. 1 root root 20163 Oct  3 00:42 postfix-files
drwxr-xr-x. 2 root root    20 Jan 12 17:04 postfix-files.d
-rw-r--r--. 1 root root  6929 Feb 14  2016 relocated
-rw-r—-r--. 1 root root    93 Jan 31 15:23 sqlite-email2email.cf
-rw-r—-r--. 1 root root   102 Jan 31 15:23 sqlite-virtual-alias-maps.cf
-rw-r—-r--. 1 root root    90 Jan 31 15:24 sqlite-virtual-mailbox-domains.cf
-rw-r—-r--. 1 root root    89 Jan 31 15:24 sqlite-virtual-mailbox-maps.cf
-rw-r--r--. 1 root root 13436 Jan 11  2020 transport
-rw-r--r--. 1 root root 13963 Jun  3  2018 virtual

The three files we created are the ones highlighted in bold. To keep them safe, please set the rw permissions as follows:

# chgrp postfix /etc/postfix/sqlite-*.cf
# chmod u=rw,g=r,o= /etc/postfix/sqlite-*.cf

The result is:

-rw-r-----. 1 root postfix    93 Jan 31 15:23 sqlite-email2email.cf
-rw-r-----. 1 root postfix   102 Jan 31 15:23 sqlite-virtual-alias-maps.cf
-rw-r-----. 1 root postfix    90 Jan 31 15:24 sqlite-virtual-mailbox-domains.cf
-rw-r-----. 1 root postfix    89 Jan 31 15:24 sqlite-virtual-mailbox-maps.cf

Changes in master.cf

master.cf file has to be changed to allows Postfix to listen on submission port.

Uncomment and change if needed to fit the following:

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=
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Changes in main.cf

Now we have to tell Postfix what to do when it has to deliver an email, even better: How to use the three files we created. Look at the main.cf file, the Postfix main configuration file, and add the following:

# FPG configurations - 2025.Jan.17
# Mailbox and domains mappings
virtual_mailbox_domains = sqlite:/etc/postfix/sqlite-virtual-mailbox-domains.cf
virtual_mailbox_maps = sqlite:/etc/postfix/sqlite-virtual-mailbox-maps.cf
virtual_alias_maps = sqlite:/etc/postfix/sqlite-virtual-alias-maps.cf,sqlite:/etc/postfix/sqlite-email2email.cf
smtpd_sender_login_maps=sqlite:/etc/postfix/sqlite-email2email.cf

# Handle encryption in postfix
smtpd_tls_security_level=may
smtpd_tls_auth_only=yes
smtpd_tls_cert_file=/etc/letsencrypt/live/webmail.example.org/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/webmail.example.org/privkey.pem
smtp_tls_security_level=may

I like to put all modifications at the bottom whenever possible, with a date and a clear header. The syntax and the meanings should be clear. If you need more details, please check Christoph blog and official Postfix documentation.

Of course change the webmail.example.org to the actual directory of your SSL certificates!

Now, to make all the changes effective, restart postfix:

# systemctl restart postfix; systemctl status postfix
● postfix.service - Postfix Mail Transport Agent
     Loaded: loaded (/usr/lib/systemd/system/postfix.service; enabled; preset: disabled)
     Active: active (running) since Mon 2025-03-03 14:54:35 CET; 18ms ago
    Process: 679623 ExecStartPre=/usr/sbin/restorecon -R /var/spool/postfix/pid (code=exited, status=0/SUCCESS)
    Process: 679624 ExecStartPre=/usr/libexec/postfix/aliasesdb (code=exited, status=0/SUCCESS)
    Process: 679626 ExecStartPre=/usr/libexec/postfix/chroot-update (code=exited, status=0/SUCCESS)
    Process: 679627 ExecStart=/usr/sbin/postfix start (code=exited, status=0/SUCCESS)
   Main PID: 679695 (master)
      Tasks: 3 (limit: 22956)
     Memory: 3.3M
        CPU: 366ms
     CGroup: /system.slice/postfix.service
             ├─679695 /usr/libexec/postfix/master -w
             ├─679696 pickup -l -t unix -u
             └─679697 qmgr -l -t unix -u

Mar 03 14:54:35 vps07 systemd[1]: Starting Postfix Mail Transport Agent...
Mar 03 14:54:35 vps07 postfix/postfix-script[679693]: starting the Postfix mail system
Mar 03 14:54:35 vps07 postfix/master[679695]: daemon started -- version 3.5.25, configuration /etc/postfix
Mar 03 14:54:35 vps07 systemd[1]: Started Postfix Mail Transport Agent.

Testing queries

It’s time to test our queries, to see if they work and return the expected results.

# postmap -q example.com sqlite:/etc/postfix/sqlite-virtual-mailbox-domains.cf
1
# postmap -q franck@example.com sqlite:/etc/postfix/sqlite-virtual-mailbox-maps.cf 
1
# postmap -q family@example.com sqlite:/etc/postfix/sqlite-virtual-alias-maps.cf 
franck@example.com,lara@example.com
# postmap -q lara@example.com sqlite:/etc/postfix/sqlite-email2email.cf
lara@example.com

The three commands above tests:

  • If domain example.com is handled by this server; 1 means: Yes!
  • If user franck@example.com is present on this server; 1 means: Yes!
  • If alias family@example.com is present on this server and how it expands and the result is the two emails the alias corresponds to;
  • If the logged in user lara@example.com is allowed to send email in it’s own name, answer is it’s own email address so obviously yes.

For the sake of completeness we will check four negative cases:

# postmap -q whitehouse.gov sqlite:/etc/postfix/sqlite-virtual-mailbox-domains.cf
# postmap -q foobar@example.com sqlite:/etc/postfix/sqlite-virtual-mailbox-maps.cf
# postmap -q workgroup@example.com sqlite:/etc/postfix/sqlite-virtual-alias-maps.cf
# postmap -q bill@whitehouse.gov sqlite:/etc/postfix/sqlite-email2email.cf
# 

As we expected, a clear nothing!

Leave a Reply

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