背景
在多台服务器和个人设备组成的运维环境中,系统通知邮件是一项非常基础但重要的能力。备份任务、定时任务、系统服务、自动化脚本和监控程序,都可能需要在异常发生时主动发出通知。
已有几台系统已经具备稳定的本机邮件发送能力。这些系统并不直接对外提供邮件服务,也不负责接收邮件,而是通过本机邮件程序把通知交给 Postfix,再由 Postfix 中继到外部 SMTP 服务器。新的边缘节点也需要具备同样的能力,以便后续承载系统通知、自动化任务提醒和本机代理程序通知。
目标不是搭建完整邮件服务器,而是搭建一个安全、轻量、统一的本机发信环境。
目标设计
最终目标如下:
本机 root / cron / systemd / 脚本 / 自动化代理
↓
mail 或 sendmail
↓
本机 Postfix
↓
外部 SMTP 中继服务器
↓
管理员邮箱
该方案具备几个特点:
- 本机只负责发信,不负责收信。
- Postfix 只监听本机回环地址。
- 不开放公网 SMTP 入站端口。
- 所有系统通知统一走同一条邮件出口。
- 发件账号使用专门的通知邮箱。
- 管理员邮箱作为最终接收地址。
- 配置结构尽量与已有服务器保持一致。
既有系统方案盘点
在正式搭建新节点之前,先对已有系统进行了只读检查。检查对象包括两类服务器节点和一台桌面工作站。
检查重点包括:
- 当前系统发行版和包管理器。
- 是否安装 Postfix、mailx、msmtp 等邮件相关组件。
sendmail、mail、mailx命令实际指向哪里。- 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 更合适。
原因包括:
root、cron、systemd等传统系统通知默认更容易接入本机 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_interfaces、inet_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
这表示系统程序调用 sendmail 或 mail 时,会进入本机 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 后,才可以确认本机监听安全边界已经生效。
经验总结
这次搭建的核心不是“安装一个能发邮件的软件”,而是把系统邮件能力纳入统一、可维护、可复用的基础设施模式。
关键经验包括:
- 先盘点已有成功环境,再复制成熟方案。
- Debian/Ubuntu 系统优先使用 Postfix
hash映射。 - Arch 系系统的
lmdb配置不能直接照搬到 Ubuntu。 - 系统级通知场景优先使用 Postfix,而不是单用户 msmtp。
- SMTP 密码文件必须严格限制权限。
- 配置完成后必须检查实际监听端口,而不是只看配置文件。
status=sent比服务状态更能说明发信链路是否真正可用。- 只发信、不收信的节点不应开放公网 SMTP 入站端口。
- root、cron、systemd、脚本和自动化代理最好统一走同一条本机邮件出口。
- 完成后应进行人工测试和自动化代理复核,避免只停留在“看起来配置正确”。
结论
边缘节点最终完成了与既有服务器一致的本机邮件通知环境。
该节点现在具备以下能力:
系统通知
cron 任务
systemd 服务
本机脚本
自动化代理
都可以通过本机 mail 或 sendmail 进入 Postfix,再由 Postfix 使用外部 SMTP 中继发送到管理员邮箱。
最终状态可以概括为:
Ubuntu/Debian 系本机 Postfix 中继
只监听 127.0.0.1:25
不开放公网 SMTP 入站
外部 SMTP 认证发信
专用通知邮箱作为发件人
管理员邮箱作为接收端
测试日志 status=sent
这是一套轻量、安全、可维护的系统通知邮件出口,适合小型服务器、边缘节点、树莓派、家用服务器和个人运维环境使用。