Wednesday, 20 May 2026

How to Set Up an AlmaLinux 10 VPS as an Outbound-Only Mail Server with Postfix, DKIM, SPF, DMARC, and rDNS

How to Set Up an AlmaLinux 10 VPS as an Outbound-Only Mail Server with Postfix, DKIM, SPF, DMARC, and rDNS

This guide explains how to configure an AlmaLinux 10 VPS as a standalone outbound-only mail server for sending transactional email from addresses such as:

noreply@example.com
contact@example.com

The server will use:

Postfix    - SMTP server
OpenDKIM   - DKIM signing
SPF        - Sender authorization
DMARC      - Domain policy and reporting
rDNS/PTR   - Reverse DNS identity
TLS        - Encrypted SMTP submission

Example values used in this guide:

Domain:      example.com
Hostname:    mail.example.com
Server IP:   203.0.113.10
Senders:     noreply@example.com, contact@example.com
OS:          AlmaLinux 10

Replace these with your actual values.


1. DNS Records

Create the following DNS records.

A Record

mail.example.com.  3600  IN  A  203.0.113.10

MX Record

Even for an outbound-only server, having an MX record is useful for domain hygiene and bounce handling.

example.com.       3600  IN  MX  10 mail.example.com.

SPF Record

example.com.       3600  IN  TXT "v=spf1 ip4:203.0.113.10 a:mail.example.com -all"

DMARC Record

Start with monitoring:

_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=s; aspf=s"

After verifying clean delivery, move to:

_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; adkim=s; aspf=s"

Eventually:

_dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=s; aspf=s"

PTR / rDNS

PTR records are not configured in your normal DNS zone. They must be set by your VPS provider.

Ask the provider to set:

203.0.113.10 -> mail.example.com

Forward and reverse DNS should match:

mail.example.com -> 203.0.113.10
203.0.113.10    -> mail.example.com

This is critical for deliverability.


2. Set the Server Hostname

hostnamectl set-hostname mail.example.com

Update /etc/hosts:

cat > /etc/hosts <<'EOF'
127.0.0.1 localhost
203.0.113.10 mail.example.com mail
EOF

Verify:

hostname -f

Expected:

mail.example.com

3. Install Required Packages

dnf update -y
dnf install -y epel-release dnf-plugins-core
dnf config-manager --set-enabled crb || true

dnf install -y postfix opendkim opendkim-tools firewalld certbot mailx bind-utils policycoreutils-python-utils cyrus-sasl cyrus-sasl-plain

This setup does not require Dovecot unless you want to host inbound mailboxes.


4. Configure the Firewall

For outbound-only authenticated SMTP, open port 587.

systemctl enable --now firewalld

firewall-cmd --permanent --add-service=submission
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

Used ports:

587  SMTP submission
80   Let's Encrypt HTTP challenge
443  HTTPS / future MTA-STS / optional web services

Port 25 is still required for outbound direct-to-MX delivery. Many VPS providers block outbound port 25, so confirm this later.


5. Get a TLS Certificate

certbot certonly --standalone -d mail.example.com

Certificate paths:

/etc/letsencrypt/live/mail.example.com/fullchain.pem
/etc/letsencrypt/live/mail.example.com/privkey.pem

6. Configure Postfix

Back up the original config:

cp /etc/postfix/main.cf /etc/postfix/main.cf.bak
cp /etc/postfix/master.cf /etc/postfix/master.cf.bak

Replace /etc/postfix/main.cf:

cat > /etc/postfix/main.cf <<'EOF'
# Identity
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain

# IPv4 only
inet_protocols = ipv4
inet_interfaces = all

# Outbound-only domain handling
mydestination = localhost.$mydomain, localhost
relay_domains =

# Local trusted network
mynetworks = 127.0.0.0/8

# Do not be an open relay
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination

# TLS
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1
smtp_tls_loglevel = 1

# SASL authentication
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = cyrus
smtpd_sasl_security_options = noanonymous
broken_sasl_auth_clients = yes

# Sender restrictions
smtpd_sender_login_maps = lmdb:/etc/postfix/sender_login_maps
smtpd_sender_restrictions = reject_sender_login_mismatch, check_sender_access lmdb:/etc/postfix/sender_access, reject

# No local mailbox delivery for example.com
local_recipient_maps =
unknown_local_recipient_reject_code = 550

# Limits
mailbox_size_limit = 0
message_size_limit = 52428800

# DKIM signing
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = inet:127.0.0.1:8891

# AlmaLinux 10 / Postfix LMDB maps
alias_maps = lmdb:/etc/aliases
alias_database = lmdb:/etc/aliases

# Misc
smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
readme_directory = no
compatibility_level = 3.6
EOF

