每个人都在讨论微服务,每个人也都希望能够实现微服务架构,而微服务安全也日渐成为大家关注的重要问题。今天小数与大家分享的文章,就从应用层面深入探讨了应对微服务安全挑战的方案,为微服务安全提供了新的思路。
面向服务架构(简称SOA)引入了一类设计规范,其核心思路在于采用高度解耦式服务部署,其中各项服务可通过一套标准信息格式经由网络实现彼此通信。这套方案与具体技术无关,即不考虑各项服务具体是如何实现的。每项服务都拥有一个明确定义,用于发布服务描述或者服务接口。在实践当中,这类信息格式通过SOAP实现标准化——即由W3C于2000年初推出的一项标准——同时亦基于XML——其中服务描述由WSDL(另一项W3C标准)进行标准化,而服务发现标准由UDDI(同样为W3C标准)实现。这一切正是基于SOAP的Web服务的实现基础,甚至使得Web服务在一定程度上成了SOA的代名词。不过这种实现方式在架构模式层面也有着自己的缺陷。SOA的基本原则正被时代所逐步淘汰,如今由OASIS提供的WS-*堆栈(包括WS-Security, WS-Policy, WS-Security Policy,WS-Trust, WS-Federation, WS-Secure Conversation, WS-Reliable Messaging, WS-Atomic Transactions, WS-BPEL等等)令SOA的复杂性不断提高,这也直接导致很多普通开发者发现自己很难对其加以驾驭。
多年之后,如今我们得以再次开启这段通往SOA基本原则的旅途——但这一次它有了新的名号,即微服务。微服务能够为应用程序设计提供一种更具针对性、范围性与模块性的实现方案。
微服务可谓当下一大热门词汇之一,与之并驾齐驱的则包括物联网、容器化与区块链。“微服务”一词最初于2011年5月亮相于威尼斯软件架构师研讨会。这个词汇用于解释一类常见的架构类型。
大家已经意识到微服务并不仅仅是做对了的SOA,它也不只是一种架构模式——而是一种围绕架构模式展开的全新文化。其由主要目标作为驱动力,旨在实现快速部署与快速生产。
在保护微服务安全时,需要从以下几个角度入手:
保护开发生命周期与测试自动化机制:微服务背后的核心驱动力在于提升投付生产的速度。我们需要向服务当中引入变更,加以测试而后立即将成果部署至生产环境。为了确保在代码层面中不存在安全漏洞,我们需要制定规划以进行静态代码分析与动态测试——更重要的是,这些测试应当成为持续交付流程的组成部分。任何安全漏洞都需要在早期开发周期内被发现,另外反馈周期也必须尽可能得到缩短。
DevOps安全:微服务部署模式可谓多种多样——但其中使用最为广泛的当数每主机服务模式。其中的主机指定的并不一定是物理设备——也很可能属于容器(Docker)。我们需要对容器层面的安全进行关注。我们该如何确保各容器之间得到有效隔离,又该在容器与主机操作系统之间采取怎样的隔离水平?
应用级别安全:我们该如何验证用户身份并对其微服务访问操作进行控制,又要怎样保障不同微服务之间的通信安全?
在今天的文章中,我们将提供一整套安全模式,旨在解决应用层级所面临的各类微服务安全保护挑战。
单体应用VS微服务
在整体型应用程序中,所有服务都被部署在同一应用服务器当中,而该应用服务器本身则提供会话管理功能。其中不同服务间的接口为本地调用,且全部服务皆可共享用户的逻辑状态。每项服务(或者组件)不需要对用户进行验证。验证工作集中由拦截器处理,其拦截所有服务调用并审查其是否可以放行。验证完成之后,其会在不同平台上的不同服务(或者组件)间发送用户登录凭证。以下示意图解释了整体应用程序中各不同组件间的交互方式。
在Java EE环境下,拦截器可以由servlet过滤器充当。该servlet过滤器会拦截全部来自其已注册上下文的请求,并强制进行验证。该服务调用要么携带有效的凭证,要么拥有能够映射至某个用户的会话令牌。一旦servlet过滤器找到该用户,则会创建登录上下文,并将其传递给下游组件。每个下游组件都能够从该登录上下文内识别出用户以完成授权。
在微服务环境下,安全性往往成为最大的挑战。在微服务架构当中,各服务分布及部署在分布式设置当中的多套容器之内。各服务接口不再存在于本地,而是通过HTTP进行远程接入。以下示意图显示了不同微服务之间的交互方式。
这里的挑战在于,我们要如何验证用户并在不同微服务之间以对称方式完成登录上下文传递,随后还要想办法让微服务完成对用户的授权。
保护服务到服务通信
在今天的文章中,我们将探讨两套方案,旨在保护服务到服务通信。其一基于JWT,其二则基于TLS相互验证。
JSON Web令牌(简称JWT)
JWT(即JSON Web令牌)负责定义一套容器,旨在完成各方之间的数据传输。其可用于:
在各方之间传播其中一方的身份。
在各方之间传播用户权利。
通过非安全通道在各方之间安全实现数据传输。
根据JWT受信指标判断用户身份。
已签名JWT被称为JWS(即JSON Web签名),而加密JWT则被称为JWE(即JSON Web加密)。事实上,JWT并不会以自身原始方式存在——其要么作为JWS,要么作为JWE,它像是一种抽象类——JWS与JWE为其具体实现方式。
来自某一微服务并将被传递至另一微服务的用户上下文可伴随JWS一同传递。由于JWS由上游微服务的某一已知密钥进行签名,因此JWS会同时包含有最终用户身份(在JWT中声明)以及上游微服务身份(通过签名实现)。为了接收JWS,下游微服务首先需要根据JWS本身中的嵌入公钥对JWS的签名进行验证。这还不够,我们还需要检查该密钥是否受信。不同微服务之间可通过多种方式建立受信关系。其一为由服务为各服务配置受信证书。很明显,这种方式在规模化微服务部署环境中并不可行。因此我建议大家建立一套专有证书中心(简称CA),同时可以为不同微服务组设置中介证书中心。现在,相较于互相信任及各自分配不同的证书,下游微服务将只需要信任根证书授权或者中介机制即可。这能够显著降低证书配置所带来的管理负担。
JWT验证的成本
每项微服务都需要承担JWT验证成本,其中还包含用于验证令牌签名的加密操作。微服务层级中的JWT会进行缓存,而非每次进行数据提取,这就降低了重复令牌验证造成的性能影响。缓存过期时间必须与JWT的到期时间相匹配。正是由于利用这种机制,因此如果JWT的过期时间设定得太短,则会给缓存性能造成严重影响。
验证用户
JWT在其声明集中包含一项参数,名为sub,其代表拥有该JWT的主体或者用户。JWT本身也可以包含各类用户属性,例如first_name、last_name、email等等。如果任何微服务需要在其操作过程中识别此用户,则需要查看对应的属性。Sub属性的值对于给定发行者而言是惟一的。如果大家拥有一项微服务,其能够从多个发行者处接收令牌,那么该用户的惟一性应被认定为该发行者与sub属性的结合体。
而aud参数同样存在于JWT声明集内,负责指定令牌的目标受众。其可以是单个接收者或者是一组接收者。在执行任何验证检查之前,该令牌接收者都必须首先查看是否发布了特定JWT供其使用,如果没有则立即拒绝。令牌发送方需要在发出令牌之前,确定该令牌实际接收者的身份,同时aud参数值必须属于令牌发送方与接收方间预先约定的值。在微服务环境中,我们可以利用正规表达式来验证令牌受众。举例来说,令牌中的aud值可以为*.facilelogin.com,意味着facilelogin.com域名下的每个接收方(例如foo.facilelogin.com,bar.facilelogin.com等)都能够拥有自己的aud值。
TLS相互身份验证
在TLS相互验证与JWT方法当中,每项微服务都需要拥有自己的证书。这两种方法的区别在于,JWT验证机制中JWS可同时携带最终用户身份以及上游服务身份。而TLS相互验证则只在应用层传输最终用户身份。
证书吊销
在以上提到的两种方案当中,证书吊销都是项棘手的任务。证书吊销尽管难以实现,但仍然存在多种选项供我们选择:
CRL (证书吊销列表 / RFC 2459)
OCSP (在线证书状态协议 / RFC 2560)
OCSP Stapling (RFC 6066)
OCSP Stapling Required (尚处于草案阶段)
CRL的使用频率并不高。客户端在发起TLS握手时,必须从对应的证书颁发中心处获取一份长长的吊销证书列表,而后检查服务器证书是否被列入该列表。相较于每一次进行列表获取,客户端可以在本地对CRL进行缓存。在此之后,大家还需要考虑如何避免以陈旧数据为基础做出判断的问题。当TLS相互验证机制被使用时,服务器也需要针对客户端进行同样的证书验证。最终,人们发现CRL的实际效果其实并不理想,因此新的解决方案也应运而生——这就是OCSP。
在OCSP当中,一切元素的实际效果都要比CRL好上那么一点。TLS客户端能够检查特定证书的状态,且无需从证书中心处下载完整的吊销证书列表。换句话来说,当客户端每次与新的下游微服务进行通信时,其都必须同对应的OCSP响应方沟通以验证当前服务器(或者服务)的证书状态——而服务器则必须面向客户端证书执行同样的操作。如此一来,OCSP响应方同样面临着巨大的流量压力。基于同样的考虑,客户端仍然可以对OCSP决策进行缓存,但这无疑继续带来同样的、基于陈旧数据进行决策的可能性。
而OCSP stapling的出现令客户端不再需要每次同下游微服务进行通信时,都与OCSP响应方“打招呼”。该下游微服务将从对应的OCSP响应方处获取OCSP响应,以及staple,或者将响应附加到证书本身当中。由于OCSP响应得到了对应证书中心的签名,因此该客户端能够验证通过其签名并接收此响应。这种方法令事情有了转机,事实上如今是由服务而非客户端与OCSP响应方进行通信。不过在TLS相互验证模式下,OCSP stapling相较于原始OCSP无法带来任何额外优势。
由于OCPS必须配合stapling,该服务(即下游服务)需要向客户端(即上游服务)提供保证,证明OCSP响应被附加到了该服务在TLS握手时接收到的证书中。如果OCSP响应未被附加至该证书中,那么结果并非出现软错误,而是客户端必须立即拒绝该连接。
临时证书
从最终用户的角度来看,临时证书的效果与目前的常规证书并无区别,只不过暂时证书的过期时间非常之短。TLS客户端并不需要针对临时证书进行CRL或者OCSP验证,而是坚持设定好的过期时间,并对证书本身进行时间戳加盖。
Netflix与临时证书
临时证书带来的最大挑战在于其部署与维护工作。自动则正是解决这些难题的灵丹妙药。Netflix公司建议使用分层方案以构建临时证书部署机制。大家可以在TPM(即受信平台模块)或者SGX(软件保护扩展)当中获得系统身份或者长期证书,从而显著提升安全性。在此之后,再使用这些凭证作为临时证书。最后,在微服务中使用临时证书——这些证书亦可由其它微服务使用。每项微服务都能够利用自身长期证书对临时证书进行定期刷新。当然,仅仅拥有临时证书还不够——托管该服务(或者TLS终止器)的主机应当支持对服务器证书的动态更新。目前存在大量能够运行服务器证书动态重载的TLS终止器,但其中大多数可能会导致短暂的服务停机。
边界安全
微服务集与外部世界的连通一般经由API网关模式实现。利用API网关模式,需要进行声明的微服务能够在该网关内获得对应的API。当然,并不是所有微服务都需要立足于API网关实现声明。
最终用户对微服务的访问(通过API实现)应当在边界或者API网关处进行验证。目前最为常见的API安全保护模式为OAuth 2.0。
OAuth 2.0
OAuth 2.0是一套作为访问代表的框架。它允许某方对另一方进行某种操作。OAuth 2.0引入了一系列grant types。其中之一用于解释协议,客户端可利用此协议获取资源拥有方的许可,从而代表拥有方进行资源访问。另外,还有部分grant types可解释用于获取令牌的协议,且整个操作完全等同于由资源拥有方执行——换言之,该客户在这种情况下即相当于资源拥有方。以下示意图解释了OAuth 2.0协议的宏观实现流程。其中描述了OAuth客户端、资源拥有方、验证服务器以及资源服务器之间的交互方式。
要想通过API网关访问某项微服务,请求发起方必须首先获得有效的OAuth令牌。系统能够以自身角色访问微服务,也可以作为其他用户实现访问。对于后一种情况,假设用户登录至某Web应用,那么此后该Web应用即可以所登录用户的身份进行微服务访问。
下面来看端到端通信的具体实现方式,如上图所显示:
用户通过Identity Provider登录至Web应用/移动应用,而Web应用/移动应用则通过OpenID Connect(也可以是SAML 2.0)信任该Provider。
该Web应用获取一条OAuth 2.0 access_token与一条id_token。其中id_token将验证访问该Web应用的最终用户。如果使用SAML 2.0,则该Web应用需要与其信任的OAuth验证服务器的token端点进行通信,同时将SAML令牌交换为一条OAuth acess_token,随后交换OAuth 2.0的SAML 2.0 grant type。
该Web应用会作为最终用户调用一个API——并随同API请求发送access_token。
API网关会拦截来自该Web应用的请求,提取access_token,与令牌交换端点(或者STS)进行通信,并由后者验证该acess_token,而后向该API网关提供JWT(由其签名)。此JWT还携带有用户上下文。在STS对acess_token进行验证时,其还将通过introspection API与对应的OAuth授权服务器进行通信。
API网关向下游微服务将同时发出请求与JWT。
每项微服务都会验证其接收到的JWT,而后作为下游服务调用,其能够创建新的自签名JWT并将其与该请求一同发送。在其它方案中,亦会用到嵌套JWT——即由新的JWT携带上一JWT。
在上述流程当中,来自外部客户端的API请求将经由该API网关。当某项微服务与其它微服务通信时,其将不再需要经过该网关。另外,从特定微服务的角度来看,无论大家是从外部客户端还是其它微服务处获取请求,获得的都是JWT——也就是说,这是一种对称安全模式。
访问控制
授权属于一项业务功能。每项微服务可以决定使用何种标准以允许各项访问操作。从简单的授权角度来讲,我们可以检查特定用户是否向特定资源执行了特定操作。将操作与资源加以结合,也就构成了权限。授权检查会评估特定用户是否具备访问特定资源的最低必要权限集合。该资源能够定义谁可以进行访问,可在访问中具体执行哪些操作。为特定资源声明必要权限可通过多种方式实现。
XACML (可扩展访问控制标记语言)
XACML已经成为细粒度访问控制领域的客观标准。其引入的方式能够代表访问某种资源所需要的权限集,且具体方法采用基于XML的特定域语言(简称DSL)编写而成。
上图所示为XACML组件架构。首先,策略管理员需要通过PAP(即策略管理点)定义XACML策略,而这些策略将被保存在策略存储内。要检查特定实体是否拥有访问某种资源的权限,PEP(即策略执行点)需要拦截该访问请求、创建一条XACML请求并将其发送至XACML PDP(即策略决策点)。该XACML请求能够携带任何有助于在PDP上执行决策流程的属性。举例来说,其能够包含拒绝标识符、资源标识符以及特定对象将对目标资源执行的操作。需要进行用户授权的微服务则需要与该PDP通信并从JWT中提取相关属性,从而建立XACML请求。PIP(即策略信息点)会在PDP发现XACML请求中不存在策略评估所要求的特定属性时介入。在此之后,PDP会与PIP通信以找到缺失的对应属性。PIP能够接入相关数据存储,找到该属性而后将其返回至PDP。
嵌入式PDP
远程PDP模式存在几大弊端,其可能与微服务的基本原则发生冲突:
性能成本:每一次被要求执行访问控制检查时,对应微服务都需要通过线缆与PDP进行通信。当该决策被缓存在客户端时,此类传输成本与策略评估成本将得到有效降低。不过在使用缓存机制时,我们亦有可能根据陈旧数据进行安全决策。
策略信息点(简称PIP)的所有权:每项微服务都应当拥有自己的PIP,其了解要从哪里引入实现访问控制所必需的数据。在以上方案中,我们建立起的一套“整体式”PDP,其中包含全部PIP——对应全部微服务。
如上图所示,嵌入式PDP将遵循一类事件模式,其中每项微服务都会订阅其感兴趣的主题以从PAP处获取合适的访问控制策略,而后更新其内嵌PDP。大家可以通过微服务组或者全局多租户模型获取PAP。当出现新策略或者策略存在更新时,该PAP会向对应的主题发布事件。
这套方案不会违反微服务中的“服务器不变”原则。“服务器不变”意味着当大家在持续交付流程结尾处,直接利用加载自库的配置构建服务器或者容器时,整个创建流程应该能够基于同样的配置进行不断重复。因此,我们不希望任何用户登入服务器并对配置做出变更。在内嵌PDP模式下,尽管服务器会加载对应的策略,但其仍同时处于运行当中。这意味着当我们启动新容器时,其仍然立足于同样的策略集。
在结束本篇文章之前,我们还有另一个重要的问题需要回答,即API网关在授权上下文中到底扮演着怎样的角色。我们可以设置全局可访问的访问控制策略——其可用于最终用户,并由网关进行强制执行——但无法设置服务层级的策略。因为顾名思义,服务层策略必须在服务层上执行。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。