#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# Generic Postfix + SASL + OpenDKIM SMTP Server Setup
# Target OS: RHEL / Rocky Linux / AlmaLinux / CentOS Stream
#
# Example:
# DOMAIN=example.com PUBLIC_IP=1.2.3.4 SMTP_USER=smtpuser bash setup-smtp.sh
#
# Optional:
# HOSTNAME_FQDN=mail.example.com
# FROM_ADDRESS=noreply@example.com
# DKIM_SELECTOR=mail
# CERT_EMAIL=admin@example.com
#
# DNS placeholders used:
# Domain: example.com
# Mail host: mail.example.com
# Public IP: 1.2.3.4
# ============================================================
DOMAIN="${DOMAIN:-example.com}"
PUBLIC_IP="${PUBLIC_IP:-1.2.3.4}"
HOSTNAME_FQDN="${HOSTNAME_FQDN:-mail.${DOMAIN}}"
SMTP_USER="${SMTP_USER:-smtpuser}"
FROM_ADDRESS="${FROM_ADDRESS:-noreply@${DOMAIN}}"
DKIM_SELECTOR="${DKIM_SELECTOR:-mail}"
CERT_EMAIL="${CERT_EMAIL:-}"
POSTFIX_DIR="/etc/postfix"
OPENDKIM_DIR="/etc/opendkim"
DKIM_KEY_DIR="${OPENDKIM_DIR}/keys/${DOMAIN}"
if [[ "$(id -u)" -ne 0 ]]; then
echo "Run as root."
exit 1
fi
echo "============================================================"
echo "SMTP setup starting"
echo "Domain: ${DOMAIN}"
echo "Hostname: ${HOSTNAME_FQDN}"
echo "Public IP: ${PUBLIC_IP}"
echo "SMTP user: ${SMTP_USER}"
echo "From address: ${FROM_ADDRESS}"
echo "DKIM selector: ${DKIM_SELECTOR}"
echo "============================================================"
# ------------------------------------------------------------
# 1. Base packages
# ------------------------------------------------------------
dnf install -y \
postfix \
cyrus-sasl \
cyrus-sasl-lib \
cyrus-sasl-plain \
cyrus-sasl-md5 \
opendkim \
opendkim-tools \
firewalld \
bind-utils \
nmap-ncat \
openssl \
perl \
glibc-langpack-en \
glibc-langpack-pt \
certbot || true
# ------------------------------------------------------------
# 2. Locale cleanup
# ------------------------------------------------------------
localectl set-locale LANG=en_US.UTF-8 || true
export LANG=en_US.UTF-8
unset LC_MEASUREMENT LC_PAPER LC_MONETARY LC_NAME LC_ADDRESS LC_NUMERIC LC_TELEPHONE LC_IDENTIFICATION || true
# ------------------------------------------------------------
# 3. Hostname
# ------------------------------------------------------------
hostnamectl set-hostname "${HOSTNAME_FQDN}"
# ------------------------------------------------------------
# 4. TLS certificate
# ------------------------------------------------------------
CERT_PATH="/etc/letsencrypt/live/${HOSTNAME_FQDN}/fullchain.pem"
KEY_PATH="/etc/letsencrypt/live/${HOSTNAME_FQDN}/privkey.pem"
if command -v certbot >/dev/null 2>&1; then
systemctl stop postfix 2>/dev/null || true
if [[ -n "${CERT_EMAIL}" ]]; then
certbot certonly --standalone \
-d "${HOSTNAME_FQDN}" \
--agree-tos \
-m "${CERT_EMAIL}" \
--non-interactive || true
else
certbot certonly --standalone \
-d "${HOSTNAME_FQDN}" \
--agree-tos \
--register-unsafely-without-email \
--non-interactive || true
fi
fi
if [[ ! -f "${CERT_PATH}" || ! -f "${KEY_PATH}" ]]; then
echo "Let's Encrypt certificate not found. Creating temporary self-signed certificate."
mkdir -p /etc/postfix/tls
CERT_PATH="/etc/postfix/tls/${HOSTNAME_FQDN}.crt"
KEY_PATH="/etc/postfix/tls/${HOSTNAME_FQDN}.key"
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "${KEY_PATH}" \
-out "${CERT_PATH}" \
-days 365 \
-subj "/CN=${HOSTNAME_FQDN}"
chmod 600 "${KEY_PATH}"
fi
# ------------------------------------------------------------
# 5. SASL auth setup
# ------------------------------------------------------------
mkdir -p /etc/sasl2
cat > /etc/sasl2/smtpd.conf <<EOF
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN
EOF
SMTP_PASSWORD="$(openssl rand -base64 36 | tr -d '\n')"
echo "${SMTP_PASSWORD}" | saslpasswd2 -p -c -u "${DOMAIN}" "${SMTP_USER}"
if [[ -f /etc/sasldb2 ]]; then
chown root:postfix /etc/sasldb2 || true
chmod 0640 /etc/sasldb2 || true
fi
# ------------------------------------------------------------
# 6. Postfix sender maps
# ------------------------------------------------------------
cat > "${POSTFIX_DIR}/sender_login_maps" <<EOF
${FROM_ADDRESS} ${SMTP_USER}
EOF
cat > "${POSTFIX_DIR}/sender_access" <<EOF
${FROM_ADDRESS} OK
EOF
chown root:root "${POSTFIX_DIR}/sender_login_maps" "${POSTFIX_DIR}/sender_access"
chmod 0644 "${POSTFIX_DIR}/sender_login_maps" "${POSTFIX_DIR}/sender_access"
postmap hash:"${POSTFIX_DIR}/sender_login_maps"
postmap hash:"${POSTFIX_DIR}/sender_access"
# ------------------------------------------------------------
# 7. Postfix main.cf
# ------------------------------------------------------------
postconf -e "compatibility_level = 2"
postconf -e "myhostname = ${HOSTNAME_FQDN}"
postconf -e "mydomain = ${DOMAIN}"
postconf -e "myorigin = \$mydomain"
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost, \$mydomain"
postconf -e "mynetworks = 127.0.0.0/8"
postconf -e "home_mailbox = Maildir/"
postconf -e "smtpd_banner = \$myhostname ESMTP"
postconf -e "biff = no"
postconf -e "append_dot_mydomain = no"
postconf -e "readme_directory = no"
# TLS
postconf -e "smtpd_tls_cert_file = ${CERT_PATH}"
postconf -e "smtpd_tls_key_file = ${KEY_PATH}"
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"
# SASL
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_sasl_type = cyrus"
postconf -e "smtpd_sasl_path = smtpd"
postconf -e "smtpd_sasl_local_domain = ${DOMAIN}"
postconf -e "smtpd_sasl_security_options = noanonymous"
postconf -e "broken_sasl_auth_clients = yes"
# Relay safety
postconf -e "smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
# Sender login control
postconf -e "smtpd_sender_login_maps = hash:${POSTFIX_DIR}/sender_login_maps"
postconf -e "smtpd_sender_restrictions = reject_sender_login_mismatch, check_sender_access hash:${POSTFIX_DIR}/sender_access"
# OpenDKIM milter
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"
# ------------------------------------------------------------
# 8. Postfix master.cf submission service
# ------------------------------------------------------------
cp -a "${POSTFIX_DIR}/master.cf" "${POSTFIX_DIR}/master.cf.bak.$(date +%Y%m%d%H%M%S)"
awk '
BEGIN { skip=0 }
/^submission[[:space:]]+inet/ { skip=1; next }
skip==1 && /^[[:space:]]+-o/ { next }
skip==1 && /^[^[:space:]#]/ { skip=0 }
skip==0 { print }
' "${POSTFIX_DIR}/master.cf" > "${POSTFIX_DIR}/master.cf.new"
cat >> "${POSTFIX_DIR}/master.cf.new" <<'EOF'
submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination
EOF
mv "${POSTFIX_DIR}/master.cf.new" "${POSTFIX_DIR}/master.cf"
# ------------------------------------------------------------
# 9. OpenDKIM setup
# ------------------------------------------------------------
mkdir -p "${DKIM_KEY_DIR}"
if [[ ! -f "${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private" ]]; then
opendkim-genkey \
-b 2048 \
-d "${DOMAIN}" \
-s "${DKIM_SELECTOR}" \
-D "${DKIM_KEY_DIR}"
fi
chown -R opendkim:opendkim "${OPENDKIM_DIR}/keys"
chmod 0700 "${DKIM_KEY_DIR}"
chmod 0600 "${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private"
cat > "${OPENDKIM_DIR}/KeyTable" <<EOF
${DKIM_SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${DKIM_SELECTOR}:${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private
EOF
cat > "${OPENDKIM_DIR}/SigningTable" <<EOF
*@${DOMAIN} ${DKIM_SELECTOR}._domainkey.${DOMAIN}
EOF
cat > "${OPENDKIM_DIR}/TrustedHosts" <<EOF
127.0.0.1
localhost
${HOSTNAME_FQDN}
${DOMAIN}
${PUBLIC_IP}
EOF
cp -a /etc/opendkim.conf /etc/opendkim.conf.bak.$(date +%Y%m%d%H%M%S) 2>/dev/null || true
cat > /etc/opendkim.conf <<EOF
Syslog yes
SyslogSuccess yes
LogWhy yes
UMask 002
Canonicalization relaxed/simple
Mode sv
SubDomains no
AutoRestart yes
AutoRestartRate 10/1h
Background yes
DNSTimeout 5
SignatureAlgorithm rsa-sha256
UserID opendkim:opendkim
Socket inet:8891@127.0.0.1
PidFile /run/opendkim/opendkim.pid
KeyTable refile:${OPENDKIM_DIR}/KeyTable
SigningTable refile:${OPENDKIM_DIR}/SigningTable
ExternalIgnoreList refile:${OPENDKIM_DIR}/TrustedHosts
InternalHosts refile:${OPENDKIM_DIR}/TrustedHosts
EOF
# ------------------------------------------------------------
# 10. Firewall
# ------------------------------------------------------------
systemctl enable --now firewalld
firewall-cmd --permanent --add-port=25/tcp
firewall-cmd --permanent --add-port=587/tcp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --reload
# ------------------------------------------------------------
# 11. Enable services
# ------------------------------------------------------------
postfix check
systemctl enable --now opendkim
systemctl restart opendkim
systemctl enable --now postfix
systemctl restart postfix
# ------------------------------------------------------------
# 12. Output DNS records and test commands
# ------------------------------------------------------------
DKIM_TXT_FILE="${DKIM_KEY_DIR}/${DKIM_SELECTOR}.txt"
echo
echo "============================================================"
echo "SETUP COMPLETE"
echo "============================================================"
echo
echo "SMTP credentials:"
echo "SMTP server: ${HOSTNAME_FQDN}"
echo "SMTP port: 587"
echo "SMTP user: ${SMTP_USER}"
echo "SMTP password: ${SMTP_PASSWORD}"
echo "From address: ${FROM_ADDRESS}"
echo
echo "IMPORTANT: save this password now. It is not stored in this script output again."
echo
echo "============================================================"
echo "CLOUDFLARE DNS RECORDS"
echo "============================================================"
echo
echo "A record:"
echo "Type: A"
echo "Name: mail"
echo "Content: ${PUBLIC_IP}"
echo "Proxy: DNS only"
echo
echo "MX record:"
echo "Type: MX"
echo "Name: ${DOMAIN}"
echo "Mail server: ${HOSTNAME_FQDN}"
echo "Priority: 10"
echo
echo "SPF TXT record:"
echo "Type: TXT"
echo "Name: ${DOMAIN}"
echo "Content: v=spf1 ip4:${PUBLIC_IP} mx ~all"
echo
echo "DMARC TXT record:"
echo "Type: TXT"
echo "Name: _dmarc"
echo "Content: v=DMARC1; p=none; rua=mailto:${FROM_ADDRESS}; fo=1"
echo
echo "DKIM TXT record:"
echo "Type: TXT"
echo "Name: ${DKIM_SELECTOR}._domainkey"
echo "Content:"
echo
if [[ -f "${DKIM_TXT_FILE}" ]]; then
sed -E 's/.*TXT[[:space:]]+\( //' "${DKIM_TXT_FILE}" \
| tr -d '\n' \
| sed -E 's/[[:space:]]*"//g; s/"[[:space:]]*//g; s/\)[[:space:]]*;.*$//'
echo
else
echo "DKIM TXT file not found at ${DKIM_TXT_FILE}"
fi
echo
echo "============================================================"
echo "VALIDATION COMMANDS"
echo "============================================================"
echo
echo "Check Postfix listens:"
echo " ss -lntp | grep -E ':25|:587'"
echo
echo "Check supported Postfix map types:"
echo " postconf -m | grep -E 'hash|lmdb'"
echo
echo "Check active Postfix config:"
echo " postconf -n | grep -E 'smtpd_relay_restrictions|smtpd_recipient_restrictions|smtpd_sender_restrictions|smtpd_sender_login_maps|smtpd_sasl_auth_enable'"
echo
echo "Check DKIM key:"
echo " opendkim-testkey -d ${DOMAIN} -s ${DKIM_SELECTOR} -vvv"
echo
echo "Check DNS:"
echo " dig +short A ${HOSTNAME_FQDN}"
echo " dig +short MX ${DOMAIN}"
echo " dig +short TXT ${DOMAIN}"
echo " dig +short TXT _dmarc.${DOMAIN}"
echo " dig +short TXT ${DKIM_SELECTOR}._domainkey.${DOMAIN}"
echo
echo "Test SMTP submission:"
echo " swaks --to recipient@gmail.com \\"
echo " --from ${FROM_ADDRESS} \\"
echo " --server ${HOSTNAME_FQDN} \\"
echo " --port 587 \\"
echo " --auth LOGIN \\"
echo " --auth-user ${SMTP_USER} \\"
echo " --auth-password '${SMTP_PASSWORD}' \\"
echo " --tls"
echo
echo "Check delivery logs:"
echo " tail -n 100 /var/log/maillog"
echo
echo "If publishing this script, replace real domains, IPs, usernames, and passwords with placeholders."
echo "============================================================"
Saturday, 6 June 2026
How to Set Up a Secure Postfix SMTP Server with DKIM, SPF, DMARC, TLS, and Auth in One Bash Script
Subscribe to:
Posts (Atom)