面向服务架构(简称 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 基本原则的旅途——但这一次它有了新的名号,即微服务microservice。微服务能够为应用程序设计提供一种更具针对性、范围性与模块性的实现方案。
微服务可谓当下一大热门词汇之一,与之并驾齐驱的则包括物联网、容器化与区块链。“微服务”一词最初于 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 无法带来任何额外优势。
由于 OCSP 必须配合 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 type。其中之一用于解释协议,客户端可利用此协议获取资源拥有方的许可,从而代表拥有方进行资源访问。另外,还有部分 grant type 可解释用于获取令牌的协议,且整个操作完全等同于由资源拥有方执行——换言之,该客户在这种情况下即相当于资源拥有方。以下示意图解释了 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)进行通信,并由后者验证该 access_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 网关在授权上下文中到底扮演着怎样的角色。我们可以设置全局可访问的访问控制策略——其可用于最终用户,并由网关进行强制执行——但无法设置服务层级的策略。因为顾名思义,服务层策略必须在服务层上执行。
来源:莫然博客,欢迎分享本文!