#!/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'
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
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 +shortExpected:
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.