7. Configure Submission Port 587

Enable SMTP submission:

postconf -M submission/inet="submission inet n       -       n       -       -       smtpd"
postconf -P 'submission/inet/syslog_name=postfix/submission'
postconf -P 'submission/inet/smtpd_tls_security_level=encrypt'
postconf -P 'submission/inet/smtpd_sasl_auth_enable=yes'
postconf -P 'submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject'

Important: postconf -P does not accept whitespace inside service override values. Keep complex sender restrictions in main.cf, not in master.cf.


8. Restrict Allowed Sender Addresses

Only allow these sender addresses:

noreply@example.com
contact@example.com

Create /etc/postfix/sender_access:

cat > /etc/postfix/sender_access <<'EOF'
noreply@example.com OK
contact@example.com OK
EOF

postmap lmdb:/etc/postfix/sender_access

Create /etc/postfix/sender_login_maps:

cat > /etc/postfix/sender_login_maps <<'EOF'
noreply@example.com noreply
contact@example.com contact
EOF

postmap lmdb:/etc/postfix/sender_login_maps

This means:

SMTP username noreply -> can send only as noreply@example.com
SMTP username contact -> can send only as contact@example.com

9. Configure SASL Authentication

Enable and start SASL:

systemctl enable --now saslauthd

Create /etc/sasl2/smtpd.conf:

cat > /etc/sasl2/smtpd.conf <<'EOF'
pwcheck_method: saslauthd
mech_list: plain login
EOF

Create system users for SMTP authentication:

useradd -r -s /sbin/nologin noreply
passwd noreply

useradd -r -s /sbin/nologin contact
passwd contact

These are SMTP auth users, not mailbox users.


10. Configure OpenDKIM

Create DKIM key directory:

mkdir -p /etc/opendkim/keys/example.com

Generate DKIM key:

opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s mail

Set permissions:

chown -R opendkim:opendkim /etc/opendkim
chmod 600 /etc/opendkim/keys/example.com/mail.private

Configure /etc/opendkim.conf:

cat > /etc/opendkim.conf <<'EOF'
Syslog                  yes
UMask                   002
Mode                    sv
Canonicalization        relaxed/simple
SubDomains              no
OversignHeaders         From

Socket                  inet:8891@127.0.0.1
PidFile                 /run/opendkim/opendkim.pid
UserID                  opendkim:opendkim

KeyTable                /etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable
ExternalIgnoreList      /etc/opendkim/TrustedHosts
InternalHosts           /etc/opendkim/TrustedHosts
EOF

Create DKIM map files:

cat > /etc/opendkim/KeyTable <<'EOF'
mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private
EOF

cat > /etc/opendkim/SigningTable <<'EOF'
noreply@example.com mail._domainkey.example.com
contact@example.com mail._domainkey.example.com
EOF

cat > /etc/opendkim/TrustedHosts <<'EOF'
127.0.0.1
localhost
mail.example.com
example.com
203.0.113.10
EOF

chown -R opendkim:opendkim /etc/opendkim

11. Add the DKIM Record to DNS

Print the generated DKIM public key:

cat /etc/opendkim/keys/example.com/mail.txt

It will look similar to:

mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
  "p=PUBLIC_KEY_HERE" )

For Cloudflare, create a TXT record like this:

Type: TXT
Name: mail._domainkey
Content: v=DKIM1; k=rsa; p=PUBLIC_KEY_HERE
TTL: Auto
Proxy status: DNS only

Do not include:

IN TXT
( )
quotation marks
comments after semicolons

12. Fix Alias Database Permissions

On AlmaLinux 10/Postfix LMDB setups, generate the aliases database cleanly:

chown root:root /etc/aliases
chmod 644 /etc/aliases

rm -f /etc/aliases.lmdb
postalias lmdb:/etc/aliases

chown root:root /etc/aliases.lmdb
chmod 644 /etc/aliases.lmdb

Then:

newaliases

13. Enable Services

systemctl enable --now postfix
systemctl enable --now opendkim
systemctl enable --now saslauthd

systemctl restart saslauthd postfix opendkim

Validate:

postfix check
postconf -n

Check that port 587 is listening:

ss -tulpn | grep master

Expected:

0.0.0.0:587

14. Test DNS

dig +short A mail.example.com
dig +short MX example.com
dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short TXT mail._domainkey.example.com
dig -x 203.0.113.10 +short

Expected:

mail.example.com resolves to 203.0.113.10
PTR resolves back to mail.example.com
SPF exists
DKIM exists
DMARC exists

Test DKIM:

opendkim-testkey -d example.com -s mail -vvv

Expected result:

key OK

15. Test SMTP Authentication

Install Swaks:

