通过SSH和受限sendmail进行电子邮件投递
项目描述
受限sendmail命令
一个更安全的sendmail命令,用于在SSH上发送电子邮件而不需要密码。
目标
此命令旨在取代内置的sendmail
命令,该命令给予调用者过多的权限。例如,Postfix的sendmail(1)命令可以列出邮件队列(-bp
),重新散列别名数据库(-bi
),启动守护进程(-bl
,-bd
),或刷新队列(-q
);都是旧Sendmail二进制文件的残余,它本身可能就是图灵完备的。
相反,rsendmail可以轻松地在系统上排队邮件,而不给予客户端任何额外的权限。反过来,这使得配置像笔记本电脑或工作站这样的卫星系统就像向authorized_keys
文件添加SSH密钥一样简单。然后,该密钥可以发送电子邮件,但仅发送电子邮件:没有shell访问或服务器管理。
当然,这可以通过常规的SMTP客户端完成,但那需要密码,而密码很弱。
快速入门
scp rsendmail.py example.net:/usr/local/bin/rsendmail
无论您在哪里调用sendmail
,现在都可以调用此命令
ssh example.net rsendmail
以下说明如何添加离线时的队列、限制与rsendmail的连接或与现有的MTA集成。
安装
此系统由两部分组成
-
rsendmail.py
- 在远程SSH服务器上安装的包装脚本,仅接受和转发邮件的连接 -
sshsendmail.py
- 一个本地的MDA,作为与远程rsendmail的兼容性适配器。这部分是可选的,如下所示。
基本配置
以下假设您的中继主机是example.net
,并且已经配置为允许名为rsendmail
的用户进行SSH连接。还假设存在一个名为devnull@localhost
的电子邮件,它可以接收邮件。
-
在远程主机上找到
$PATH
ssh rsendmail@example.net 'echo $PATH'
-
将
rsendmail.py
安装到$PATH
中的某个位置,作为rsendmail
scp rsendmail.py rsendmail@example.net:/usr/local/bin/rsendmail
-
为rsendmail生成SSH密钥
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_rsendmail
-
将密钥复制到您的
authorized_keys
文件中( printf 'command="rsendmail",restrict '; cat ~/.ssh/id_ed25519_rsendmail.pub ) | ssh rsendmail@example.net 'cat >> .ssh/authorized_keys'
-
发送测试邮件
printf "Subject: test\n\nThis is a test" | ssh -i ~/.ssh/id_ed25519_rsendmail rsendmail@example.net rsendmail devnull@localhost
-
验证邮件是否正确投递且消息内容完整。如果是这样,则
rsendmail
已正确配置
现在您可以发送邮件,但还有一些内容缺失。大多数工具都会期望有一个可用的sendmail
命令,并且您可能想要在本地排队邮件以避免网络不可用时失败。因此,您需要某种类型的包装器,一个MDA,以标准方式实际投递邮件。排队将由一个MTA处理,该MTA将调用MDA。
MDA配置
所以下一步是设置一个本地的MDA与rsendmail
通信。以下是下面文档中记录的可能配置的快速比较
MDA | 优点 | 缺点 |
---|---|---|
独立 | 最简单的 | 单用户,无队列 |
Nullmailer | 最小化 | 不寻常的标准,可靠性问题 |
Postfix | 知名 | 队列过期 |
与其他MTA的集成也是可能的,欢迎提供完成该任务的文档。
独立
最简单的配置是使用MDA的简单包装脚本,没有其他MTA。例如,以下是一个可能的sendmail
命令的内容
#!/bin/sh -e
exec ssh -i /var/mail/.ssh/id_ed25519 rsendmail@example.net rsendmail "$@"
上述假设私钥存储在~mail
主目录中。私钥需要由所有调用命令的用户可读,这可能是多用户系统的一个安全问题。这还假设在远程系统上创建了名为rsendmail
的用户。
Nullmailer兼容性
Nullmailer是一个“简单的仅中继邮件传输代理”,有些人使用它来在网络上不可用时本地排队邮件。我们不能使用像上述那样的简单包装器,因为nullmailer向MDA传递收件人有一个非标准的方式。这就是sshsendmail.py
包装器的作用所在。
-
为
mail
用户生成SSH密钥sudo -u mail ssh-keygen -t ed25519
-
确保远程服务器身份已验证
sudo -u mail ssh rsendmail@example.net true
-
安装版本至少为2.0的
nullmailer
包apt install -t buster nullmailer
-
部署MDA包装器
install sshsendmail.py /usr/lib/nullmailer/sshsendmail
-
将其添加到
/etc/nullmailer/remotes
中的远程服务器example.net sshsendmail --mta=nullmailer --identity=/var/mail/.ssh/id_ed25519 --user=rsendmail
同样,根据您的配置调整example.net
主机和rsendmail
用户。
我发现nullmailer使用的线协议相当不寻常。它似乎是完全非标准的,这很烦人。更糟糕的是,上述说明只适用于Nullmailer 2.x - 之前的版本有一个不同的协议,这里不支持。此外,我对该软件的可靠性有担忧:在测试期间,nullmailer在处理rsendmail
中的一个错误时崩溃...
Postfix兼容性
Postfix可以通过管道服务轻松与远程rsendmail
服务器通信。以下是配置Postfix客户端的步骤,一旦在服务器上安装了rsendmail
并设置了authorized_keys
-
安装Postfix
apt-get install postfix
-
将其配置为
卫星
系统并使用推荐的主机名。作为中继主机
,使用SSH服务器的主机名(和用户名!)例如:rsendmail@example.net
,换句话说postconf -e 'relayhost=rsendmail@example.net'
-
在
/etc/postfix/master.cf
中配置管道
服务rsendmail unix - n n - - pipe user=mail argv=ssh ${nexthop} rsendmail -f ${sender} ${recipient}
-
将该传输设置为默认中继
postconf -e 'default_transport=rsendmail:'
-
确保
mail
用户可以自动登录到中继服务器并发送邮件sudo -u mail ssh rsendmail@example.net rsendmail devnull@localhost < /dev/null
-
上述操作将要求进行主机验证。一旦成功,重新加载Postfix,它应该开始通过其他服务器中继邮件
postfix reload
请注意,上述配置如果SSH无法连接到远程服务器,则会导致消息退回。这是因为SSH返回非标准(根据sysexits.h
)的错误代码(即失败时的255
),Postfix无法直接解析。要正确处理,可以安装sshsendmail.py
包装器,再次在master.cf
rsendmail unix - n n - - pipe
user=mail argv=/usr/local/bin/sshsendmail --host ${nexthop} -f ${sender} ${recipient}
示例
avr 23 20:38:06 curie postfix/pickup[28657]: 61947125AA4: uid=0 from=<root>
avr 23 20:38:06 curie postfix/cleanup[28716]: 61947125AA4: message-id=<20180424003806.61947125AA4@curie.example.net>
avr 23 20:38:06 curie postfix/qmgr[28658]: 61947125AA4: from=<root@curie.example.net>, size=386, nrcpt=1 (queue active)
avr 23 20:38:06 curie postfix/pipe[28718]: 61947125AA4: to=<anarcat@example.net>, relay=rsendmail, delay=0.49, delays=0.03/0/0/0.46, dsn=2.0.0, status=sent (delivered via rsendmail service (sending message through command: ['sendmail', '-f',
avr 23 20:38:06 curie postfix/qmgr[28658]: 61947125AA4: removed
请注意,Postfix在5天后将从队列中退回电子邮件。如果您离线时间超过这个时间段,您可能需要调整maximal_queue_lifetime
设置为一个更大的值
postconf -e maximal_queue_lifetime=30d
postfix reload
实施细节
我们大大限制了从sendmail
接受的选项数量。只有那些选项被认为是有效的
<recipient> [ <recipient> [ ... ] ]
- 发送电子邮件的电子邮件地址。这些不能以破折号开头,且不能包含空格。每个电子邮件都必须作为单独的参数传递给rsendmail-t
:从To
或Cc
电子邮件标题中推断收件人。这直接传递给底层的sendmail命令,rsendmail不会直接解析。这假设在另一侧没有-t
选项的漏洞。-f <sender>
:设置信封发件人地址。这是发送交付问题的地址。-oi
:不特别处理单独一行的.
。
以下选项故意忽略,尽管它们最终可能被实现
-R <return>
和-N <dsn>
:我们并不真正关心状态。只是接受远程服务器的默认值。-r <sender>
:与-f
相同-v
:将来可能有用,但现在保持简单
所有其他选项都可能导致错误,或者可能在未来出于向后兼容的目的而被忽略,但不应产生任何影响。除非另有说明,否则本文件中的sendmail
参数均指Postfix sendmail(1)手册页。
使用mail
日志功能将消息发送到syslog。
陷阱和注意事项
sshd
就关于8位清洁通道的no-pty
和command=
提出了一些噪音。我们假设一个8位清洁通道,因此请确保authorized_keys
文件有一个no-pty
设置。最好使用restrict
参数,但这仅从OpenSSH 7.2开始提供- 创建一个专用用户可能比重新使用特权账户更合适。
- 如果启用
mail-interactive
(默认值),Emacs的sendmail-send-it
函数将失败,如果sendmail命令有任何输出。这意味着将日志级别更改为比WARNING
更详细的内容将导致Emacs认为即使电子邮件实际上已发送,也存在失败。这意味着Fcc
也会失败,如果用户没有意识到问题,将会发送多封电子邮件。 - Armstrong的脚本使用(MD5)校验和来确保消息的完整性。这在这个提交中引入,作为“避免由于断开连接而发送截断文件”的方式。我们不知道rsendmail是否受到此错误的影响。
现有技术
-
LMTP 在某种程度上实现了我们在这里的需求,但是没有一个真正的客户端可以在另一端运行,因此它并不是非常有用。
-
msmtp 与我们所需要的非常接近,但它只支持SMTP,这意味着需要在客户端存储秘密。我们可以尝试通过SSH连接将SMTP套接字管道化,但这感觉相当混乱,而且用途不广泛。它也没有本地队列。
-
nullmailer 几乎是我们所需要的,但仍然使用SMTP。
-
dma(DragonFly邮件代理)与上述类似,并执行一些奇怪的操作,如修改正在传输的消息(例如删除
Bcc
)。 -
esmtp 更像是上述内容,并且“不再维护”(访问于2018-04-21)。
-
ssmtp 与msmtp类似,但它在Debian中没有活跃的上游。
-
masqmail 与上述类似,但似乎有自己的别名数据库和其他复杂的东西。
-
UUCP(Unix-to-Unix CoPy)是为此目的而设计的,并且sendmail提供了一个rmail命令,可以从UUCP客户端读取电子邮件,但这些有自己的特性。尽管如此,应该可以配置UUCP客户端通过SSH连接发送电子邮件,但这似乎过于复杂。
-
NNCP(节点到节点复制)“是一组简化安全存储和转发文件以及邮件交换的实用程序。”在理论上有趣,但实际上它做了比我们这里实际需要的多得多。但如果我要重新做这个,我可能会选择用它来代替我的设置,因为它很容易集成到Postfix中,并且比SSH更健壮(例如,通过业余无线电发送电子邮件?)
-
Don Armstrong 编写了一个名为nullmailer的远程程序sshsendmail,它基本上实现了我们的需求,但它通过SSH连接作为
perl -e
可执行文件注入了一个nullmailer屏蔽。这使得限制SSH连接变得困难。David Bremner 将此版本的早期版本重新包装为nullmailer-ssh,它至少没有使用perl -e
,但仍然在rsendmail
命令中使用了nullmailer特定的方言。 -
一些IMAP服务器支持一个
Outbox
文件夹,该文件夹将通过配置的邮件服务器发送放入该文件夹的电子邮件。只有Courier MTA似乎有这个功能(称为IMAP send),我早就停止使用该服务器。我的首选服务器(Dovecot)在2006年讨论了该功能,但它从未实现。
未来工作
这可以在Debian包中实现:一个将是服务器端的rsendmail
,另一个是客户端的sshsendmail
,也许还有各种集成机制的插件包。我现在太懒了,不想做这个。
通过SSH管道化内容使得很难区分暂时性故障(例如DNS或TCP失败)和配置错误(SSH密钥不匹配)。我甚至不确定应该将什么弹回,因此我通过将所有SSH故障视为暂时性的来避免这个问题,但在未来可能会重新实现它,使用Paramiko或一些其他库。
致谢
在上述“现有技术”的基础上,我站在Bremner和Armstrong的肩膀上,因为他们为这个程序提供了基本思路。
此软件由Antoine Beaupré于2018年编写,并发布在 Affero GPLv3 许可下。
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。