零信任安全:SPIFFE 和 SPIRE 通用身份验证的标准和实现

语言: CN / TW / HK

最近正在读 《Solving the Bottom Turtle》  [1] 这本书,这篇是对部分内容的总结和思考,由于内容较多,会分几篇来发。

这本书的副标题是:

a SPIFFE Way to Establish Trust in Your Infrastructure via Universal Identity. 通过通用身份以 SPIFFE 的方式在基础设施中建立信任。

大家记住其中的有几个关键词:通用身份、SPIFFE 的方式、基础设施、建立信任。

背景

零信任是一种异常火热的安全模型,在之前翻译的文章中 《零信任对 Kubernetes 意味着什么》 ,对什么是零信任、为什么要实施零信任,做了初步的介绍。

过去几十年流行的边界安全模型,已经不在适合如今分布式的微服务架构、复杂多样的系统环境以及不可避免的人为失误。从零信任的原则来看,通过安全边界防护的人、系统和网络流量是不可信的,因为“你可能不是你”,身份不可信。

可能有人会问不是有用户名密码、证书么?这些都可能存在泄露,尤其是时间越长泄露的概率越高(在安全领域,只有 0 和 100,所有做不到 100% 的安全,都将其视作 0。这也是笔者对零信任的浅薄理解)。

零信任模型的核心是重塑身份的分配和验证方式,身份是构建零信任模型的基石。

身份

现实世界的身份

在说系统身份之前,先说下现实中的身份。大家都有身份证(Identity Card),生活中用到身份证的场景也越来越多,很多的服务都会查验身份证。为什么身份证就能验证持有人的身份,首先身份证是由政府机构颁发的,在颁发前会查验过申请的人信息(证明你就是你),这个充分可信的;身份证本身做了防伪处理,有特定的查验手段很难被伪造。这里有几个定义:

  • 主体:人

  • 身份:唯一 ID。实际上身份证号这个唯一 ID 就是个人的身份,而姓名只是代称。

  • 身份证明文件:身份证,身份证号、姓名、性别、照片、有效期等等(申请时还会录入生物识别信息)。

  • 颁发机构:政府机构。

再者身份证仅限在国内使用,只有在国内被信任,这个区域就是另一个定义“信任域”。

出国就要使用另一种证件护照,护照是按照国际标准制作和颁发的,可以被其他国家接受,也就是可以信任域间互信。这些互信的信任域,就组成了信任联盟。

这里一共提到了如下的定义:

  • 主体

  • 身份

  • 身份证明文件

  • 颁发机构

  • 信任域

  • 信任联盟

数字世界的身份

数字世界的身份也有与现实世界类似的定义。

数字身份证明是数字世界的主体的身份证明文件,常见的有:X.509 证书、签名的 JWT 和 Kerberos 令牌等。数字身份证明可以使用密码技术进行验证。计算系统可以使用数字身份证明通过验证,就像人使用身份证和护照一样。

为实现这些有一个非常流行的技术 Public Key Infrastructure (PKI) ,其定义了一系列的为创建、管理、分发、使用、存储、撤销数字证书和管理公钥加密所需要的角色、策略、硬件、软件和流程。

接下来简单介绍下 X.509,有经验的同学可以直接跳过。

X.509 概述

1988 年 X.509 作为 PKI 的标准首次发布,X.509 假设有一套严格的层次化的证书颁发机构(CA,Certificate Authority)。申请者要提供证书签名请求(CSR,Certificate Signing Request。申请者通过自己的私钥签名创建的,里面包含了申请者的身份信息,进行用于验真的公钥、请求证书的名称、以及 CA 可能要求的其他身份信息等)。然后 CA 对该 CSR 进行签名转换成合格的证书发回申请方。

CA 将受信任的根证书(包含了公钥)发给所有成员,成员使用根证书中的公钥对其他成员的证书(数字身份证明)进行查验。

procedure-for-request-cert

证书的生命周期

前面讲了证书的颁发流程,每个证书在创建的时候都会设置有效截止日期,如果过期证书就无效了。

证书在过期之前,如果申请方出了问题,证书变成不可信会被 CA 吊销。就像用户名密码一样,有效日期越长出问题的几率就越大。因此一个有效的办法就是 在成本可控的情况下,尽可能缩短证书的有效期。 然后通过更新操作,发放新的证书。

