编者的话
本文翻译节选自 A Kubernetes engineer’s guide to mTLS,为了便于读者理解,笔者对原文做了一点修改 1。因为笔者最近在研究 Istio 中的身份认证及 SPIFFE,对如何在 Kubernetes 中应用 mTLS 以及为什么要使用 mTLS 产生了浓厚的兴趣,再回想起五年前手动安装 Kubernetes 时,因为给集群开启 TLS 问题而导致安装停滞不前。
本文的主要观点是:在 Kubernetes 中我们不能仅依靠网络层加密,还需要 mTLS 来对客户端和服务端进行双向的传输层认证。本文将聚焦于 TLS 的真实性,以及证书管理的难题,说明服务网格对于在 Kubernetes 中开启 mTLS 带来的便利。
简介
Mutual TLS(双向 TLS),或称 mTLS,是 Kubernetes 中的一个热门话题,尤其是对于那些负责为应用程序提供传输层加密的人来说。但是,你有没有考虑过,什么是 mTLS,它提供什么样的安全,为什么需要 mTLS?
本指南我将介绍什么是 mTLS,它与常规 TLS 的关系,以及为什么它与 Kubernetes 有关。我还会谈论 mTLS 的一些优缺点及替代方案。
什么是 mTLS?
对于常规 TLS,只需要服务端认证,mTLS 相对来说有一个额外的规定:客户端也要经过认证。但这意味着什么,为什么要这样做呢?
在回答这些问题之前,我们需要先对 TLS 有一个基本的了解。TLS 是一个传输层协议,旨在为 TCP 连接提供安全保障(我们将在下面看到安全的确切含义)。TLS 在传输层工作,可以与任何使用 TCP 的应用层协议结合使用。例如,HTTPS 是 HTTP 与 TLS 的结合(HTTPS 中的 S 指的是 SSL,即 TLS 的前身),HTTP 不需要做任何改变来适应 TLS 2。
因为 TLS 中存在各种各样的问题,使得其从安全的角度来看是不理想的。TLS 规范复杂,而且没有得到充分的说明,有些地方并没有真正的意义,而且不管怎样,实现起来也不会 100% 符合 TLS 规范。
尽管有这些担忧,TLS 还是无处不在。你现在就在使用 TLS:这个页面是通过 HTTPS 提供的,你可能在浏览器的 URL 栏中看到一个小锁图标。
TLS 提供什么样的安全性?
大多数人把 TLS 与加密联系起来。但 TLS 不仅仅是这样。TLS 为连接提供了三种安全保证:
- 真实性:任何一方都能证明他们是自己所声称的身份。
- 保密性:其他人无法看到正在交换的数据。
- 完整性:收到的数据与发送的数据相同。
因此,虽然 TLS 确实给你提供了加密 —— 这就是它实现保密的方式 —— 但从 TLS 的角度来看,这对安全通信来说是不够的:你需要所有这三种属性。如果你没有真实性,那么有人就可以在连接的另一端进行欺骗。如果你没有完整性,那么有人可以修改通信中的关键信息。如果你没有保密性,那么任何人都可以监听。
在这三种保证中,本文主要关注真实性。
mTLS 什么时候有用?
回到我们最初的定义:mTLS 是简单的常规 TLS,其中有一个额外的规定,即客户端也要经过认证。有了对 TLS 的基本了解,我们现在可以解析这个声明了。TLS 保证了真实性,但默认情况下,这只发生在一个方向上:客户端对服务器进行认证,但服务器并不对客户端进行认证。
为什么 TLS 的默认只在一个方向进行认证?因为客户端的身份往往是不相关的。例如,在加载这个页面时,你的浏览器已经验证了要访问的网站服务端的身份,但服务端并没有验证你的浏览器的身份。它实际上并不关心你的浏览器的身份。
当然,不验证客户端身份对于提供网页服务是有意义的,但有很多类型的通信,客户端的身份也很重要。例如 API 调用:如果你调用像 GitHub 这样的服务,那么 GitHub 需要知道你是谁 —— 除其他原因外,这样他们就可以给你发送账单。如果不向 GitHub 提供某种客户端身份,你就不能对 GitHub 的 API 进行调用。
但 GitHub 并不使用 mTLS。相反,你通过给 GitHub 一个秘密的认证令牌(token)来认证自己,这个令牌是创建账户时分配给你的。坦率地说,mTLS 设置起来很烦人(后面会有很多这方面的内容),所以如果你提供像 GitHub 这样的公共 API,你可能会只使用 auth token。
然而,使用 mTLS 的认证有一些非常强大的 auth token 方法没有的优势。首先,mTLS 认证可以完全在应用程序之外完成,不需要任何应用程序级别的功能来创建、注册或管理身份。使用 auth token 时,在你进行第一次 GitHub API 调用之前,你需要登录网站,创建一个账户,获得令牌 3。GitHub 的 API 必须知道这个 auth token,并提供将其传递给 API 调用和管理它的方法。但有了 mTLS,一个全新的客户端就可以直接认证自己,即使从没有人见过它。而应用程序不需要知道任何关于认证的事情,也不需要提供端点来管理认证。
综上所述,我们看到 mTLS 非常适合以下情况:
有种场景具有所有这些特征,那就是微服务!
使用 mTLS 来保护微服务的安全
mTLS 是保证微服务之间跨服务通信安全的好方法,原因就在上面。
首先,你想要安全的通信。当我们把我们的应用程序拆分为多个服务时,我们最终会在这些服务之间的网络上发送敏感数据。任何能够进入网络的人都有可能读取这些敏感数据并伪造请求。
第二,你关心客户端的身份。首先,你要确保你能知道调用是什么时候发生的,以便进行诊断,并正确记录指标等事项。此外,你可能想对这些身份进行授权(允许 A 调用 B 吗)。我们将在后面讨论更多关于授权的问题。
第三,你并不真的想为管理服务身份建立应用级的流程。这不是业务逻辑,开发人员的时间最好用在其他地方。
最后,如果你控制了平台,你实际上可以管理实施 mTLS 的复杂性。或者至少,比 GitHub 做得更好。在我们的 GitHub 例子中,每个用户都必须解决对 GitHub 进行身份验证的难题。这个挑战越难,对用户就越不利(对 GitHub 的底线也越不利)。但是,如果我们能在平台层面上实现 mTLS,我们就能一次性支付成本,而不是为每个服务或每个用户支付。
综上所述,mTLS 非常适用于确保微服务之间的通信安全。但是有一个问题。
实施 TLS 的难点:证书管理
到目前为止,我们已经为 mTLS 描绘了一幅美好的图景。客户端和服务器愉快地相互认证,然后它们之间的通信就安全了。在实践中,阻碍 mTLS 工作的最大挑战是证书管理。
TLS 中的认证是通过 公钥密码学和公钥基础设施(PKI)进行的 。这两者本身就是一个巨大的话题,在这篇文章中我们不会去讨论这些细节。但简而言之,它们涉及大量的证书。
TLS 认证基于 X.509 证书。X.509 证书中包含身份和公钥。公钥有一个相应的私钥,它不是证书的一部分。TLS 认证分两步,第一步是向对方展示你的证书,然后用私钥来证明证书中包含的身份属于你(公钥密码学的神奇之处在于,任何复制证书的人都无法进行这种证明,因为他们没有私钥。因此,你可以非常自由地使用证书,包括通过明文渠道发送证书或将其存储在公开场合)。
X.509 证书是由一个 证书授权机构(Certificate Authority,简称 CA) 签署,其中包括受 CA 信任该的身份。证书用于 TLS 认证的第二步:如果有人向你展示他们的身份并证明他们拥有该身份,你现在必须决定是否信任该身份。TLS 在这里使用了一个简单的规则:如果证书是由 CA 签署的,且你信任该 CA,那么你就应该信任该身份。如何验证 CA 对证书的签名?通过使用该 CA 本身的 X.509 证书。怎么知道是否应该信任该 CA?嗯,这个就与 TLS 协议本身无关,你会被外界告知应该信任它 4。
CA 也签发证书。要获得证书,你首先要创建公钥和私钥对。你保留私钥,嗯,私钥 —— 千万不要在网络上发送私钥 —— 你向 CA 发送一个包含公钥和你身份的证书签名请求(Certificate Signing Request,简称 CSR)。如果 CA 批准了这个请求,它就会创建和签署证书,并把证书发送给你。
所以,证书管理就是就成了证书创建和分发流程中的挑战。我们需要确保有一个 CA,每个服务都可以向其发送 CSR,而且 CA 可以把证书发送给服务。我们还需要确保 CA 的安全,没有人能够访问任何服务的私钥,而且每个服务都知道自己的身份,而且不能被改变。
在 Kubernetes 这样的环境中,服务实际上是一组不断变化的副本,可以随时创建或销毁,每个副本都需要自己的证书,这使得证书分发的挑战更加严峻。
而且,由于在实践中,减少证书暴露损失(即当有人未经授权获得秘钥时)的最好方法是证书轮换:缩短证书的寿命,在证书过期前重新颁发。这意味着我们需要每隔 n 小时为每个副本重复整个证书请求和签名的流程。
如果我们想在多个集群之间扩展安全通信,需要一种方法来确保在一个集群中产生的身份可以被其他集群所使用,而且如果某个集群被破坏,我们可以禁用该集群而不禁用其他集群,这就进一步增加了证书管理的复杂性,因为这将产生更多的证书。
总之,实施 mTLS 涉及到管理大量的证书,消耗大量的时间。这一挑战的复杂性令人生畏。但尽管如此,mTLS 在 Kubernetes 的世界里已经看到了一些复兴的趋势。这是因为有一门技术使 mTLS 变得可行:服务网格。
Kubernetes、mTLS 和服务网格
服务网格是为集群开启 mTLS 的一个绝佳的机制。它不仅可以处理证书管理的挑战,还可以处理建立和接收 TLS 连接本身。它使得为集群添加 mTLS 成为一个零配置的操作:当你在 Kubernetes 集群上安装服务网格的时候,网格化的 pod 之间的所有通信都自动被 mTLS 化。对于像 mTLS 这样复杂的东西来说,这是很不可思议的。
这一切之所以能够实现,是因为 Kubernetes 使一些本来非常复杂的事情,如 sidecar 模式,变得简单易行。得益于 Kubernetes,服务网格可以:
- 透明地将一个 sidecar 代理注入到每个应用程序的 pod 中,并通过该代理路由所有进出 pod 的 TCP 通信。
- 将一个内部 CA 作为其控制平面的一部分,签发 TLS 证书,并将该 CA 的证书安全地分配给所有代理。
- 使用这个 CA 向每个代理发放短期的证书,与 pod 的 Kubernetes ServiceAccount 身份相联系。
- 每隔 N 小时重新签发这些证书。
- 让每个代理对所有使用这些证书的 pod 的连接执行 mTLS,确保客户端和服务器双方都有有效的身份。
- 在连接进入应用程序之前,使用这些身份应用授权策略。
当然,这只是一种简化的描述。例如,Linkerd 实际上使用了两级 CA,一个在集群层面,一个在全局层面,以便允许跨集群通信。Linkerd 可以使用多个信任根,所以你也可以轮流使用 CA。
常见问题:mTLS 实际上能保护什么?
事实上,mTLS 只能用于防止特定的攻击:未经授权的网络访问。阻止入侵者嗅探网络请求中的内容,阻止冒充服务进行访问。
但是有很多东西是 mTLS 不能保护的,例如未经授权主机访问。如果黑客入侵进了主机,mTLS 保护就无济于事了:入侵者可以读取密匙,嗅探或欺骗连接,颠覆 CA 并造成破坏,或任何其他恶意活动。
确保 Kubernetes 的安全并不容易,实际上 mTLS 只解决了 Kubernetes 的一小部分安全漏洞。
mTLS 与 IPSec 或 Wireguard 等网络层加密相比怎么样?
在 Kubernetes 中,一些 CNI 插件如 Calico 和 Cilium 可以通过 IPSec 或 Wireguard 等协议提供网络层加密。像服务网格一样,这种网络层加密可以提供传输层加密,而应用程序本身不需要做任何事情。
虽然网络层加密可以与 mTLS 结合使用,作为一种深度防御的形式,但有几个原因可以说明网络层加密不足以替代 mTLS。
如你所料,网络层加密的最大缺点是围绕身份的。因为它们在网络层工作,Wireguard 和 IPSec 只能提供网络身份,而不是工作负载身份。换句话说,它们不是使用工作负载本身固有的身份(比如 SPIFFE 或者 Kubernetes 的 ServiceAccount),而是使用该工作负载运行的 IP 地址。
依靠网络身份有一系列的问题,包括:
由于这些原因,在 Kubernetes 中 mTLS 为你提供了比仅仅依靠网络层加密更好的安全态势。
参考
本文删除了原文中的 Linkerd 安装的部分,将 Twillio 替换成国内读者比较熟悉的 GitHub。 ↩︎
至少,在协议层面。在实践中,随着 HTTPS 的引入,HTTP 的使用方式肯定已经发生了变化。例如,像 HSTS 这样的功能现在被用来防止 HTTPS 可能发生的某些类型的攻击。 ↩︎
尽管这个客户端令牌流程没有使用 TLS 客户端认证,但它仍然依靠 TLS 服务器认证来保证安全。TLS 确保令牌来自 GitHub 而不是一个伪装者。 ↩︎
例如,你的浏览器带有知名公共 CA 的证书,如 Verisign、Digicert 等,这些证书在发布时被打包在一起。当你下载 Firefox 时,你相信 Mozilla 已经把正确的证书放进了浏览器。对于集群内的通信,我们将创建我们自己的 CA,这意味着我们也必须以一种安全的方式,将这个 CA 的证书分发给集群的每个部分,这些部分需要做出信任决定。 ↩︎