MariaDB and PostfixAdmin-Style Virtual Mail Database
A virtual-mail database is an authorization system, not just an address book. Postfix asks whether a domain or recipient exists and where an address routes. Dovecot asks whether a user may authenticate and which UID, GID, home and mailbox apply. If every daemon connects as a database administrator, a single configuration disclosure becomes full database compromise. This lesson builds explicit tables and narrow read-only service accounts before either daemon depends on them.
Prerequisites and inherited lab checkpoint
Begin with the Lesson 3 checkpoint: Postfix is reachable only on the intended SMTP path, relay trust is still narrow, and submission/mailbox ports remain closed. Use a separate lab database host where practical. If MariaDB shares the mail host, bind it to a private interface or local socket and document the combined failure boundary.
This course demonstrates a PostfixAdmin-style data model but does not require its web interface. If an administration UI is later installed, patching, TLS, multifactor access, session security and its write-capable database account become separate responsibilities. Mail daemons still receive read-only accounts.
Install and prepare the required components
sudo dnf install -y mariadb-server mariadb
sudo systemctl enable --now mariadb
sudo systemctl status mariadb --no-pager
sudo ss -lntp | grep ':3306 ' || true
sudo mariadb -e 'SELECT VERSION();'- Use supported RHEL modules and repositories. Record the exact MariaDB stream and lifecycle before production adoption; changing a database stream is not an ordinary package update.
- A local Unix-socket administrative login avoids placing a root password in shell history. Remote administration requires a separate named identity and protected network path.
- Do not expose TCP/3306 publicly. Database access belongs on localhost or an explicitly controlled private network with host and network firewall evidence.
- Run the distribution hardening workflow deliberately: remove test objects and remote administrative access after confirming automation and recovery will not depend on them.
Create a normalized schema and separate lookup identities
The following lab schema records active domains, mailboxes and aliases. Production schemas can add quota, transport, ownership and audit fields, but each mail query should return one defined value. Store canonical lowercase domains and addresses at the application boundary so lookup behavior is predictable.
CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE mailserver.virtual_domains (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190) NOT NULL UNIQUE,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE mailserver.virtual_users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
domain_id BIGINT UNSIGNED NOT NULL,
email VARCHAR(320) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
);
CREATE TABLE mailserver.virtual_aliases (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
domain_id BIGINT UNSIGNED NOT NULL,
source VARCHAR(320) NOT NULL,
destination TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE,
INDEX (source),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id)
);
Create two local service accounts with generated unique secrets delivered through a protected secret mechanism. The literal placeholders below must never be deployed:
CREATE USER 'postfix_lookup'@'localhost' IDENTIFIED BY 'REPLACE_WITH_SECRET';
CREATE USER 'dovecot_lookup'@'localhost' IDENTIFIED BY 'REPLACE_WITH_DIFFERENT_SECRET';
GRANT SELECT ON mailserver.virtual_domains TO 'postfix_lookup'@'localhost';
GRANT SELECT ON mailserver.virtual_users TO 'postfix_lookup'@'localhost';
GRANT SELECT ON mailserver.virtual_aliases TO 'postfix_lookup'@'localhost';
GRANT SELECT ON mailserver.virtual_domains TO 'dovecot_lookup'@'localhost';
GRANT SELECT ON mailserver.virtual_users TO 'dovecot_lookup'@'localhost';
FLUSH PRIVILEGES;
Dovecot needs password hashes, not recoverable passwords. Select a scheme supported by the installed Dovecot build, generate hashes through a protected input channel and plan migration away from weak hashes. Never put a password after -p on a shared command line.
| Data object | Mail decision | Integrity rule |
|---|---|---|
| Domain | May this system receive as final destination for the domain? | Unique canonical name and explicit active state |
| Mailbox | Is the exact recipient provisioned and may it authenticate? | Unique address, domain foreign key and strong password hash |
| Alias | Where does this source expand? | Validate every destination; detect loops and expansion limits |
| Lookup account | Which tables and operations may a daemon use? | SELECT only, restricted origin and independently rotated secret |
| Backup | Can identity and mailbox state be restored to one point? | Coordinated database, Maildir, key and configuration checkpoint |
Verify the working path
sudo mariadb -e 'SHOW DATABASES;'
sudo mariadb mailserver -e 'SHOW TABLES;'
sudo mariadb -e "SHOW GRANTS FOR 'postfix_lookup'@'localhost';"
sudo mariadb -e "SHOW GRANTS FOR 'dovecot_lookup'@'localhost';"
sudo mariadb mailserver -e 'CHECK TABLE virtual_domains,virtual_users,virtual_aliases;'
sudo ss -lntp | grep ':3306 ' || true
sudo firewall-cmd --list-all- Grant output must show only the required SELECT privileges. A global grant, GRANT OPTION, FILE or write privilege fails this checkpoint.
- If the database listens on TCP, prove that only the intended private address and clients can reach it. Local-only designs should prefer the Unix socket.
- Insert lab records in a transaction, verify exact and inactive lookups, then remove them or preserve them as the documented next-lesson fixture.
CHECK TABLEis not a backup or restore test. A successful isolated restore is the evidence that recovery material is usable.
Production decisions before continuing
| Decision | Choose deliberately | Evidence to retain |
|---|---|---|
| Database placement | Choose local socket simplicity or a private database tier with explicit availability and latency requirements. | Network diagram, firewall evidence and failure test |
| Schema ownership | Define whether an application, migration tool or DBA changes schema and address data. | Versioned migration and audit record |
| Password scheme | Use a modern scheme supported by Dovecot and plan rehash-on-login or controlled reset. | Scheme inventory and verification test |
| Secret delivery | Use root-readable files or an approved secret system, never article examples or source control. | Mode, owner, rotation record and repository scan |
| Recovery consistency | Coordinate SQL, Maildir, Sieve, configuration and DKIM keys. | Restore runbook and isolated acceptance |
Define database truth without making it a single silent dependency
The database becomes authoritative for virtual recipient and authentication decisions. That makes query correctness, latency and failure semantics part of SMTP availability. A recipient query that fails must not be interpreted as “recipient does not exist” without deliberation; temporary database failure should normally produce a temporary SMTP failure rather than discard or permanently reject legitimate mail.
Postfix and Dovecot do not need the same columns or privileges. Postfix queries return domain existence, recipient existence and alias destinations. Dovecot returns one authentication record and stable mailbox attributes. Avoid SELECT *, write access and shared credentials. Root-owned query files must not be readable by ordinary users.
PostfixAdmin can manage a compatible schema, but the course focuses on the data contract. Before installing any UI, verify its expected schema rather than pointing it at a handcrafted database and hoping versions agree.
Understand the component before configuring it
| Layer | Question to answer | Evidence |
|---|---|---|
| Provisioning | Who creates, disables and audits an address? | Authenticated administration event and database row |
| Postfix lookup | Does the envelope address exist and where does it route? | One-row parameterized SELECT result |
| Dovecot lookup | Does the login verify and which mailbox identity applies? | Password-scheme match plus UID/GID/home result |
| Failure behavior | What happens when SQL is unavailable or ambiguous? | Temporary failure, bounded timeout and alert |
| Recovery | Can database truth be reconciled with mailbox state? | Point-in-time restore and orphan/missing-mailbox report |
Build it step by step
- Classify data. Separate domain, mailbox, alias, quota and authentication responsibilities before creating tables.
- Create constraints. Use uniqueness, foreign keys and explicit active state so invalid relationships fail early.
- Create service identities. Give each daemon only SELECT on its required tables and restrict connection origin.
- Load a reserved fixture. Add one active domain, two active users, one disabled user and one alias for deterministic tests.
- Test both answers. Verify exact success, unknown address, disabled address, ambiguous result and database-unavailable behavior.
- Protect secrets. Deliver generated values outside shell history and source control; set restrictive file ownership later.
- Back up and restore. Restore into an isolated database and compare schema, row counts and test queries.
Operate and inspect the component
sudo mariadb mailserver
sudo mariadb-dump --single-transaction --routines --events mailserver > /root/mailserver-lab.sql
sudo install -d -m 0700 /root/mail-db-restore-test
mysql --protocol=socket -u postfix_lookup -p -e 'SELECT name FROM mailserver.virtual_domains WHERE active=1;'
doveadm pw -l
doveadm pw -s ARGON2ID- The dump path is illustrative. Production backup files require encryption, access control, integrity evidence, off-host retention and restore testing.
- Prompt for service passwords interactively during a lab; do not include them in command arguments, terminal recordings or process listings.
- Confirm the exact password schemes shown by
doveadm pw -l. A hash label unsupported by the deployed Dovecot will cause every login to fail. - A consistent SQL snapshot still needs coordination with mailbox data when recovery objectives require both to represent the same point.
Evidence and acceptance criteria
| Evidence | Healthy result | Failure meaning |
|---|---|---|
| Schema | Constraints reject duplicate domains and orphaned users | Address truth can become ambiguous or inconsistent |
| Privileges | Daemon accounts show SELECT-only grants on named tables | Credential disclosure has excessive impact |
| Lookup tests | Active, disabled and absent fixtures return distinct expected results | Queries cannot enforce provisioning state |
| Network boundary | No public database reachability | Identity data is exposed beyond its trust zone |
| Restore | Isolated schema and fixtures reproduce exact query answers | Backup exists but is not operationally usable |
A database outage must not become a permanent recipient rejection
During maintenance, the mail database stops accepting connections. A poorly written recipient map treats query failure like an empty result and Postfix returns a permanent unknown-user response. Remote senders generate bounces for valid customers. Reconnecting the database cannot recover messages that were never accepted.
The corrected design uses supported Postfix SQL maps whose lookup errors remain errors, bounded connection timeouts and monitoring that distinguishes an unknown recipient from an unavailable lookup. A controlled outage test expects a 4xx temporary response. Once SQL recovers, the same address is accepted and follows the normal delivery path.
Troubleshooting by symptom
| Symptom | Inspect first | Defensible next action |
|---|---|---|
| Access denied | Account origin, socket/TCP path and exact grants | Correct the narrow identity; do not grant global privileges |
| Query returns multiple rows | Uniqueness, joins and canonical address input | Repair schema/data and require a deterministic one-value result |
| All password checks fail | Stored scheme prefix, Dovecot support and query field | Generate one controlled hash and verify without exposing the secret |
| Database is reachable publicly | Bind address, host firewall, network ACL and active interface | Close exposure before continuing and rotate potentially exposed secrets |
| Restore succeeds but mailboxes mismatch | Backup timestamps and provisioning transactions | Reconcile in isolation using documented recovery policy |
Unsafe operations and recovery boundaries
- Unsafe: copying plaintext database credentials from a working server or reference file into course content, Git or chat exposes the entire lookup boundary.
- Unsafe: granting mail daemons administrative or write privileges turns a configuration compromise into data modification or destruction.
- Unsafe: running an unreviewed PostfixAdmin schema installer against an existing database can alter tables expected by live services.
- Unsafe: treating lookup failure as “not found” creates permanent delivery errors during a temporary dependency outage.
Rewritten knowledge checks
Cumulative lab checkpoint
- Install MariaDB and prove it is not reachable from an untrusted network.
- Create the lab schema and constraints using reserved identities.
- Create separate SELECT-only Postfix and Dovecot accounts without recording their secrets.
- Load active, disabled, missing and alias fixtures; document every expected query result.
- Simulate database unavailability and specify the required temporary SMTP behavior for Lesson 5.
- Perform an isolated restore test and retain a secret-free schema, grant and acceptance report as the Lesson 4 checkpoint.