笔者早年进入互联网行业之时,微服务未起,Nginx未兴。那些年有段时间我每天的工作之一就是配置大量的Apache重写规则,把请求路径用一系列复杂的正则匹配重定向到SEO希望的路径。我们那时候的期望就是能够通过努力提升我们的网站在SEO的排名。

时至今日,我们早已远去了那些刀耕火种的岁月。现在的同学可能对Apache的理解只是一个软件基金会了,但是这个名字曾经属于一个反向代理。软件的更迭是如此迅速,Redis的作者在文章中这样描述:我相信软件虽然很棒,但不会像一本存活了几个世纪的书一样伟大,这绝不是因为它本身不好,而是因为其中的副作用,并且,它终将被更有用的软件替换掉。因此有时我会觉得自己做的一切终将都是徒劳的。国内一位著名的Golang推广者也曾说相比于做一款软件,其更愿意发明一门语言,因为语言的寿命也许是百年。

01 介绍

今天要讲的是中通的零信任安全代理ZFE,在ZFE之前我们也像大多数互联网企业一样采用Nginx做流量反代,激进一点的地方我们采用了Openrestry。Nginx很棒,他能解决用户遇到的大多数问题,同时它也因为高性能在一众反代产品中脱颖而出。但是对于像中通这种体量的公司,Nginx总是不够的,反代是一个流量的入口,里面流过的是数据,也是财富,但同样也包藏危机,我们需要一个完全可控的,能够高度定制化并且响应业务的流量反代。此刻,摆在我们面前的有三条路:

  1. 基于Nginx或Openrestry二次开发

  2. 完全自研

  3. 基于开源社区已有的成熟产品开发

对于方案1和2来说时间成本不可控,可能会陷入泥潭,最终我们敲定了方案3,我们研究了Envoy、Traefik、Nginx的实现,取长补短,开发ZFE。在笔者写这篇文章时ZFE已经在中通成功落地,并且我们交付的产品在经过不断的调整优化后已经具备不错的性能和稳定性。

但是在项目刚开始时我们手头上只有繁复的需求,庖丁解牛,梳理结构,制定开发方向,写下第一行代码......

经过努力我们做出取舍,调整方向找到了自己的节奏。

以下是目前的ZFE的架构图

图1. ZFE架构图

02 ZFE优势

高级条件表达式

NGINX基于正则路由做请求转发,学习成本较高,对于新手而言很容易写出存在歧义的配置。在ZFE之前笔者其实也一直寻找一种优雅且强大的解决方案:既能支持灵活的配置,同样方便易学,而且除了域名和路径之外能够根据请求内容做转发。

最终我们得到的答案是用条件表达式去做,对于开发和运维而言,这类表达式学习基本零成本,运维人员能够根据表达式的名称知其用途,在实践中我们的运维同事也基本做到了即学即用。

我们看下同样的规则在Nginx和ZFE下分别是如何配置的:

// Nginx配置

server_name test.com;

location /wwww/test {}

// 基于条件表达式的ZFE配置

req_host_in("test.com") && req_path_in("/www/test")

通过这个简略的配置我们大致知道了二者使用上的不同,对于简单的配置二者并不会存在明显的优略,但是对于复杂路由,ZFE的高级表达式的优势就体现出来了,无歧义,简单。NGINX借助于正则的高级特性也许可以做到配置更少,但是ZFE无疑是更简单的,简单即是美!

除了简单之外ZFE更擅长基于内容做分析和转发。

拦截已经离职的用户对于敏感系统的访问

cond: req_user_type_in("departure") && req_host_in("a.com|b.com|c.com")

effect: block

记录用户对于敏感信息的访问

cond: req_host_in("a.com|b.com|c.com") && req_user_action_in("privacy")

effect: record

筛选包含特定cookie的请求

cond: req_cookie_contain("sessionid")

根据用户ID筛选用户

cond: req_ssouserid_in("234|3444")

凡此种种,不一而足。其中出现的形如req_host_in的判断单元为条件原语,ZFE正是由这些数以百计的条件元 语来实现基于内容的转发的。

ZFE判断请求满足条件表达式后,有两种选择。一为流量转发,二为基于策略实现更高级别的流量控制。上面出现的effect: record即为满足对应策略之后的高级控制。