数字身份的使用

  • 认证:服务通过 X.509 证书或者 JWT 向其他服务提供身份信息。

  • 保密性和完整性:保密性是指攻击者无法获取消息的内容;完整性指消息传输的过程中无法被更改。TLS 是广泛使用的协议,用来通过证书在不可信的网络连接上提供认证、保密性和消息完整性。TLS 的特性之一是可以作为双向的 TLS 认证(mutual TLS authentication)。

  • 授权:认证完成之后,使用认证过的数字身份判断是否可以访问服务。

  • 可观测性:唯一的身份可以帮助提升组织内部基础设施的可观测性。

  • 计量:比如微服务架构中常见的限流,可以通过唯一身份标识来管理请求的限额。

说完基础的身份,接下来就是 SPIFFE 和 SPIRE 了。

SPIFFE

Secure Production Identity Framework for Everyone 的缩写 SPIFFE,是一个软件身份标识的开源标准(CNCF 下),为了以组织和平台无关的方式实现身份标识的互操作性,SPIFFE 定义了以全自动方式获取和验证加密身份所需的接口和文档。为构建分布式系统的通用身份的控制平面提供了标准的支撑。这个控制平面就是我们文章开头提到的“基础设施”。作为基础设施,需要是语言、技术栈无关的,并可以做到全自动。

SPIFFE(适用于所有人的安全生产身份框架)以特制 X.509 证书的形式为现代生产环境中的每个工作负载提供安全身份。SPIFFE 消除了对应用程序级身份验证和复杂网络级 ACL 配置的需求。

实施 SPIFFE 有着重要的前提,假设零信任网络安全模型中:

  • 网络通信是不可信的,可能被攻破

  • 运行 SPIFFE 实现的组件的硬件及其运维人员是可靠的

概念

SPIFFE 框架包含了如下几部分:

  • SPIFFE ID:

  • SVID(SPIFFE Verifiable Identity Document)

  • Workload API

  • Trust Bundle

  • SPIFFE 联盟

SPIFFE ID

