Postfix Virtual Domains, Mailboxes, Aliases and SQL Maps
Virtual mail works only when Postfix classifies each domain exactly once and every lookup has a precise contract. A domain placed in both mydestination and virtual_mailbox_domains can loop or use the wrong delivery agent. A recipient map that accepts every local part turns misspellings and dictionary attacks into queue growth. This lesson connects the Lesson 4 database using protected SQL map files and tests all failure directions before final delivery is added. It also separates an authoritative empty result from a database error and an ambiguous multi-row response, because each condition requires different SMTP behavior and operator action.
Prerequisites and inherited lab checkpoint
Retain the Lesson 4 schema, read-only postfix_lookup identity and deterministic fixtures. Postfix must still have the narrow Lesson 3 exposure and pass an unauthenticated third-party relay denial. The virtual UID/GID and Maildir root will be established with Dovecot later; this lesson may accept a message into the queue but must not pretend final mailbox delivery is ready.
Inventory every domain currently appearing in mydestination, relay_domains, virtual_alias_domains and virtual_mailbox_domains. Domain classes must not overlap. Keep the host’s own administrative destination separate from hosted customer domains.
Install and prepare the required components
sudo dnf install -y postfix postfix-mysql
rpm -q postfix postfix-mysql
postconf -m | grep -E 'mysql|lmdb'
sudo install -d -o root -g postfix -m 0750 /etc/postfix/sql
sudo postconf -n > /root/mta-change-backups/lesson-05-postconf-before.txt- On the installed release, verify which package supplies Postfix MySQL map support. Package names can differ by repository and build; do not enable an unofficial repository silently.
- The SQL directory is traversable only by root and the Postfix group. Individual files containing passwords should use mode 0640 or tighter and the minimum required group.
- The query account remains SELECT-only. Postfix never needs to create, disable or change a mailbox.
- Record
postconf -mbecause an available database server does not prove the Postfix client map type is installed.
Create one SQL file per lookup contract
Create root-owned files under /etc/postfix/sql. Supply the database secret through the protected deployment process; REDACTED_SECRET is intentionally unusable.
# /etc/postfix/sql/virtual_domains.cf
user = postfix_lookup
password = REDACTED_SECRET
hosts = unix:/var/lib/mysql/mysql.sock
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s' AND active=1
# /etc/postfix/sql/virtual_mailboxes.cf
user = postfix_lookup
password = REDACTED_SECRET
hosts = unix:/var/lib/mysql/mysql.sock
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s' AND active=1
# /etc/postfix/sql/virtual_aliases.cf
user = postfix_lookup
password = REDACTED_SECRET
hosts = unix:/var/lib/mysql/mysql.sock
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s' AND active=1
Then attach each map through the Postfix proxy:mysql service so chrooted processes do not open database connections directly:
sudo postconf -e 'virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/virtual_domains.cf'
sudo postconf -e 'virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/virtual_mailboxes.cf'
sudo postconf -e 'virtual_alias_maps = proxy:mysql:/etc/postfix/sql/virtual_aliases.cf'
sudo postconf -e 'virtual_mailbox_base = /var/vmail'
sudo postconf -e 'virtual_uid_maps = static:5000'
sudo postconf -e 'virtual_gid_maps = static:5000'
sudo postconf -e 'smtpd_reject_unlisted_recipient = yes'
sudo postfix check
The UID/GID values are a course design choice, not universal constants. Lesson 8 creates the matching operating-system identity and ownership. Do not reload a production server until postmap -q tests demonstrate active, inactive and absent results.
| Lookup | Input | Expected result |
|---|---|---|
| Virtual domain | example.test | A nonempty value only when the domain is active |
| Virtual mailbox | [email protected] | A nonempty value only for an active exact recipient |
| Virtual alias | [email protected] | One or more validated destination addresses |
| Unknown recipient | [email protected] | Empty lookup followed by SMTP rejection before message content |
| Database outage | Any SQL-backed recipient | Lookup error and temporary SMTP failure, not false unknown-user success |
Verify the working path
sudo postfix check
sudo postmap -q example.test proxy:mysql:/etc/postfix/sql/virtual_domains.cf
sudo postmap -q [email protected] proxy:mysql:/etc/postfix/sql/virtual_mailboxes.cf
sudo postmap -q [email protected] proxy:mysql:/etc/postfix/sql/virtual_mailboxes.cf
sudo postmap -q [email protected] proxy:mysql:/etc/postfix/sql/virtual_aliases.cf
sudo postconf -n | grep '^virtual_'
sudo namei -l /etc/postfix/sql/virtual_mailboxes.cf- A successful lookup prints only the value Postfix needs. An absent lookup should be empty; a connection or SQL error must remain distinguishable in command status and logs.
namei -lverifies every path component. A secure file is ineffective if directory traversal or group ownership exposes it unexpectedly.- Use an SMTP
RCPT TOtest for active, missing and disabled recipients. Directpostmapqueries do not prove smtpd invokes restrictions in the intended order. - Inspect logs for SQL errors without printing configuration secrets. Redact addresses and query data before sharing evidence.
Production decisions before continuing
| Decision | Choose deliberately | Evidence to retain |
|---|---|---|
| Domain classes | Assign each domain to exactly one Postfix address class. | Automated overlap report and effective configuration |
| Unknown recipients | Reject invalid recipients during SMTP after required temporary lookup behavior is verified. | Positive, negative and SQL-outage transcripts |
| Catch-all | Default to no catch-all; approve exceptions with abuse and capacity controls. | Named owner, expiry and volume monitoring |
| Alias expansion | Limit recursion/expansion and validate destinations during provisioning. | Loop test, expansion limit and audit trail |
| SQL availability | Choose timeouts and temporary-failure semantics that protect legitimate delivery. | Dependency alert and controlled outage test |
Make address classes, maps and final delivery distinct
virtual_mailbox_domains says Postfix is final destination for a hosted domain. virtual_mailbox_maps identifies valid mailbox recipients. virtual_alias_maps rewrites an address to one or more destinations before final delivery. These are not interchangeable lists. Alias domains, catch-all rules and relay domains need explicit semantics rather than a broad query returning a convenient truthy value.
Recipient validation should happen during SMTP so an invalid address receives a direct permanent response and its message body is not queued. During database failure, however, returning permanent unknown-user is dangerous. Test the error path. An accepted recipient can still fail at LMTP later because quota or storage state is a different decision.
Catch-all accepts arbitrary local parts and concentrates typo traffic, dictionary attacks and spam. It also prevents clean unknown-recipient rejection and can inflate reputation and storage costs. If a business exception requires it, constrain the domain, monitor volume, document ownership and provide an expiry.
Postfix proxy SQL maps use the proxy:mysql: prefix so supported Postfix processes ask the proxymap service to perform the database lookup. The SQL account remains read-only, and present, absent, inactive and database-unavailable results still require separate tests.
Understand the component before configuring it
| Layer | Question to answer | Evidence |
|---|---|---|
| Domain class | Is this local, virtual, relayed or remote? | Disjoint parameter/map membership |
| Recipient validity | Does the exact active mailbox or alias exist? | SQL result and RCPT-stage response |
| Address rewrite | Does an alias expand to valid bounded destinations? | Expansion trace and loop/limit evidence |
| Final transport | Which service will own mailbox delivery? | Transport decision, deferred until LMTP lesson |
| Dependency state | Was the answer empty or did the lookup fail? | postmap exit/log evidence and SMTP 4xx test |
Build it step by step
- Audit domain classes. Remove overlaps and document the one intended class for every accepted domain.
- Protect map files. Install one root-owned SQL configuration for each lookup contract.
- Test queries offline. Check active, inactive, absent and database-error results before attaching maps.
- Attach maps explicitly. Configure virtual domains, mailboxes and aliases without enabling broad relay trust.
- Validate and reload. Run Postfix checks, inspect the effective diff, reload and retain logs.
- Exercise SMTP recipient tests. Prove valid acceptance, invalid rejection, relay denial and temporary SQL failure.
- Inspect queue impact. Ensure negative recipients are not queued and expected lab messages remain explainable.
Operate and inspect the component
postconf mydestination relay_domains virtual_alias_domains virtual_mailbox_domains
postmap -q ADDRESS proxy:mysql:/etc/postfix/sql/virtual_mailboxes.cf
postmap -q ADDRESS proxy:mysql:/etc/postfix/sql/virtual_aliases.cf
postconf -n
postfix check
swaks --server mail1.example.test --to [email protected] --quit-after RCPT
swaks --server mail1.example.test --to [email protected] --quit-after RCPT- Replace
ADDRESSwith one reviewed lab fixture; never build shell loops from an untrusted address list without quoting and rate control. --quit-after RCPTtests envelope acceptance without sending content. Retain reply codes and redact unnecessary identities.- Test third-party relay separately: a valid local sender name must not authorize an unauthenticated remote client.
- A map query against a live database is operational traffic. Use bounded fixtures and do not enumerate customer addresses.
Evidence and acceptance criteria
| Evidence | Healthy result | Failure meaning |
|---|---|---|
| Domain separation | No domain exists in multiple Postfix classes | Routing ambiguity or mail loop risk |
| Valid recipient | Active mailbox returns one value and RCPT is accepted | Map, restriction order or fixture is wrong |
| Invalid recipient | Empty result and permanent RCPT rejection with no queue item | Backscatter and queue abuse risk |
| SQL outage | Temporary RCPT response and dependency alert | Legitimate recipients may be permanently rejected |
| Secret boundary | Map files and path components restrict read access | Database lookup credential is exposed |
Why a catch-all can look convenient and damage operations
A new domain is configured with a catch-all so no customer message is “lost.” Dictionary attacks immediately generate thousands of unique recipients. Postfix accepts every address, filters and mailbox delivery consume resources, and the destination mailbox reaches quota. The team cannot distinguish genuine misspellings from attack traffic and remote senders see delayed responses rather than immediate unknown-user rejection.
The correction provisions explicit addresses and aliases, removes the catch-all after an approved transition, and tests unknown recipients at RCPT time. If a temporary catch-all is unavoidable, it has a domain-specific query, monitored rate, dedicated destination, expiry date and owner. Convenience does not silently become permanent routing policy.
Troubleshooting by symptom
| Symptom | Inspect first | Defensible next action |
|---|---|---|
| Map reports unsupported dictionary type | Package providing mysql support and postconf -m | Install the supported plugin or choose an available backend |
| Every query returns empty | SQL socket/host, credentials, schema, input formatting and active flag | Fix the first incorrect contract without broadening the query |
| Valid RCPT still rejected | Restriction order, domain class and map runtime logs | Trace smtpd decision; do not add client to mynetworks |
| Unknown RCPT is accepted | Recipient map attachment and unlisted-recipient setting | Correct validation before exposing the domain |
| Alias loops | Source/destination graph and expansion limits | Disable the offending alias, preserve evidence and repair provisioning |
Unsafe operations and recovery boundaries
- Unsafe: using an SQL query such as
SELECT 1without an exact address condition accepts every recipient. - Unsafe: placing SQL passwords in world-readable files, command output or source control exposes the identity database.
- Unsafe: adding virtual domains to
mydestinationcan select local system delivery instead of the designed virtual transport. - Unsafe: enabling catch-all globally creates abuse, capacity and backscatter-like operational consequences even when relay policy remains closed.
Rewritten knowledge checks
Cumulative lab checkpoint
- Produce a report proving no accepted domain overlaps Postfix address classes.
- Install the supported MySQL map plugin and create three protected lookup files through the secret process.
- Test active, disabled, absent and alias fixtures with
postmap -q. - Attach maps, validate and reload without broadening
mynetworks. - Run RCPT-only valid, unknown, disabled and third-party relay tests from the client VM.
- Stop MariaDB briefly in the isolated lab, prove temporary behavior, restore it and save the Lesson 5 checkpoint.