策略引擎

高级条件表达式是ZFE的一个重大特性,基于高级条件表达式的策略引擎更是将这一特性推向另一个高度,它让ZFE不再仅仅是一个反向代理,ZFE借助高级条件表达式具备了控制和响应异常流量的能力。

策略是用于定义和描述一条或多条权限的语法规范。一条策略包含以下要素:

  • 产品线(product):策略生效的产品线。

  • 身份主体(principal):描述策略授权的身份实体。包括用户、应用、API、设备、用户组和角色等实体。

  • 操作(action):满足条件后授权的操作。

  • 效力(effect):描述声明产生的结果是“允许”还是“显式拒绝”。包括 allow(允许)和 deny (显式拒绝)两种情况,对应于action。

  • 资源(resource):描述授权的具体数据即资源。资源采用分段式设计,不同的段代表不同的区间,一般而言从大范围向小范围递进,直到能够标志具体数据。

  • 条件(cond):描述策略生效的约束条件,这里由高级表达式实现。

{

"version":"2.0",

"statement":[

{

"product":"sec",

"effect":"allow",

"action":"api:GetUserInfo",

"principal":{

"zto":"zto:iam:ztemp:1234567"

},

"resource":"*",

"cond":"remote_ip_in(10.217.182.3/24|11.21.33.72/24)"

}

]

}

该样例表示允许sec产品线中属于zto租户下的ztempy用户池(zt employee) 的用户 123456710.217.182.* 或者 111.21.33.* 网段对iam服务中的api:GetUserInfo的访问。

ZFE基于策略可以实现针对各种复杂场景无侵入的访问控制,但是ZFE如何基于自然语言而不是传统的ACL来实现真正的应用层的零信任?

上述条件表达式举例中仍然部分沿用了传统网络的基于IP的描述符,可以做一定的过渡和兼容,但我们演进的方向是利用全局唯一的资源名称、身份实体、操作及其上的元数据,利用打标签的形式来构建元数据体系,从而应用在使用自然语言书写的表达式中,构成跨越传统网络边界的更具有普适灵活性的基于策略访问控制模型。在实际的落地中我们要解决诸如:

  • 资源、身份实体、操作收集和定义的问题

  • 标签库的梳理建立和自动化聚合更新的问题

  • 访问资源行为的多级依赖关系梳理和动态更新问题

  • 策略的统一管理、动态更新、策略冲突处理等问题

  • ......

ZFE的发展中挑战很多,但前进的方向一定是正确的,以此拥抱云原生......

方便的控制面

ZFE是一个模块化的程序,每个模块之间分工明确。对于一些常见的业务需求我们通过拓展对应的模块实现,然后通过控制平面进行管理。控制平面提供了丰富的管理入口,所有的配置都是实时生效,我们可以简单的在web控制台上配置就实现数以百计的服务器节点管理,彻底告别了如Nginx在机器上编辑配置文件然后再reload的繁琐流程。每一个配置都包含了完备的自我检查能力,以便尽可能减少因人而产生的错误。

基于中通的现状ZFE的控制平面抽象了集群这一概念,集群即是物理上的边界也是逻辑上的边界,一个集群内部共享所有配置,一个集群即为一组域名的最小逻辑操作单元。我们根据业务的重要性,流量大小,把不同域名分配到不同集群,在一个集群内进行一般的域名配置,转发配置。

下图展示了ZFE的控制平面中的集群管理页面 :

图2. ZFE集群管理

ZFE丰富的功能是由十多个模块组成的,这个数字随着接入ZFE的业务越来越多,需求越来越多,将会不断增加。为了解决模块配置的不便,ZFE控制平面提供了统一的模块配置管理方案,并且这些配置都是实时生效的。

图3. ZFE模块管理

ZFE的所有配置都有对应的版本号,这便是说我们能够灵活的在不同版本间切换,极大限度的降低了运维成本,提升了使用舒适度。

在后续的计划中ZFE将会提供大量的控制API,通过这些API业务方可以轻松的知道各个业务节点的负载情况,根据不同的情况灵活变更负载策略。

必要的错误恢复能力