dnf install -y swaks

Test sending as noreply@example.com:

swaks --to recipient@gmail.com \
  --from noreply@example.com \
  --server mail.example.com \
  --port 587 \
  --auth LOGIN \
  --auth-user noreply \
  --auth-password 'YOUR_PASSWORD' \
  --tls

Test sending as contact@example.com:

swaks --to recipient@gmail.com \
  --from contact@example.com \
  --server mail.example.com \
  --port 587 \
  --auth LOGIN \
  --auth-user contact \
  --auth-password 'YOUR_PASSWORD' \
  --tls

This should fail because the sender does not match the authenticated user:

swaks --to recipient@gmail.com \
  --from contact@example.com \
  --server mail.example.com \
  --port 587 \
  --auth LOGIN \
  --auth-user noreply \
  --auth-password 'YOUR_PASSWORD' \
  --tls

16. Check Whether Outbound Port 25 Is Blocked

Direct-to-MX delivery requires outbound TCP port 25.

Test:

nc -vz gmail-smtp-in.l.google.com 25
nc -vz alt1.gmail-smtp-in.l.google.com 25

Also test non-SMTP outbound connectivity:

nc -vz smtp.gmail.com 587
nc -vz google.com 443

If 587 and 443 work but 25 times out, the VPS provider is blocking outbound SMTP.

Example blocked result:

Connection to gmail-smtp-in.l.google.com 25 failed: TIMEOUT
Connected to smtp.gmail.com 587
Connected to google.com 443

In that case, Postfix can accept mail locally or through submission, but it cannot deliver directly to Gmail, Outlook, Yahoo, or other MX servers.


17. Ask the VPS Provider to Unblock Port 25

Send this to the provider:

Please unblock outbound TCP port 25 for VPS IP 203.0.113.10.

Hostname: mail.example.com
PTR/rDNS requested: 203.0.113.10 -> mail.example.com

This server is used only for legitimate transactional outbound mail from:
noreply@example.com
contact@example.com

SPF, DKIM, DMARC, TLS, and sender restrictions are configured.

After the provider unblocks port 25, flush the Postfix queue:

postqueue -f

Watch logs:

journalctl -u postfix -f

18. Alternative: Use an SMTP Relay on Port 587

If the VPS provider refuses to unblock outbound port 25, use a transactional email relay such as:

Amazon SES
Mailgun
Postmark
SMTP2GO
Brevo
SendGrid

Configure Postfix relayhost:

postconf -e 'relayhost = [smtp-relay.example.net]:587'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_tls_security_level = encrypt'
postconf -e 'smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt'

Create credentials:

cat > /etc/postfix/sasl_passwd <<'EOF'
[smtp-relay.example.net]:587 USERNAME:PASSWORD
EOF

postmap lmdb:/etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.lmdb
systemctl reload postfix

Flush the queue:

postqueue -f

19. Queue Management

Show queued mail:

mailq

Retry delivery:

postqueue -f

Delete all queued test emails:

postsuper -d ALL

20. Useful Logs

Postfix logs:

journalctl -u postfix -f

OpenDKIM logs:

journalctl -u opendkim -f

SASL logs:

journalctl -u saslauthd -f

Recent Postfix logs:

journalctl -u postfix -n 100 --no-pager

21. Application SMTP Settings

Use these settings in your application:

SMTP host:      mail.example.com
SMTP port:      587
Security:       STARTTLS
Username:       noreply
Password:       password for noreply user
From address:   noreply@example.com

Or:

SMTP host:      mail.example.com
SMTP port:      587
Security:       STARTTLS
Username:       contact
Password:       password for contact user
From address:   contact@example.com

Do not allow arbitrary sender addresses from applications.


22. Final Checklist

Before production use, confirm:

A record exists for mail.example.com
MX record points to mail.example.com
PTR/rDNS points server IP back to mail.example.com
SPF includes the server IP
DKIM TXT record exists and validates
DMARC record exists
Postfix is not an open relay
Only approved sender addresses are allowed
SMTP submission on port 587 requires authentication
Outbound port 25 is open, or relayhost is configured
TLS certificate is valid
DKIM signing works

Conclusion

A secure outbound-only mail server does not need full mailbox hosting. For transactional sending, the key pieces are:

Postfix for SMTP
OpenDKIM for signing
SPF, DKIM, and DMARC for domain authentication
rDNS/PTR for server identity
Port 587 for authenticated submission
Port 25 outbound for direct MX delivery

The most common blocker is not Postfix. It is VPS providers blocking outbound TCP port 25.

If outbound port 25 is blocked, direct delivery will fail with timeout errors. Either request that the provider unblock port 25, or configure Postfix to use a reputable SMTP relay over port 587.