SPIFFE ID 使用 URI(统一资源标志符)格式的字符串,软件服务的唯一身份标识。由多个部分组成:

  • 前缀  spiffe://
  • example.com
    stagging.example.com
    k8s-cluster.example.com
    
  • 工作负载的名字或者身份标识,比如  /payment/mysql/payment/web-fe 、`/9eebccd2-12bf-40a6-b262-65fe0487d4

比如 spiffe://example.com/bizops/hr/taxrun/withholding

spiffe-id

SVID

SVID 是可以对外提供服务身份标识加密的、可验证的证明文件。SVID 由机构签发,包含了单一 SPIFFE ID,以及服务属于信任域。

目前 SVID 的格式有两种:X.509 和 JWT。

X.509 SVID

SPIFFE ID 位于扩展字段 SAN(Subject Alternative Name)中。作为 SVID,SAN 中只能有一个值(其他场景中,SAN 中可以有多个值)。

建议尽可能使用 X.509 SVID 而不是 JWT-SVID,因为安全性更高,尤其是与 TLS 结合使用。

JWT-SVID

JWT-SVID 将 SPIFFE ID 编码在 标准的 JWT [2] ) 中。在应用层,JWT-SVID 被用作 bearer 令牌向对端提供身份标识。

与 X.509 SVID 不同,JWT-SVID 存在 重放攻击 [3] 的风险,令牌会被未经授权的一方获取并冒用。

SPIFFE 定义了三种机制来避免这种风险:

  • JWT-SVID 必须通过安全通道传输

  • aud 声明必须与令牌针对方的严格字符串匹配
  • 所有的 JWT-SVID 必须包含有效期

Workload API

Workload API 是一个本地的、非联网的 API,最重要的是, 这个 API 无需认证 。工作负载无需调用 API 时无需提供任何信息,就像在操作系统中执行  whoami 操作系统会返回用户名一样,但 workload API 会返回更多的信息。工作负载可以访问其获取当前的身份证明文件、信任包及其他相关信息。

SPIFFE 的实现可以通过创造性的方案(比如操作系统提供的特性)来对调用方(工作负载)进行辨认。

Workload API 作为 gRPC 服务器暴露提供服务,并使用双向流,服务器端的更新可以推送给工作负载。

Trust Bundle

SPIFFE 信任包,是包含了信任域公钥的文件。每种类型的 SVID 在信任包都有其特有的方式,比如 X.509 SVID 中 CA 证书中包含了公钥。

信任包中不会包含除公钥以外的其他敏感内容,可以被随意传播。

SPIFFE 联盟

SPIFFE 联盟有点类似前面讲的护照可以在其他国家来识别身份。

SPIFFE 联盟是一种可以共享 SPIFFE 信任包的简单机制。通常我们希望允许不同信任域中的服务可以安全地进行网络通信。通信的双方检查对方提供提供的信任包,来决定是否进行通信。这是基于双方可以互相暴露或者共享信任包,这种机制被称为包端点(bundle endpoint)。

包端点是个简单的、由 TLS 保护的 HTTP 服务。SPIFFE 的实现通过暴露该端点,使得包的内容可以被获取到。

spiffe-federation

SPIRE

SPIRE 是 SPIFFE 运行时环境(SPIFFE Runtime Environment)的首字母,是一个成产就绪、开源的(同在 CNCF 下) SPIFFE 实现。SPIRE 实现了 SPIFFE 五个定义。

SPIRE 包含了两个主要组件:服务端和代理。服务端负责验证代理和生成 SVID,而代理负责提供 workload API,并与服务端进行通信。

这两个组件的设计都支持插件,可以很容易地扩展支持不同的配置和平台。

SPIRE 架构

服务端

SPIRE 服务端管理和颁发 SPIFFE 信任域中的所有身份标识,使用数据库(SQLite)来保存代理和工作负载的信息。

服务端通过使用注册条目来获知其管理的工作负载,注册条目是为节点和工作负载分配 SPIFFE ID 的灵活规则。

服务端提供了 API 和 CLI 两种访问方式。

服务端为工作负载签发 SVID 时,可以使用其生成的自签发证书(默认),也可以通过上游证书颁发机构插件来配置使用上游证书颁发机构,来完成证书的签发。

代理

代理只有一个非常重要的功能:提供 workload API。为了提供这个 API,还要支持工作负载身份标识的确定、调用并将自身安全地接入 SPIRE 服务端。

SPIRE 代理会从 SPIRE 服务端接收 本地信任域 以及  会直接调用它(SIRE 代理)的工作负载 的信息。

当在指定信任域增加新的工作负载时,会在 SPIRE 服务端创建和更新记录,该工作负载的信息会自动地传播到正确的代理。

spire-architecture

插件架构

spire-plugin-architecture

前面提到服务端和代理都支持插件,通过插件来扩展适应新的 节点证明者 、工作  负载证明者 ,以及上游机构。

SIVD 管理

节点证明过程中会为 SPIRE 代理申请到身份标识,代理使用该标识来完成 SPIRE 服务端的认证。通过认证时候,从服务端获取被授权管理的所有工作负载的 SVID(保存在内存中)。

证明

证明是使用可用信息作为证据确认工作负载身份的过程。在 SPIRE 中有两种证明方式:节点证明和工作负载证明。

节点证明是断言描述节点属性(比如 AWS 自动扩展组的成员);工作负载证明是断言描述工作负载的属性(比如使用的 Kubernetes Service Account,或者二进制文件的路径)。

这些属性在 SPIRE 中统统称为选择器(selector)。SPIRE 提供了大量的开箱即用的选择器,比如支持逻辑、Kubernetes、AWS、GCP、AZure 等等的节点选择器;支持 Docker、Kubernetes、Unix 等的工作负载选择器。

SPIRE 的插件架构设计,也支持更多选择器的扩展。

节点证明

节点证明发生在代理第一次启动时,代理与服务端通信进行信息交换。代理将通过插件采集的信息上报到服务端,服务器端使用对等的插件进行信息的确认。节点证明成功后,节点会拿到身份证明,并使用身份证明与服务端进行后续的通信。

工作负载证明

工作负载证明发生在工作负载与 SPIRE 代理的 workload API 建立连接时。证明的过程完全由 SPIRE 代理完成,代理利用操作系统特性来确认是哪个进程创建的连接。

比如在 Linux 下通过系统调用来获取调用 socket 的对端进程 ID、用户标识和全局唯一标识。不同操作系统的内核元数据不同,拿到工作负载的 ID 之后,代理将 ID 提供给各个插件,从选择器中获取调用者的信息。

每个插件都会对调用者进行检查。比如一个插件从系统内核中获信息,生成取用户、组等选择器;一个插件则会与 Kubernetes 交互,生成命名空间、service account 等选择器;另一个与 Docker daemon 进行交互,生成 Docker 镜像 ID、标签和容器环境变量的选择器。

workload-attestation

注册条目

SPIRE 通过注册条目获知工作负载的各种信息,比如工作负载是否应该或允许出现在环境中、应该运行的位置、SPIFFE ID 和其他信息。这些信息是通过 SPIRE API 创建和管理的。

每个注册条目包含了 3 个核心属性:

  • Parent ID:告诉 SPIRE 工作负载运行在哪里,进一步感知哪些代理有权获取其 SVID。

  • SPIFFE ID:工作负载可以使用的 SPIFFE ID

  • 可以用来辨识工作负载的信息:也就是选择器从证明中获取到的

core-attributes-of-registration-entry

注册条目可以描述一组节点或者一个工作负载,通常后者通过 Parent ID 来引用后者。

节点条目

描述一个节点或者一组节点的注册条目使用节点证明生成的选择器来分配 SPIFFE ID,在注册工作负载时可以引用该 ID。

一个节点可以被多个节点条目的选择器证明,因此可以存在于多个组中。

SPIRE 服务端一次可以加载多个节点证明器,而代理一次只能加载一个。

注册条目的 Pareent ID 指向的是 SPIRE 服务端的 SPIFFE ID。

工作负载条目

工作负载条目的 Parent ID 是节点(一个或者一组)的 SPIFFE ID。表示其可以运行在哪个或哪组节点上。该节点上的 SPIRE 代理收到该工作负载的条目,包含了在颁发 SIVD 之前必须要证明的选择器。

与节点证明过程不同,SPIRE 代理一次可以加载多个工作负载证明器插件。

SPIFFE/SPIRE 应用概念威胁模型

安全边界

安全边界通俗地讲,就是从不安全的一方到安全的一方,并且都有其各自的证明方式。

SPIFFE/SPIRE 中定义了 3 个安全边界:工作负载与代理之间、代理与服务端之间、不同信任域的服务端之间。

工作负载和其他信任域中的服务端是完全不可信的,网络通信也是完全不可信的。

spire-security-boundary

工作负载与代理的安全边界

代理不会信任工作负载的任何输入,代理对工作负载的检查都是通过外部检查完成的。换句话说,所有值可以被工作负载修改的选择器,也是不安全的。

代理与服务端的安全边界

代理比工作负载安全一些,但是安全性不如服务端。SPIRE 的一个明确设计目标是它应该在节点被攻破后存活。

代理负责根据工作负载的行为来创建和管理标识,但是也有必要限制代理的能力在其职责范围内(最少权限原则)。

代理在获取标识前,必须能有证明其对注册条目的所有权。

在节点尚未证明自己获得标识之前,与服务端的通信使用单向 TLS;获得身份标识和有效的 SVID 之后,使用双向的 TLS。

服务端之间的边界

SPIRE 服务端仅被信任在其直接管理的信任域内生成 SVID。

各组件被攻破的影响

假如代理被攻破,会影响其管理的工作负载。如果工作负载跟代理是 1:1 部署时(参考 Kubernetes Pod 中的多 sidecar),只会影响一个工作负载。但是代理管理多个工作负载时,影响范围变大(参考 Kubernetes 中的 Daemonset,每个节点运行一个副本)。

SPIRE 服务端是整个系统中最为敏感的组件,如果被攻破会影响到整个信任域,强烈建议将其部署到与要管理的工作负载不同的硬件上。

概念威胁模型 vs 传统边界安全模型

这个概念威胁模型,实际上是将传统安全边界模型粒度进行细化,将隔离区缩小并进行自治。缩得越小,安全性越高,破防时影响的范围越小。

总结

本篇主要介绍了零信任安全模型中身份定义,以及通用身份验证的标准和实现 SPIFFE/SPIRE。

回到开头书的副标题中的几个关键词:通用身份、SPIFFE 的方式、基础设施、建立信任。SPIFFE 提供了覆盖了身份、身份证书的类型以及创建、管理和颁发方式的定义,贯彻了零信任的安全模型,为构建基础设施提供了标准框架。SPIRE 则提供了基础设施的实现,使用服务端 + 代理的分级策略采用概念威胁模型,尽可能的提升安全性和降低威胁带来的影响。

标准和实现都有了,实际上落地实施也是一个复杂的过程。书中也提供了落地计划的设计方案以及分享落地的经验,后续会继续你大家解读。

参考资料

[1] 

《Solving the Bottom Turtle》 : https://spiffe.io/book/

[2] 

标准的 JWT: https://tools.ietf.org/html/rfc7519

[3] 

重放攻击: https://zh.wikipedia.org/wiki/重放攻击