像Nginx这类反代软件,程序出现异常会直接导致服务崩溃,如果你需要对Nginx做二次开发这无疑会变成巨大的障碍,毕竟不是每一个程序员都能写出高质量的c代码。ZFE借助于golang的强大特性可以直接实现崩溃自动恢复,这个特性对于响应业务,快速迭代非常重要,我们可以做到在异常发生时第一时间从错误中恢复,稳定的提供服务。对于反代产品稳定性压倒一切。

在ZFE的整个实施过程中,我们观察到各种类型的复杂请求涌入,也观察到了攻击请求。这些流量大都被拦截在了ZFE这一层,但是由于ZFE本身的复杂性我们的请求处理协程(golang特性)可能会在某个不够健壮的模块下崩溃,golang的错误恢复能力此时就是兜底方案,它提供了一定程度的容错性。

灵活的日志记录能力

ZFE中集成了多种日志模板引擎,通过配置模板可以输出各种常见的日志格式,所有通过ZFE中转的流量,都会被详细记录。在日志模块中我们声明了各种用于表征流量的变量, 只要在模板中按照对应的格式写入这些变量, 整个日志模块就会按照对应的算法实现开始搜集并记录各种指标。

我们目前的方案是日志存盘,经由filebeat上报kafka,最终落地es。在后续的计划中,日志的记录会受到策略引擎的控制,这就是说我们可以根据不同的场景做灵活的日志搜集策略。举个例子,某次请求符合隐私访问策略,而该策略的定义中要求记录响应的数据,那么最终的日志输出中便会包含后端的响应数据。

数据中蕴含巨大的财富,也蕴含了巨大的危机。近些年UEBA(user and entity behavior analytics)逐渐兴起,我们期望通过ZFE收集到的数据能够助力于实现:

  • 账号失陷检测

  • 主机失陷检测

  • 数据泄漏检测

  • 内部用户滥用

  • 提供事件调查的上下文

UEBA通过机器学习得到的行为分析数据将会做为ZFE策略拦截引擎的依据,从而使ZFE具备强大的风险预测,风险拦截能力。这样整个链路形成了闭环,形成了一套现代的安全检查与治理方案。

一流的可见性

每个工程师都不希望自己的程序是一个黑盒,我们总是期望能够时刻了解程序在干什么,将要干什么。

ZFE提供了丰富详尽的监控指标以便于做数据分析与可视化,除此ZFE还支持请求分布式Tracing,真正做到了记录并跟踪每一次请求。

图4. ZFE可见性

这张图很好的概括了ZFE是如何提供一流的可见性的。

下面的数据是HTTP指标中的一部分

HTTP_BACKEND_CONN_ALL与后端建立的总连接数

HTTP_BACKEND_CONN_SUCC与后端建立成功的连接数

HTTP_BACKEND_REQ_ALL转发到后端的总请求数

HTTP_BACKEND_REQ_SUCC成功转发到后端的请求数

HTTP_PANIC_BACKEND_READ后端READ协程panic的次数

HTTP_PANIC_BACKEND_WRITE后端WRITE协程panic的次数

HTTP_PANIC_CLIENT_FLUSH_LOOP客户端FLUSH协程panic的次数

HTTP_PANIC_CLIENT_WATCH_LOOP客户端WATCH协程panic的次数

借助于grafana的图形化展示,我们可以做到对ZFE的运行状态了然于胸。

告警平台通过ZFE提供的各种指标能够及时发现问题,防范于未然。

强大的流量控制能力

流量控制在网络传输中是一个常用的概念,它用于解决发送方和接收方速度不匹配的问题。从系统稳定性角度考虑,在处理请求的速度上也有非常多需要注意的地方。任意时间到来的请求往往随机不可控而系统的处理能力是有限的,这时我们就需要根据系统的处理能力对流量进行控制。

图5. ZFE哨兵

ZFE集成了阿里开源的哨兵系统,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来保护业务的稳定性。

不错的性能

借助于golang语言,虽然ZFE本身的极限性能略逊色于Nginx,但仍能让人满意,在高峰时期,ZFE整体内存开销也并不算高。

异步传输

提起Nginx一般首先会想到的是高性能,其实Nginx在高性能之外的异步传输是其能够迅速普及的另外一大优势。

