Lesson 005 · Linux MTA Operations Learning Path

Postfix Virtual Domains, Mailboxes, Aliases and SQL Maps

· Published · 10 min read

Labelled Postfix virtual mail flow showing SQL domain mailbox alias and alias-domain lookups recipient validation queue and negative tests

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 -m because 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.

LookupInputExpected result
Virtual domainexample.testA 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 outageAny SQL-backed recipientLookup 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 -l verifies every path component. A secure file is ineffective if directory traversal or group ownership exposes it unexpectedly.
  • Use an SMTP RCPT TO test for active, missing and disabled recipients. Direct postmap queries 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

DecisionChoose deliberatelyEvidence to retain
Domain classesAssign each domain to exactly one Postfix address class.Automated overlap report and effective configuration
Unknown recipientsReject invalid recipients during SMTP after required temporary lookup behavior is verified.Positive, negative and SQL-outage transcripts
Catch-allDefault to no catch-all; approve exceptions with abuse and capacity controls.Named owner, expiry and volume monitoring
Alias expansionLimit recursion/expansion and validate destinations during provisioning.Loop test, expansion limit and audit trail
SQL availabilityChoose 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

LayerQuestion to answerEvidence
Domain classIs this local, virtual, relayed or remote?Disjoint parameter/map membership
Recipient validityDoes the exact active mailbox or alias exist?SQL result and RCPT-stage response
Address rewriteDoes an alias expand to valid bounded destinations?Expansion trace and loop/limit evidence
Final transportWhich service will own mailbox delivery?Transport decision, deferred until LMTP lesson
Dependency stateWas the answer empty or did the lookup fail?postmap exit/log evidence and SMTP 4xx test

Build it step by step

  1. Audit domain classes. Remove overlaps and document the one intended class for every accepted domain.
  2. Protect map files. Install one root-owned SQL configuration for each lookup contract.
  3. Test queries offline. Check active, inactive, absent and database-error results before attaching maps.
  4. Attach maps explicitly. Configure virtual domains, mailboxes and aliases without enabling broad relay trust.
  5. Validate and reload. Run Postfix checks, inspect the effective diff, reload and retain logs.
  6. Exercise SMTP recipient tests. Prove valid acceptance, invalid rejection, relay denial and temporary SQL failure.
  7. 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 ADDRESS with one reviewed lab fixture; never build shell loops from an untrusted address list without quoting and rate control.
  • --quit-after RCPT tests 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

EvidenceHealthy resultFailure meaning
Domain separationNo domain exists in multiple Postfix classesRouting ambiguity or mail loop risk
Valid recipientActive mailbox returns one value and RCPT is acceptedMap, restriction order or fixture is wrong
Invalid recipientEmpty result and permanent RCPT rejection with no queue itemBackscatter and queue abuse risk
SQL outageTemporary RCPT response and dependency alertLegitimate recipients may be permanently rejected
Secret boundaryMap files and path components restrict read accessDatabase 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

SymptomInspect firstDefensible next action
Map reports unsupported dictionary typePackage providing mysql support and postconf -mInstall the supported plugin or choose an available backend
Every query returns emptySQL socket/host, credentials, schema, input formatting and active flagFix the first incorrect contract without broadening the query
Valid RCPT still rejectedRestriction order, domain class and map runtime logsTrace smtpd decision; do not add client to mynetworks
Unknown RCPT is acceptedRecipient map attachment and unlisted-recipient settingCorrect validation before exposing the domain
Alias loopsSource/destination graph and expansion limitsDisable the offending alias, preserve evidence and repair provisioning

Unsafe operations and recovery boundaries

  • Unsafe: using an SQL query such as SELECT 1 without 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 mydestination can 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

What does <code>virtual_mailbox_domains</code> decide?
It identifies domains for which Postfix is final destination using the virtual mailbox address class.
What does <code>virtual_mailbox_maps</code> decide?
It identifies provisioned virtual mailbox recipients and can supply mailbox routing values.
Why reject unknown recipients during SMTP?
The sender receives a direct response and the server avoids queuing undeliverable message content.
How should SQL outage differ from no row?
Outage is a lookup error that should normally produce temporary failure; no row is a valid negative answer.
Can a virtual domain also be in <code>mydestination</code>?
It should not overlap; the classes select different delivery semantics.
Why separate SQL files by lookup?
Each file has one auditable input/output contract and can use only the required query.
What is the main catch-all risk?
It accepts unlimited unprovisioned local parts, increasing abuse, filtering, storage and operational ambiguity.
Does a passing postmap query prove SMTP behavior?
No. Test the smtpd restriction path and reply code with an SMTP client.
Why protect every directory component?
A file mode alone may not prevent unintended traversal, replacement or access through its path.
What remains unfinished after this lesson?
Final mailbox transport, Dovecot identity, TLS, submission AUTH and content/authentication policy are built later.

Cumulative lab checkpoint

  1. Produce a report proving no accepted domain overlaps Postfix address classes.
  2. Install the supported MySQL map plugin and create three protected lookup files through the secret process.
  3. Test active, disabled, absent and alias fixtures with postmap -q.
  4. Attach maps, validate and reload without broadening mynetworks.
  5. Run RCPT-only valid, unknown, disabled and third-party relay tests from the client VM.
  6. Stop MariaDB briefly in the isolated lab, prove temporary behavior, restore it and save the Lesson 5 checkpoint.

Primary references

Advertisement