
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 onlyexample.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
andlara@example.com
; both are assigned tofamily@example.com
so this query should return:franck@example.com
andlara@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 oflara@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