背景

在多台服务器和个人设备组成的运维环境中,系统通知邮件是一项非常基础但重要的能力。备份任务、定时任务、系统服务、自动化脚本和监控程序,都可能需要在异常发生时主动发出通知。

已有几台系统已经具备稳定的本机邮件发送能力。这些系统并不直接对外提供邮件服务,也不负责接收邮件,而是通过本机邮件程序把通知交给 Postfix,再由 Postfix 中继到外部 SMTP 服务器。新的边缘节点也需要具备同样的能力,以便后续承载系统通知、自动化任务提醒和本机代理程序通知。

目标不是搭建完整邮件服务器,而是搭建一个安全、轻量、统一的本机发信环境。

目标设计

最终目标如下:

本机 root / cron / systemd / 脚本 / 自动化代理
        ↓
mail 或 sendmail
        ↓
本机 Postfix
        ↓
外部 SMTP 中继服务器
        ↓
管理员邮箱

该方案具备几个特点:

  • 本机只负责发信,不负责收信。
  • Postfix 只监听本机回环地址。
  • 不开放公网 SMTP 入站端口。
  • 所有系统通知统一走同一条邮件出口。
  • 发件账号使用专门的通知邮箱。
  • 管理员邮箱作为最终接收地址。
  • 配置结构尽量与已有服务器保持一致。

既有系统方案盘点

在正式搭建新节点之前,先对已有系统进行了只读检查。检查对象包括两类服务器节点和一台桌面工作站。

检查重点包括:

  • 当前系统发行版和包管理器。
  • 是否安装 Postfix、mailx、msmtp 等邮件相关组件。
  • sendmailmailmailx 命令实际指向哪里。
  • Postfix 是否启用外部 SMTP 中继。
  • 是否配置了 SMTP 认证。
  • 是否使用地址重写。
  • 是否存在 root 邮件转发。
  • 是否已经有成功发信日志。
  • 是否有收信服务或对外监听 SMTP 端口。

盘点结果显示,已有几台系统虽然发行版不同,但实际发信路径已经收敛到同一种思路:

本机 Postfix
+
外部 SMTP 中继
+
SMTP 认证
+
TLS 加密
+
地址重写
+
root 别名转发

其中,Debian/Ubuntu 系系统使用 hash:/etc/postfix/... 类型的 Postfix 映射文件;Arch 系系统则使用 lmdb:/etc/postfix/... 类型的映射文件。这一点非常重要,因为不同发行版的 Postfix 默认映射类型可能不同,不能直接照抄。

新节点是 Ubuntu 系统,因此更适合复刻 Debian/Ubuntu 系的 Postfix hash 方案,而不是照搬 Arch 系的 lmdb 方案。

为什么选择 Postfix,而不是 msmtp

轻量脚本发信可以使用 msmtp,但如果目标是让整个系统具备统一发信能力,Postfix 更合适。

原因包括:

  • rootcronsystemd 等传统系统通知默认更容易接入本机 MTA。
  • sendmail 接口兼容性更好。
  • 多个程序可以统一把邮件交给本机 Postfix。
  • 后续更容易接入日志巡检、备份提醒、自动化代理通知等系统级场景。
  • 与已有服务器环境保持一致,便于维护和排查。

因此,新节点采用:

Postfix + bsd-mailx + SASL 模块 + CA 证书

而不是:

msmtp 单用户发信方案

安全边界

该方案的安全边界非常明确:

  • 不部署收信服务。
  • 不开放公网 SMTP 入站端口。
  • 不监听所有网卡。
  • 不暴露 SMTP 密码。
  • 不在日志或汇报中输出认证文件内容。
  • SMTP 密码文件仅允许 root 读取。
  • Postfix 只监听 127.0.0.1:25
  • 外部发信统一使用专用通知账号。

这意味着本机邮件环境只是一个“本地通知出口”,不是对外邮件服务器。

安装组件

在 Ubuntu 系节点上安装必要组件:

apt update
apt install postfix bsd-mailx libsasl2-modules ca-certificates

安装 Postfix 时,如果出现交互式配置界面,可以选择:

General mail configuration type: Internet Site
System mail name: <LOCAL_MAIL_NAME>

其中 <LOCAL_MAIL_NAME> 可以使用类似:

node.localdomain

这只是本机邮件身份,不需要使用真实公网域名。

Postfix 主配置

通过 postconf -e 写入核心配置,避免直接覆盖整个主配置文件。

示例配置如下:

