适用场景
SMTP 开放中继(Open Relay)指邮件服务器在未做身份校验的前提下,允许任意来源的客户端把邮件转发到任意第三方域。一旦暴露在公网,通常在数小时内就会被垃圾邮件团伙扫描并接管,后果包括服务器 IP 被列入 Spamhaus、Barracuda 等黑名单,正常业务邮件(订单通知、验证码)全部退信,云厂商还会因滥用封停 25 端口甚至整台实例。
本文针对三类场景给出完整的加固流程:自建邮件服务器(Postfix)的站长;用 VPS 跑网站通知邮件、此前误开中继的主机使用者;以及被第三方通报“IP 在发垃圾邮件”后需要紧急止损的运维人员。
前置条件
- 系统:CentOS 7+/Rocky/AlmaLinux/Ubuntu 20.04 及以上,Postfix 3.4 以上。
- 权限:root 或具备 sudo 权限的账号。
- 端口:能访问本机 25、587 端口;如计划只做“认证发信”,需要提前确认业务是否依赖 25 端口投递。
- 工具:
postconf(Postfix 自带)、swaks或nc用于验证,openssl用于测试 STARTTLS。 - 业务确认:列出所有会调用本机发信的来源,包括 PHP-FPM、Java 应用、cron 脚本、监控告警、WordPress 站点等。
原理说明
Postfix 的转发决策由两个参数决定:smtpd_relay_restrictions(是否允许中继到外部域)和 mynetworks(被信任的客户端网段)。默认安装下 Postfix 只信任 127.0.0.0/8 与 ::1,因此默认是安全的。绝大多数开放中继都来自两类误配置。
第一类是 mynetworks 被写成 0.0.0.0/0,或为了“让内网都能发信”直接写入过大的网段,导致任意来源都被视为可信客户端。第二类是把 permit_mynetworks 放在了 smtpd_relay_restrictions 的第一位,并且规则里缺少 reject_unauth_destination,检查链在遇到第一个 permit 后就短路返回,后面的拒绝规则形同虚设。
正确的模型是“默认拒绝 + 白名单放行”:先拒绝所有未认证的目标域,再允许本机(127.0.0.1)与已通过 SASL 认证的账号发信,最后对收件人数量、连接速率、消息大小做资源约束。这一模型对应后文的全部配置项。
操作步骤
步骤 1:备份现状并导出参数
cp -a /etc/postfix/main.cf /etc/postfix/main.cf.bak.$(date +%F)
postconf -n > /root/postconf-before-$(date +%F).txt
postconf -d mynetworks
grep -nE "mynetworks|relay_restrictions|recipient_restrictions" /etc/postfix/main.cf
把 postconf -n 的输出留存,出问题时可以逐项比对回滚。
步骤 2:收敛 mynetworks
postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'
postconf -e 'mynetworks_style = host'
mynetworks 只保留本机回环。如果确有内网应用服务器需要免认证发信,请改用 SASL 认证(步骤 4),或单独把该服务器 IP 写成 /32,不要写整个网段,更不要写 0.0.0.0/0。
步骤 3:重写中继与收件人限制
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, reject_non_fqdn_recipient, reject_unknown_recipient_domain'
postconf -e 'smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain'
postconf -e 'smtpd_helo_required = yes'
postconf -e 'smtpd_helo_restrictions = reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname'
关键字眼是 reject_unauth_destination:只要收件人域名不属于本机 mydestination、virtual_* 或 relay_domains,且来源既不在 mynetworks 也未通过 SASL 认证,连接会在 RCPT 阶段被直接拒绝,返回 554 Relay access denied。
同时把 permit_mynetworks 放在第一位是有意为之:本机应用(PHP mail、cron 告警)走 127.0.0.1 时必须放行,此时来源本身可信,无需再走后续规则。公网来源没有 SASL 凭据,因此一定会落到 reject_unauth_destination。
步骤 4:启用 SASL 认证,让外发账号可发信
# 安装 Cyrus SASL
yum install -y cyrus-sasl cyrus-sasl-plain cyrus-sasl-md5 # Debian/Ubuntu: apt install sasl2-bin
# 声明要在认证后才提供的服务,25 端口不提供 AUTH
postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_sasl_type = cyrus'
postconf -e 'smtpd_sasl_local_domain = $myhostname'
postconf -e 'smtpd_sasl_security_options = noanonymous'
postconf -e 'smtpd_sasl_tls_security_options = noanonymous'
postconf -e 'smtpd_tls_auth_only = yes'
postconf -e 'smtpd_sasl_path = smtpd'
postconf -e 'broken_sasl_auth_clients = no'
# 定义 AUTH 出现的位置:仅在 587/465
postconf -e 'smtpd_sasl_authenticated_header = yes'
postconf -e 'permit_sasl_authenticated' 2>/dev/null
# master.cf 中为 587 端口打开 AUTH,为 25 端口关闭
sed -i 's/^smtp inet n - y - - smtpd$/smtp inet n - y - - smtpd\n -o smtpd_sasl_auth_enable=no\n -o smtpd_tls_auth_only=yes\n -o smtpd_client_restrictions=permit_mynetworks,reject/' /etc/postfix/master.cf
smtpd_tls_auth_only = yes 保证 AUTH 凭据只在加密通道内传输,避免明文口令被嗅探。25 端口关闭 AUTH 可以显著降低被暴力猜解的风险。
步骤 5:限制资源占用,抑制被滥用后的损失
postconf -e 'smtpd_client_connection_rate_limit = 30'
postconf -e 'smtpd_client_message_rate_limit = 60'
postconf -e 'smtpd_client_recipient_rate_limit = 200'
postconf -e 'smtpd_client_connection_count_limit = 10'
postconf -e 'smtpd_recipient_limit = 50'
postconf -e 'smtpd_timeout = 300s'
postconf -e 'message_size_limit = 20971520'
postconf -e 'mailbox_size_limit = 0'
postconf -e 'default_destination_recipient_limit = 25'
这些阈值由 Postfix 的 anvil 服务统计,smtpd_client_message_rate_limit 与 smtpd_client_recipient_rate_limit 是最关键的两条:即使某个账号口令泄露,攻击者也无法一次性投递上万封收件人,能在被拉黑前争取到处置窗口。
步骤 6:强制 TLS 并关闭无用监听
postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
postconf -e 'smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem'
postconf -e 'smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem'
postconf -e 'smtpd_tls_mandatory_ciphers = high'
postconf -e 'inet_interfaces = all'
postconf -e 'inet_protocols = ipv4'
systemctl reload postfix
postfix check
postfix check 会做语法自检。若输出为空说明配置无误,随后执行 reload 使新策略生效。
步骤 7:清理队列中的滞留垃圾邮件
mailq | tail -3 # 查看队列长度
postqueue -p | grep -c '^[0-9A-F]' # 统计队列内邮件数
postsuper -d ALL deferred # 清空延迟队列(确认无误后再执行)
postsuper -d ALL hold
systemctl restart postfix
配置验证
第一步,确认参数已生效:
postconf -n | grep -E 'mynetworks|relay_restrictions|recipient_restrictions|rate_limit'
应看到 mynetworks = 127.0.0.0/8, [::1]/128,且限制串中包含 reject_unauth_destination。
第二步,从外部主机模拟未认证中继。以下会话应当在中途被拒绝:
nc mail.example.com 25
EHLO test.local
MAIL FROM:<spam@test.local>
RCPT TO:<victim@gmail.com>
# 期望输出: 554 5.7.1 <victim@gmail.com>: Relay access denied
第三步,用 swaks 做完整的一轮对照测试:
# 不认证直投外部域 —— 应失败
swaks --to someone@gmail.com --from me@test.local --server mail.example.com --quit-after RCPT
# 认证后发信 —— 应成功进入队列
swaks --to someone@outlook.com --from postmaster@example.com \
--server mail.example.com --auth LOGIN --auth-user no-reply@example.com \
--auth-password '你的口令' --tls
第四步,观察日志确认拒绝原因:
tail -f /var/log/maillog | grep -E 'Relay access denied|SASL|554'
# Debian/Ubuntu
journalctl -u postfix -f
第五步,做一次外网自查。从一台与服务器无关的公网主机执行上面的 nc 会话,若能成功投递说明策略仍未生效,需要回头检查 master.cf 中 25 端口是否被按步骤 4 覆盖、以及是否有第二个 Postfix 配置文件被加载。
常见问题
Q1:加固后本机 PHP/WordPress 发信失败,日志报 Relay access denied
说明发信来源不是 127.0.0.1。检查 PHP 的 sendmail_path 与容器网络模式:若站点运行在 Docker 容器内,容器发出的包源地址是网桥网段(如 172.17.0.2),不在 mynetworks 中。两种做法:推荐让容器通过宿主机做中继,把宿主机容器网桥网段单独加入白名单并限 /24;更安全的做法是在容器内用 SASL 认证发信,密码通过环境变量注入。
Q2:我想让服务器接收自己的多个域邮件,会不会被 reject_unauth_destination 拦掉
不会。reject_unauth_destination 的判定依据是收件人域是否在本机“负责投递”的清单内,即 mydestination、virtual_mailbox_domains、virtual_alias_domains、relay_domains 四组参数。把这些域写进 mydestination(本地 Unix 用户投递)或 virtual_mailbox_domains(虚拟用户投递)即可。注意不要图省事写通配符,也不要留下已经停用的历史域名,否则会形成新的开放中继面。
Q3:已经被当成中继滥用了,如何紧急止损
按顺序执行四步:一是立刻把 mynetworks 收敛为回环并 reload,切断滥用链路;二是 postsuper -d ALL 清空队列,避免继续外发被拉黑;三是排查口令泄露来源,检查 /var/log/secure 中的登录记录与应用日志,重置所有发信账号口令;四是向 Spamhaus 提交 delist 申请(https://check.spamhaus.org/),并在服务器上补齐步骤 5 的速率限制,防止二次复发。
Q4:25 端口被云厂商封禁,怎么发信
这是云平台防滥用的常规策略。可选方案:改用服务商提供的 587 提交端口配合 SASL 认证;或接入第三方发信服务(阿里云邮件推送、腾讯云 SES 等),在 Postfix 中通过 relayhost 转发并配置 SASL 凭据;若必须自建,需向云厂商提交工单申请解封 25 端口,通常要求提供 PTR 记录、SPF/DKIM 配置和退订机制证明。
总结
- 开放中继的根因集中在 mynetworks 过大 和 限制规则缺少 reject_unauth_destination 两点,加固的第一步永远是
postconf -n导出实测参数,而不是凭截图判断。 - 安全模型是“默认拒绝 + 白名单放行”:回环与 SASL 认证视为可信来源,其余一律拒绝,配合速率限制把单点失陷的影响控制在可处置范围内。
- 验证必须来自外部主机。在本机测试永远看不到真实的中继行为,用 nc 手写 RCPT 是成本最低、结论最明确的方式。
- 加固不是一次性动作。把
postconf -n的输出纳入变更基线,每次上线新业务后重新跑一次 nc 验证,才能长期守住这条防线。
配套建议:邮件安全是分层工程,除本文的服务器侧加固外,还应在 DNS 侧配置 SPF、DKIM、DMARC 三条记录,形成“服务器拒绝转发 + 域验证发信来源”的双重约束。