Saturday, 6 June 2026

How to Set Up a Secure Postfix SMTP Server with DKIM, SPF, DMARC, TLS, and Auth in One Bash Script on Almalinux 10

#!/usr/bin/env bash
# AlmaLinux 10 outbound-only Postfix submission server with Cyrus SASL and DKIM.
# Review the variables below, DNS requirements, and provider port-25 policy first.
set -Eeuo pipefail
umask 077

# Edit these values for each server. PUBLIC_IP must be the server's real static
# public IPv4 address; ACME_EMAIL must be an address you monitor.
DOMAIN="delrobai.com"
MAIL_HOST="mail.delrobai.com"
PUBLIC_IP="1.2.3.4"
SMTP_USER="smtpuser"
FROM_ADDRESS="noreply@delrobai.com"
DKIM_SELECTOR="mail"
ACME_EMAIL="postmaster@delrobai.com"

die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
info() { printf '\n==> %s\n' "$*"; }

[[ $EUID -eq 0 ]] || die 'Run as root: bash almalinux10-mail-setup.sh'
[[ -r /etc/os-release ]] || die 'Cannot identify the operating system.'
# shellcheck disable=SC1091
source /etc/os-release
[[ ${ID:-} == almalinux && ${VERSION_ID:-} == 10* ]] || die 'This script is for AlmaLinux 10.'
[[ -t 0 ]] || die 'Run this script from an interactive terminal; it will prompt for the SMTP password.'