postconf -e 'relayhost = [<SMTP_HOST>]:<SMTP_PORT>'
postconf -e 'smtp_tls_wrappermode = yes'
postconf -e 'smtp_tls_security_level = encrypt'
postconf -e 'smtp_sasl_auth_enable = yes'
postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
postconf -e 'smtp_sasl_security_options = noanonymous'
postconf -e 'smtp_sasl_tls_security_options = noanonymous'
postconf -e 'smtp_generic_maps = hash:/etc/postfix/generic'

postconf -e 'inet_interfaces = loopback-only'
postconf -e 'inet_protocols = ipv4'

postconf -e 'myhostname = <LOCAL_MAIL_NAME>'
postconf -e 'myorigin = $myhostname'

postconf -e 'alias_maps = hash:/etc/aliases'
postconf -e 'alias_database = hash:/etc/aliases'

其中:

<SMTP_HOST>   外部 SMTP 服务器地址
<SMTP_PORT>   外部 SMTP 端口
<LOCAL_MAIL_NAME>   本机邮件身份,例如 node.localdomain

关键配置含义如下:

配置项作用
relayhost指定外部 SMTP 中继服务器
smtp_tls_wrappermode = yes使用 SMTPS wrapper 模式
smtp_tls_security_level = encrypt要求加密传输
smtp_sasl_auth_enable = yes启用 SMTP 认证
smtp_sasl_password_maps指定 SMTP 账号密码映射文件
smtp_generic_maps指定发件地址重写规则
inet_interfaces = loopback-only只监听本机
inet_protocols = ipv4只使用 IPv4 监听
alias_maps指定系统别名文件
alias_database指定别名数据库

SMTP 认证文件

创建 SMTP 认证文件:

vim /etc/postfix/sasl_passwd

内容格式如下:

[<SMTP_HOST>]:<SMTP_PORT> <SMTP_USER>:<SMTP_PASSWORD>

示例:

[mail.example.net]:465 notify@example.net:SMTP_PASSWORD_HERE

注意事项:

  • 真实密码不要写入公开文档。
  • 不要在聊天、工单或日志中粘贴该文件内容。
  • 文件权限必须限制为 root 可读。

保存后执行:

chown root:root /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd.db

确认权限:

ls -l /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db

期望结果:

-rw------- root root /etc/postfix/sasl_passwd
-rw------- root root /etc/postfix/sasl_passwd.db

发件地址重写

为了避免系统程序直接以 root@localdomain 之类的地址对外发信,需要配置地址重写。

编辑:

vim /etc/postfix/generic

示例内容:

root@<LOCAL_MAIL_NAME> <SMTP_USER>
root@localhost <SMTP_USER>
root <SMTP_USER>
<LOCAL_USER>@<LOCAL_MAIL_NAME> <SMTP_USER>
<LOCAL_USER>@localhost <SMTP_USER>
<LOCAL_USER> <SMTP_USER>

示例:

root@node.localdomain notify@example.net
root@localhost notify@example.net
root notify@example.net
operator@node.localdomain notify@example.net
operator@localhost notify@example.net
operator notify@example.net

生成映射数据库:

chown root:root /etc/postfix/generic
chmod 644 /etc/postfix/generic
postmap /etc/postfix/generic

确认文件:

ls -l /etc/postfix/generic /etc/postfix/generic.db

这样,本机系统用户发出的邮件在对外投递时会统一显示为专用通知邮箱。

root 邮件转发

系统内部很多通知默认会发给 root。为了让这些通知最终到达管理员邮箱,需要配置 /etc/aliases

编辑:

vim /etc/aliases

至少包含:

postmaster: root
root: <ADMIN_EMAIL>

示例:

postmaster: root
root: admin@example.net

生成别名数据库:

newaliases

确认:

ls -l /etc/aliases /etc/aliases.db

启动并重启 Postfix

启用并启动 Postfix:

systemctl enable --now postfix

如果在安装后修改了 inet_interfacesinet_protocols 等监听相关配置,需要重启 Postfix,而不仅仅是检查配置文件。

systemctl restart postfix

检查服务状态:

systemctl is-enabled postfix
systemctl is-active postfix
systemctl status postfix --no-pager

正常情况下应看到:

enabled
active

在某些 Debian/Ubuntu 系统中,Postfix 服务状态可能显示为 active (exited),这不一定代表异常。关键需要结合实际发信日志判断。

检查命令来源

确认本机发信命令是否来自预期软件包:

command -v sendmail
command -v mail
command -v mailx
readlink -f /usr/sbin/sendmail /usr/bin/mail /usr/bin/mailx 2>/dev/null || true

期望结果:

sendmail -> postfix
mail/mailx -> bsd-mailx

这表示系统程序调用 sendmailmail 时,会进入本机 Postfix 发信链路。

