NGINX 对IMAP的SSL连接支持有问题,测试GMAIL无法转发,效果并不够好
https://docs.nginx.com/nginx/admin-guide/mail-proxy/mail-proxy/
2017-05-31 from–https://www.freebuf.com/articles/network/135640.html
一、起因
最近在研究双因子认证的时候突然想到:能不能在邮件系统中应用双因子验证呢?作为一个有了想法就想落地的四有好少年,我决定试试。
受制于文化程度和钱包鼓起程度,我在本地用Exchange搭建了一套模拟环境(当然不是正版)。大家不要学习我这种做法,我们要支持正版。嗯嗯。
整个改造分为两块进行,一块是Web端进行双因子验证支持,这块不是难点,通过反向代理服务可以迅速解决。但是,在进行其他协议(例如SMTP、POP3、Exchange)改造的时候,发现事情并没有想象中的这么简单。
二、技术选型遇到的一点问题
作为一条万年没人权的Web狗,突然间给自己设定了一个这么艰巨的任务,顿时怀疑起了人生。翻了翻自己的技术栈,看来看去也就nginx可能也许大概好像能满足要求,那么久拿他来试试吧。
Nginx是个优秀的反向代理(负载均衡)工具,大多数人都对它在Web方向应用比较熟悉,但其实Nginx还支持对SMTP、IMAP、POP3这些邮件协议进行反向代理或者负载均衡。官网文档见https://www.nginx.com/resources/admin-guide/mail-proxy/。
于是,按照官方文档,写了个配置文件:
mail {
server_name mail.example.com;
auth_http localhost:9000/auth_http;
server {
listen 25;
protocol smtp;
smtp_auth login plain;
}
server {
listen 110;
protocol pop3;
pop3_auth plain;
}
server {
listen 143;
protocol imap;
}
}
然后再用世界上最好的语言PHP,写一个auth_http的配套服务(这个内容下文会提到)。
跑一跑程序,成了,auth_http服务能够正确收到客户端提交的账号密码,可是遇到了两个问题:
1、压根儿没动态验证码出场的机会啊;
2、在SMTP协议的代理中,auth_http只能向nginx返回是否验证通过的结果,nginx无法继续将账号密码向后端节点传递,导致真实的节点无法验证用户身份。
(填写完账号密码后,后端SMTP服务器回应拒绝发送,从Nginx的日志中的确发现我们完成了auth_http的认证)
对于问题1,其实SMTP/POP3/IMAP协议本身并没有提供支持双因子验证的设计,谷歌采用的思路是设置一个随机生成的静态密码与账号绑定。大部分人和谷歌不太一样,我们只能考虑在用户名或密码上动点手脚了:
这样一来,我们在兼容原有协议的基础上可以使邮件系统能够完整地支持双因子验证。至于后一个问题,我翻遍nginx的文档都没有找到着解决方法,上谷歌一搜后,发现很久之前就有同仁们提出过类似疑惑:
http://mailman.nginx.org/pipermail/nginx/2010-February/019028.html
看来是无解咯?
幸好Nginx是开源的软件,我们还可以靠自己。
三、阅读源码,发现问题
要怒怼Nginx,首先得深入源码。
Nginx用来处理Mail协议的代码在src/mail/目录下,我们主要关注ngx_mail_proxy_module.c和ngx_mail.h两个文件。
这两个文件大致定义了Nginx对协议的处理过程,其中SMTP协议长这样:
(上图省略了例如环境初始化、容错、其他场景特殊处理等过程)
在原有的Nginx处理过程中,从auth_http得到返回数据后,程序跳过了与后端节点通信的过程,而仅设置了该session对应的后端节点后便不作处理。
所以在官方文档中,auth_http仅能响应Auth-Server、Auth-Port两个头部来指示Nginx的后续操作,不具备向后端转发认证信息的过程。
这时候,我们有两种选择:
1、后端SMTP服务器不再进行登录验证,仅允许来自Nginx的即可;
2、改造Nginx,使得其支持SMTP的认证过程。
第一种方案当然可行,从Nginx的实现来看,甚至可以认为是推荐这么操作的。但是,这个方案有天然的弱点,认证过程需要被解耦。也就是说,认证服务需要同时存在于邮件服务器(IMAP/POP3需要用到)和auth_http中,两者必须一致。
(Nginx源码中,对IMAP已有相关实现)
在我看来这样有点麻烦,在一些特殊的场景下可能会有适配问题,所以我决定改造Nginx,使得它能够支持SMTP协议认证过程的转发。
四、修改源码,解决问题
修改源码其实非常简单,因为此前的IMAP、POP3中Nginx已经完成了相关的实现,只不过没把它加入SMTP中。考虑到SMTP协议本身不是强认证的(即认证过程是可选的),所以不建议直接强硬地设置这个流程,应当通过配置项管理。
我把修改后的源代码放到了全球最大的同性交友平台上( idapro123/mail_protector),修改的内容并不多,大家不妨可以看看。
这里简要说明修改的内容:
1、在mail配置节中新增了一个可选配置项:smtp_auth_reproxy on | off (default),该配置项的作用是用来控制是否将客户端的AUTH过程传递给后端服务器进行验证;
2、调整了SMTP协议的代理逻辑,当smtp_auth_reproxy被显式置为on的时候,会向后端服务器转发auth_http响应头中的Auth-User和Auth-Pass字段。
(修改后的代码中,同样增加了向后端服务器转发认证请求的过程)
这样一来,我们上面提到的问题都不复存在了,剩下的,就是如何去写一个双因子验证服务。
我同样将auth_http的代码提交到了交友平台上,因为考虑到产线环境需要高性能的读写,才为auth_http服务引入了redis作为中间结果的缓存服务器。另外,在我这边的实际环境中,双因子验证本身即作为一个独立的基础服务提供API调用,所以我在auth_http的实现上也只是简单的引用了这个服务。根据不同的情况,则可能还需要自己进一步修改其中的业务逻辑和具体内容。
顺带的,我们还能通过auth_http完成频控,阻断那些天天开着扫描器在网上拿弱密码库到处碰撞的家伙。
(auth_http中向Nginx返回验证结果和原始密码)
五、实际测试
编译一下修改的Nginx,这里要注意,编译时至少需要指定以下两个参数,否则,在加载mail模块的时候会报告错误:
–with-mail –with-mail_ssl_module
这里提供一个openresty的编译配置,供大家参考:
cd /opt/ngx_openresty-1.9.3.1/
./configure –user=nobody –group=nobody –prefix=/opt/openresty –with-google_perftools_module –with-http_stub_status_module –with-google_perftools_module –with-pcre-jit –with-pcre=/opt/pcre-8.36 –with-http_ssl_module –with-http_addition_module –with-mail –with-mail_ssl_module –with-http_gzip_static_module
gmake -j32
gmake install
make clean
编译需要一点时间,完成之后启动Nginx:
/opt/openresty/nginx/sbin/nginx
我们测试一下效果。
(正常情况)
(未验证就想来一发的情况)
(双因子验证失败情况)
(原始密码错误情况)
(触发频控被拒绝的情况)
在实际测试过程中,仍然发现了一些缺陷。
其中最为操蛋的是目前还无法支持Exchange协议,这样一来将导致通讯录和日历功能变得不可用。
参考其他厂的解决方案时,发现他们将日历、通讯录服务独立出来,做成一个在线的iCalendar服务让邮件客户端调用。iCalendar本身可以使用HTTP进行分发(例如使用世界上最好的语言PHP写的DAViCal),所以这个问题其实也能解决。
突然间,我面对着服务器上新增的四个服务(Mail代理、auth_http、Redis、iCal),竟然没有一点欣慰的感觉。为了消灭一个潜在的安全风险,不得不引入多个额外的环节,这么做是不是在和自己最早的愿望背道而驰呢?