Install Postfix on RHEL 9 or 10 and Build a Safe SMTP Baseline
The first Postfix milestone is intentionally small: a packaged, patched service that accepts only controlled local test mail and whose configuration can be explained. Public exposure, virtual domains, SASL and milters come later. This prevents a half-built server from becoming an open relay while the operator learns how the master process, smtpd, cleanup, queue manager and delivery agents cooperate.
Prerequisites and inherited lab checkpoint
Complete Lesson 1 and retain its architecture contract, relay matrix and baseline listeners. The host must have a stable name, synchronized clock, approved repositories, SELinux enforcing and console access. Use mail1.example.test; do not substitute a public domain until the full policy and abuse controls are ready.
If Sendmail is present on RHEL 9, identify why before changing the system MTA. RHEL 10 removes Sendmail from the distribution and recommends Postfix. A service replacement requires an application inventory because local jobs may depend on the sendmail command interface even when no network listener is intended.
Install and prepare the required components
sudo dnf info postfix
sudo dnf install -y postfix s-nail
rpm -q postfix s-nail
rpm -V postfix
postconf mail_version
postconf -m
sudo systemctl enable --now postfix- Install from approved RHEL repositories and retain package versions. Do not compile a random Internet build to obtain one feature without accepting its patch and support ownership.
postconf -mlists map types supported by the installed build. RHEL 10 removed Berkeley DB support and uses LMDB by default, so do not copyhash:assumptions from another platform.s-nailprovides a simple local test client. It does not replace an SMTP transcript for network policy testing.- Starting the package before firewall exposure is acceptable only because the lesson keeps the service local and verifies
inet_interfacesbefore opening a zone.
Create a minimal local-only main.cf baseline
Back up configuration with metadata, generate a parameter inventory, and use postconf -e for the small initial baseline. The package file contains many documented defaults; acceptance should be based on postconf -n, which shows non-default settings actually interpreted by Postfix.
sudo install -d -m 0700 /root/mta-change-backups/lesson-02
sudo cp -a /etc/postfix/main.cf /etc/postfix/master.cf /root/mta-change-backups/lesson-02/
sudo postconf -e 'myhostname = mail1.example.test'
sudo postconf -e 'mydomain = example.test'
sudo postconf -e 'myorigin = $myhostname'
sudo postconf -e 'inet_interfaces = loopback-only'
sudo postconf -e 'inet_protocols = all'
sudo postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost'
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'
sudo postconf -e 'relay_domains ='
sudo postconf -e 'smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination'
sudo postfix check
sudo systemctl reload postfixmynetworks is deliberately loopback-only. Do not insert a whole office, cloud or container subnet merely because clients receive “relay denied.” Those clients need an authenticated submission design or a narrowly controlled application relay path.
| File or command | Responsibility | Operator rule |
|---|---|---|
/etc/postfix/main.cf | Global and service-default parameters | Change one reviewed behavior at a time; validate before reload |
/etc/postfix/master.cf | Services, listeners and per-service overrides | Keep submission-specific AUTH and TLS policy here |
postconf -n | Effective non-default configuration | Capture before and after every change |
postfix check | Basic configuration and permission checks | Required but not an end-to-end acceptance test |
| Mail log and queue ID | Runtime decision path | Correlate behavior; do not infer success from active service state |
Verify the working path
sudo postfix check
sudo postconf -n
sudo postconf -M
sudo ss -lntp | grep ':25 '
sudo systemctl status postfix --no-pager
printf 'Lesson 2 local delivery test\n' | mail -s 'Postfix baseline' root
sudo postqueue -p
sudo journalctl -u postfix --since '-10 minutes' --no-pager- The listener must be bound only to loopback at this checkpoint. If it is reachable on a public address, stop and correct
inet_interfaces. postfix checkcan detect some syntax, ownership and directory problems. It cannot prove relay restrictions, DNS behavior or successful mailbox access.- The local message should produce a queue ID and a terminal delivery status. On current RHEL logging may be in the journal or a configured mail log; identify the authoritative source.
- An empty queue after a test is meaningful only when logs show why the message left it. It could have delivered, bounced or been removed.
Production decisions before continuing
| Decision | Choose deliberately | Evidence to retain |
|---|---|---|
| Package source | Use supported RHEL packages unless a documented requirement and patch owner justify another source. | Repository, version and update owner |
| Network exposure | Remain loopback-only until DNS, firewall, recipient and relay policies are built. | Socket and external negative connection test |
| Protocol family | Use both IPv4 and IPv6 only when DNS, routing, firewall and reputation are managed for both. | A/AAAA, PTR and route evidence |
| Local destination | Keep system-host delivery separate from later virtual mailbox domains. | Explicit mydestination and virtual domain inventory |
| Configuration deployment | Use versioned, secret-safe configuration and staged validation. | Diff, syntax output, change record and rollback copy |
Understand what the packaged service starts
The Postfix master process reads master.cf and starts restricted services as needed. An inbound smtpd process handles the SMTP conversation. The cleanup service normalizes content and creates a queue file; the queue manager schedules delivery; delivery agents such as smtp, local or later Dovecot LMTP handle the next hop. Process separation limits privilege and failure scope, but unsafe policy can still make the whole service relay mail incorrectly.
main.cf parameters can expand other parameter values. Quote shell commands so Bash does not expand Postfix dollar expressions before writing them. Always inspect postconf -n after scripted changes. A copied configuration can also reference unavailable map types, paths or services, which is why capability discovery precedes activation.
The baseline does not claim production readiness. It creates a known-good control point for later lessons. Public port 25, virtual recipients, TLS identities, authentication and filtering each receive separate positive and negative tests.
Understand the component before configuring it
| Layer | Question to answer | Evidence |
|---|---|---|
| master | Which services may start and with which overrides? | postconf -M, process list and listeners |
| smtpd | Which SMTP commands and envelopes are accepted? | Transcript, restriction decision and queue ID |
| cleanup | How is a message prepared for the queue? | Cleanup log and queue file creation |
| qmgr | Which delivery is scheduled next? | Active/deferred queue and delivery log |
| delivery agent | Was the next hop local, SMTP or another transport? | Agent-specific status and enhanced code |
Build it step by step
- Inventory first. Record release, package sources, current MTA alternative, listeners and application dependencies.
- Install supported packages. Verify package ownership and supported map types before referencing paths or databases.
- Back up with permissions. Keep the original main and master configuration in a root-only checkpoint.
- Set identity and boundary. Use reserved lab names, loopback listeners and explicit trusted networks.
- Validate offline. Run
postfix checkand inspect the effective diff before reload. - Test locally. Submit one identifiable message, retain its queue ID and follow its terminal state.
- Prove denial by design. Confirm there is no external listener and no broadened relay trust before continuing.
Operate and inspect the component
postconf -d myhostname mydestination mynetworks
postconf -h myhostname
postconf -M
postconf -F
postconf -m
postfix status
postqueue -p
postcat -q QUEUE_ID
journalctl -u postfix --since '-15 minutes' --no-pagerpostconf -dshows defaults;postconf -nshows explicitly set non-defaults;postconf -hprints a value without the parameter name.postcatcan expose full headers and body. Use it only for an authorized queue ID and redact evidence before sharing.- Never paste a placeholder such as
QUEUE_IDinto a destructive queue command. Resolve and review the exact identifier first. - Use a reload for validated configuration that supports it. Restart only when required and after understanding connection and queue impact.
Evidence and acceptance criteria
| Evidence | Healthy result | Failure meaning |
|---|---|---|
| Packages | Expected signed RHEL package and version installed | Unsupported source or incomplete transaction |
| Effective configuration | Only reviewed non-default parameters appear | Hidden drift or copied configuration remains |
| Listener | TCP/25 is loopback-only | Premature exposure or wrong service owns the port |
| Local test | Queue ID reaches a documented terminal result | Submission, queue or local delivery is unresolved |
| Relay boundary | Only loopback is trusted and no external listener exists | The baseline may relay or expose unfinished policy |
A service can be active and still be wrong
An operator installs Postfix and sees active (running), then opens port 25. A copied main.cf trusts an entire private cloud network and declares a business domain in both local and virtual destination classes. Some messages relay unexpectedly while others loop. The process health check never detects either policy error.
The controlled recovery closes external exposure, restores the reviewed loopback baseline, runs postfix check, compares postconf -n, and sends one local trace message. Domain classes and relay trust are then introduced in their dedicated lessons with negative tests. Service state becomes one piece of evidence, not the definition of health.
Troubleshooting by symptom
| Symptom | Inspect first | Defensible next action |
|---|---|---|
| Postfix fails to start | postfix check, journal and port ownership | Correct syntax, permissions or conflict; do not disable SELinux |
| Wrong process owns port 25 | ss -lntp, unit state and MTA inventory | Resolve the approved MTA transition before killing processes |
| Local test remains queued | Queue record, next-hop agent and terminal log line | Repair the responsible delivery layer and retry one item |
| Configuration value looks altered | Shell expansion, postconf -n and backup diff | Restore the intended literal value and use safe quoting |
| Copied map reports unsupported type | postconf -m and RHEL release | Convert to a supported map such as LMDB and rebuild it |
Unsafe operations and recovery boundaries
- Unsafe: broadening
mynetworksto silence relay errors can authorize unknown workloads to send through the host. - Unsafe: using
postfix start-fgor manual daemons alongside systemd can create conflicting process ownership and misleading state. - Unsafe: replacing configuration without preserving ownership, mode and a validated rollback can prevent both startup and recovery.
- Unsafe: reading arbitrary queued mail with
postcatcan disclose customer content. Access requires an operational reason and controlled evidence handling.
Rewritten knowledge checks
Cumulative lab checkpoint
- Capture the pre-install MTA, listener, package and system baseline.
- Install Postfix and s-nail from approved repositories; record versions and supported map types.
- Back up configuration and apply the local-only baseline exactly once.
- Validate, reload and prove TCP/25 binds only to loopback.
- Submit a uniquely labelled local message and correlate its queue ID through the journal.
- Compare effective configuration to the architecture contract and preserve the checkpoint for Lesson 3.