检查监听端口

这是整个方案中非常重要的一步。

执行:

ss -lntp | grep -E ':25|:465|:587' || true

期望只看到:

127.0.0.1:25

不应该看到:

0.0.0.0:25
[::]:25
0.0.0.0:465
0.0.0.0:587

如果仍然看到 0.0.0.0:25[::]:25,通常说明 Postfix 还没有重新加载或重启到新配置。此时应执行:

systemctl restart postfix

然后再次检查监听状态。

发送测试邮件

直接向管理员邮箱发送测试邮件:

echo "local mail test from $(hostname) at $(date)" | mail -s "local mail test" <ADMIN_EMAIL>

随后查看日志:

grep -Ei 'status=sent|status=bounced|warning|error|relay=' /var/log/mail.log 2>/dev/null | tail -n 80

也可以查看 systemd 日志:

journalctl -u postfix --since '10 minutes ago' --no-pager

成功标志是日志中出现类似:

relay=<SMTP_HOST>[...]:<SMTP_PORT>
status=sent

其中 status=sent 是最关键的成功标志,说明邮件已经被外部 SMTP 服务器接受。

root 别名测试

为了确认系统内部发给 root 的邮件也能转发到管理员邮箱,可以再做一次 root 别名测试:

echo "root alias test from $(hostname) at $(date)" | mail -s "root alias test" root

检查日志:

grep -Ei 'status=sent|status=bounced|warning|error|relay=' /var/log/mail.log | tail -n 40

如果同样出现 status=sent,说明 root 别名转发也正常。

最终验证结果

最终应确认以下状态:

Postfix: enabled, active
监听地址: 127.0.0.1:25
外部中继: <SMTP_HOST>:<SMTP_PORT>
SMTP 认证: 已启用
TLS wrapper: 已启用
发件地址重写: 已启用
root 别名转发: 已启用
测试邮件: status=sent

同时确认不存在以下情况:

对外监听 0.0.0.0:25
对外监听 [::]:25
监听 465 入站端口
监听 587 入站端口
SMTP 密码文件权限过宽
日志或汇报中泄露认证文件内容

一次容易忽略的问题

配置中已经写入:

inet_interfaces = loopback-only
inet_protocols = ipv4

但端口检查时仍然可能看到:

0.0.0.0:25
[::]:25

这通常不是配置写错,而是 Postfix 主进程仍在使用旧配置。安装过程中 Postfix 可能已经以默认配置启动,后续通过 postconf -e 修改配置后,如果没有重启服务,监听状态不会立即变成预期结果。

解决方式很简单:

systemctl restart postfix

重启后再次检查:

ss -lntp | grep -E ':25|:465|:587' || true

只剩 127.0.0.1:25 后,才可以确认本机监听安全边界已经生效。

经验总结

这次搭建的核心不是“安装一个能发邮件的软件”,而是把系统邮件能力纳入统一、可维护、可复用的基础设施模式。

关键经验包括:

  1. 先盘点已有成功环境,再复制成熟方案。
  2. Debian/Ubuntu 系统优先使用 Postfix hash 映射。
  3. Arch 系系统的 lmdb 配置不能直接照搬到 Ubuntu。
  4. 系统级通知场景优先使用 Postfix,而不是单用户 msmtp。
  5. SMTP 密码文件必须严格限制权限。
  6. 配置完成后必须检查实际监听端口,而不是只看配置文件。
  7. status=sent 比服务状态更能说明发信链路是否真正可用。
  8. 只发信、不收信的节点不应开放公网 SMTP 入站端口。
  9. root、cron、systemd、脚本和自动化代理最好统一走同一条本机邮件出口。
  10. 完成后应进行人工测试和自动化代理复核,避免只停留在“看起来配置正确”。

结论

边缘节点最终完成了与既有服务器一致的本机邮件通知环境。

该节点现在具备以下能力:

系统通知
cron 任务
systemd 服务
本机脚本
自动化代理

都可以通过本机 mailsendmail 进入 Postfix,再由 Postfix 使用外部 SMTP 中继发送到管理员邮箱。

最终状态可以概括为:

Ubuntu/Debian 系本机 Postfix 中继
只监听 127.0.0.1:25
不开放公网 SMTP 入站
外部 SMTP 认证发信
专用通知邮箱作为发件人
管理员邮箱作为接收端
测试日志 status=sent

这是一套轻量、安全、可维护的系统通知邮件出口,适合小型服务器、边缘节点、树莓派、家用服务器和个人运维环境使用。

Leave a Reply

Your email address will not be published. Required fields are marked *