在文件传输,或者慢网络场景中,整个请求会持续较长时间,整个后端服务会长时间保持连接等待传输完毕。这在一些高并发场景下无疑会造成后端服务巨大的线程开销。

中通在双十一期间经历了超高流量的考验,由于当时ZFE并不支持异步传输,后端压力陡然增加,但是得益于中通整个技术团队的深厚技术积累,ZFE并未造成实际影响。为了弥补不足,ZFE实现了自己的异步传输方案,并且经受住了后续考验,ZFE通过配置可以控制是否缓存用户请求数据和响应数据。

原生支持各种协议

作为中通Nginx的替代方案,ZFE自然需要支持各种常见的协议。

目前ZFE对HTTP、HTTPS、SPDY、HTTP/2、gRPC、WebSocket、TLS、FastCGI等提供了较为完善的支持。后续也将会拓展对TCP的支持。

03 ZFE的不足

在文章的上半部分我们列举了各种丰富且强大的特性,但是这些特性需要大量的配置参数控制,ZFE管理员需要理解并运用好这些参数无疑需要大量的实践。

我们未来的一个努力方向便是简化管理人员的操作,尽可能隐藏不必要的细节,做好各种参数的默认值调优,这样用户初次便可较容易的使用,然后随着使用的深入逐渐解锁高级操作。简单地说就是要做一个能够循序渐进引导用户学习的控制平面。

04 更加深入地了解ZFE

ZFE也是一个现代的WAF程序,我们有一个WAF模块专门用户检查各种攻击请求。

作为一个能够响应业务的反向代理,ZFE中集成了用户身份识别的能力,并且这将成为ZFE的一项基础功能,我们正在利用ZFE结合IAM平台基于身份做访问控制管理。详细信息读者可以参考本频道IAM的相关文章,ZFE将会是整个IAM体系中的重要组件之一,相关信息本文不再赘述。

05 未来的方向

反向代理

目前ZFE的一项主要工作就是替换中通的Nginx,所以ZFE会继续在这一块努力,对于Nginx的兼容性是一个主要的考虑方向,未来会继续改进。

云原生Ingress ZFE

流量入口代理作为互联网系统的门户组件,具备众多选型:从老牌代理 HAProxy、Nginx,再到容器化Ingress规范与实现,不同选型间功能、性能、可扩展性、适用场景参差不齐,这其中更有Envoy这个后期之秀。之所以把容器化Ingress这个方向作为ZFE的发展方向之一是因为中通的云原生平台也正在如火如荼的进行中,在和兄弟部门的沟通中我们同样看到了ZFE在这个领域,在中通的巨大需求。乘势而为,方能有所为。

SDP隐身网关

软件定义的边界(SDP)是由云安全联盟(CSA)开发的一种安全框架,它根据身份控制对资源的访问。该框架基于美国国防部的“need to know”模型——每个终端在连接服务器前必须进行验证,确保每台设备都是被允许接入的。其核心思想是通过SDP架构隐藏核心网络资产与设施,使之不直接暴露在互联网下,使得网络资产与设施免受外来安全威胁。

SDP有时被说成是“黑云”,因为应用架构是“黑”的——根据美国国防部的定义,这个“黑”代表了架构无法被检测到。如果攻击者无法知道目标在何方,那么攻击将无法进行。因此,在SDP架构中,服务器没有对外暴露的DNS或者IP地址,只有通过授权的SDP客户端才能使用专有的协议进行连接。

看到这里我想读者应该已经明白为何ZFE会把SDP隐身网关作为方向之一,往往技术的发展会在想象不到的地方开花结果,但一切又是这么理所当然。

06 尾声

ZFE从无到有,从小到大,一切都围绕着安全,最终也将安全作为归宿。正所谓见之于未萌而治之于未乱。

作者简介

王佳君,中通快递高级安全工程师,负责中通零信任安全代理的开发。积极的开源参与者。

https://github.com/iwannay

相关参考

Traefik:

https://doc.traefik.io/traefik/

SDP:

http://blog.nsfocus.net/sdp-iaas/

kubernetes云原生纪元:

https://blog.csdn.net/weixin_37546425/article/details/104179122

阿里哨兵:

https://github.com/alibaba/Sentinel

声明:本文来自中通安全应急响应中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。