[[ $DOMAIN =~ ^([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+$ ]] || die "Invalid DOMAIN: $DOMAIN"
[[ $MAIL_HOST =~ ^([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+$ ]] || die "Invalid MAIL_HOST: $MAIL_HOST"
[[ $MAIL_HOST == *."$DOMAIN" ]] || die 'MAIL_HOST must be a hostname beneath DOMAIN.'
[[ $SMTP_USER =~ ^[A-Za-z0-9._-]+$ ]] || die 'SMTP_USER may contain only letters, numbers, dot, underscore, and hyphen.'
[[ $DKIM_SELECTOR =~ ^[A-Za-z0-9_-]+$ ]] || die 'Invalid DKIM_SELECTOR.'
[[ $FROM_ADDRESS =~ ^[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)*[A-Za-z0-9-]+$ ]] || die "Invalid FROM_ADDRESS: $FROM_ADDRESS"
[[ ${FROM_ADDRESS##*@} == "$DOMAIN" ]] || die 'FROM_ADDRESS must use DOMAIN.'
[[ $ACME_EMAIL =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]] || die "Set a valid ACME_EMAIL in the script."
[[ $PUBLIC_IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || die "Set PUBLIC_IP to the actual public IPv4 address."
[[ $PUBLIC_IP != 1.2.3.4 ]] || die "Replace the example PUBLIC_IP (1.2.3.4) with the real server public IP."

DKIM_DIR="/etc/opendkim/keys/$DOMAIN"
DKIM_KEY="$DKIM_DIR/$DKIM_SELECTOR.private"
DKIM_TXT="$DKIM_DIR/$DKIM_SELECTOR.txt"
CERT_DIR="/etc/letsencrypt/live/$MAIL_HOST"

info 'Checking DNS before making changes'
command -v dig >/dev/null 2>&1 || { dnf install -y bind-utils; }
if ! dig +short A "$MAIL_HOST" | grep -Fxq "$PUBLIC_IP"; then
  printf 'DNS A lookup for %s returned:\n' "$MAIL_HOST" >&2
  dig +short A "$MAIL_HOST" >&2 || true
  die "Create/update the A record so $MAIL_HOST resolves to $PUBLIC_IP, wait for DNS propagation, then rerun."
fi

info 'Enabling repositories and installing AlmaLinux 10 packages'
dnf install -y epel-release dnf-plugins-core
dnf config-manager --set-enabled crb
dnf clean all
dnf makecache
dnf install -y \
  postfix postfix-lmdb \
  cyrus-sasl cyrus-sasl-lib cyrus-sasl-plain \
  opendkim opendkim-tools \
  certbot firewalld bind-utils policycoreutils-python-utils

postconf -m | grep -Fxq lmdb || die 'Postfix LMDB map support is missing after installing postfix-lmdb.'
command -v opendkim-genkey >/dev/null 2>&1 || die 'opendkim-genkey was not installed.'

info 'Setting the host name and firewall rules'
hostnamectl set-hostname "$MAIL_HOST"
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=submission
firewall-cmd --permanent --add-service=http
firewall-cmd --reload

info "Requesting or reusing the Let's Encrypt certificate"
certbot certonly --standalone --non-interactive --agree-tos \
  --email "$ACME_EMAIL" -d "$MAIL_HOST"
[[ -s $CERT_DIR/fullchain.pem && -s $CERT_DIR/privkey.pem ]] || die 'Certbot did not create the expected certificate files.'

info 'Backing up existing mail configuration'
BACKUP_DIR="/root/mail-setup-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 -p "$BACKUP_DIR"
for f in /etc/postfix/main.cf /etc/postfix/master.cf /etc/opendkim.conf /etc/sasl2/smtpd.conf; do
  if [[ -e $f ]]; then cp -a --parents "$f" "$BACKUP_DIR"; fi
done
printf 'Backups: %s\n' "$BACKUP_DIR"

info 'Configuring Cyrus SASL authentication through saslauthd'
install -d -m 755 /etc/sasl2
cat > /etc/sasl2/smtpd.conf <<EOF
pwcheck_method: saslauthd
mech_list: plain login
EOF
chmod 644 /etc/sasl2/smtpd.conf
if [[ ! -e /etc/sysconfig/saslauthd ]]; then touch /etc/sysconfig/saslauthd; fi
if grep -q '^MECH=' /etc/sysconfig/saslauthd; then
  sed -i 's/^MECH=.*/MECH=shadow/' /etc/sysconfig/saslauthd
else
  printf '\nMECH=shadow\n' >> /etc/sysconfig/saslauthd
fi
if ! id "$SMTP_USER" >/dev/null 2>&1; then
  useradd --system --no-create-home --shell /sbin/nologin "$SMTP_USER"
fi
printf 'Set or update the SMTP password for system account %s.\n' "$SMTP_USER"
passwd "$SMTP_USER"
getent group sasl >/dev/null || die 'The Cyrus SASL group does not exist.'
usermod -aG sasl postfix
systemctl enable --now saslauthd

info 'Creating DKIM key if it does not already exist'
install -d -o opendkim -g opendkim -m 750 "$DKIM_DIR"
if [[ ! -s $DKIM_KEY ]]; then
  opendkim-genkey -b 2048 -d "$DOMAIN" -D "$DKIM_DIR" -s "$DKIM_SELECTOR"
fi
[[ -s $DKIM_KEY && -s $DKIM_TXT ]] || die 'DKIM key generation failed.'
chown -R opendkim:opendkim /etc/opendkim
chmod 600 "$DKIM_KEY"
chmod 644 "$DKIM_TXT"

cat > /etc/opendkim.conf <<EOF
Syslog                  yes
UMask                   007
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
# Sign authenticated SMTP submissions as well as local mail.
MacroList               auth_type
EOF

cat > /etc/opendkim/KeyTable <<EOF
$DKIM_SELECTOR._domainkey.$DOMAIN $DOMAIN:$DKIM_SELECTOR:$DKIM_KEY
EOF
cat > /etc/opendkim/SigningTable <<EOF
$FROM_ADDRESS $DKIM_SELECTOR._domainkey.$DOMAIN
EOF
cat > /etc/opendkim/TrustedHosts <<EOF
127.0.0.1
::1
localhost
EOF
chown opendkim:opendkim /etc/opendkim.conf /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts
chmod 640 /etc/opendkim.conf /etc/opendkim/KeyTable /etc/opendkim/SigningTable /etc/opendkim/TrustedHosts

info 'Configuring Postfix for outbound-only mail and LMDB maps'
postconf -e "myhostname = $MAIL_HOST"
postconf -e "mydomain = $DOMAIN"
postconf -e 'myorigin = $mydomain'
postconf -e 'inet_protocols = ipv4'
postconf -e 'inet_interfaces = all'
postconf -e 'mydestination = localhost.$mydomain, localhost'
postconf -e 'mynetworks = 127.0.0.0/8'
postconf -e 'relay_domains ='
postconf -e 'smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination'
postconf -e "smtpd_tls_cert_file = $CERT_DIR/fullchain.pem"
postconf -e "smtpd_tls_key_file = $CERT_DIR/privkey.pem"
postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtp_tls_security_level = may'
postconf -e 'smtpd_tls_auth_only = yes'
postconf -e 'smtpd_tls_loglevel = 1'
postconf -e 'smtpd_sasl_auth_enable = no'
postconf -e 'smtpd_sasl_type = cyrus'
postconf -e 'smtpd_sasl_path = smtpd'
postconf -e 'smtpd_sasl_local_domain = $mydomain'
postconf -e 'smtpd_sasl_security_options = noanonymous'
postconf -e 'smtpd_sender_login_maps = lmdb:/etc/postfix/sender_login_maps'
postconf -e 'smtpd_sender_restrictions = reject_sender_login_mismatch'
postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination'
postconf -e 'local_recipient_maps ='
postconf -e 'unknown_local_recipient_reject_code = 550'
postconf -e 'mailbox_size_limit = 0'
postconf -e 'message_size_limit = 52428800'
postconf -e 'alias_maps = lmdb:/etc/aliases'
postconf -e 'alias_database = lmdb:/etc/aliases'
postconf -e 'milter_default_action = accept'
postconf -e 'milter_protocol = 6'
postconf -e 'smtpd_milters = inet:127.0.0.1:8891'
postconf -e 'non_smtpd_milters = inet:127.0.0.1:8891'
postconf -e 'smtpd_banner = $myhostname ESMTP'
postconf -e 'biff = no'
postconf -e 'append_dot_mydomain = no'
postconf -e 'compatibility_level = 3.6'

# Permit exactly the configured From address for the configured SMTP login.
printf '%s %s\n' "$FROM_ADDRESS" "$SMTP_USER" > /etc/postfix/sender_login_maps
chown root:root /etc/postfix/sender_login_maps
chmod 600 /etc/postfix/sender_login_maps
postmap lmdb:/etc/postfix/sender_login_maps

# Ensure submission is active and forces STARTTLS plus authentication.
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'
postconf -P 'submission/inet/smtpd_sender_restrictions=reject_sender_login_mismatch'

# Build the aliases database using LMDB, never the unsupported hash backend.
touch /etc/aliases
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

info 'Configuring automatic Postfix reload after certificate renewal'
install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-postfix.sh <<'EOF'
#!/usr/bin/env bash
systemctl reload postfix
EOF
chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-postfix.sh

info 'Starting services and validating configuration'
restorecon -RF /etc/postfix /etc/opendkim /etc/sasl2 /etc/letsencrypt 2>/dev/null || true
systemctl enable --now opendkim
systemctl enable --now postfix
postfix check
systemctl restart opendkim postfix
systemctl reload postfix

if ! ss -ltnH | awk '$4 ~ /:587$/ { found=1 } END { exit !found }'; then
  ss -ltnp >&2
  die 'Postfix is not listening on TCP port 587.'
fi

printf '\nSetup complete. This config is outbound-only; it does not host mailboxes.\n'
printf 'SMTP host:      %s\nSMTP port:      587\nSecurity:       STARTTLS\nSMTP username:  %s\nFrom address:   %s\n' \
  "$MAIL_HOST" "$SMTP_USER" "$FROM_ADDRESS"
printf '\nPublish/check these DNS records before sending production mail:\n'
printf 'A:      %s -> %s\n' "$MAIL_HOST" "$PUBLIC_IP"
printf 'PTR:    ask the VPS provider to map %s -> %s\n' "$PUBLIC_IP" "$MAIL_HOST"
printf 'DKIM:   publish the TXT content from %s\n' "$DKIM_TXT"
printf 'SPF:    merge ip4:%s into the domain\x27s existing SPF record; do not add a second SPF TXT record.\n' "$PUBLIC_IP"
printf 'DMARC:  publish _dmarc.%s with a monitoring policy such as p=none, then review reports.\n' "$DOMAIN"
printf '\nDKIM TXT record file:\n'
cat "$DKIM_TXT"
printf '\nCheck DKIM after publishing DNS: opendkim-testkey -d %q -s %q -vvv\n' "$DOMAIN" "$DKIM_SELECTOR"
printf 'Check logs: journalctl -u postfix -u opendkim -n 100 --no-pager\n'
printf 'Direct delivery requires outbound TCP 25; if your VPS blocks it, configure a relayhost on port 587.\n'

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:   1.2.3.4
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  1.2.3.4

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:1.2.3.4 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:

1.2.3.4 -> mail.example.com

Forward and reverse DNS should match:

mail.example.com -> 1.2.3.4
1.2.3.4    -> 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
1.2.3.4 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
1.2.3.4
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 1.2.3.4 +short

Expected:

mail.example.com resolves to 1.2.3.4
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 1.2.3.4.
Hostname: mail.example.com PTR/rDNS requested: 1.2.3.4 -> 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.