现在我们已经成功地安装了 Kubernetes,成功部署的最基本方面之一是围绕一致的用户管理。与任何多租户、分布式系统一样,用户管理构成了 Kubernetes 最终如何验证身份、确定适当的访问级别、实现自助服务功能和保持可审计性的基础。

在本章和下一章中,我们将探讨如何充分利用 Kubernetes 的身份验证和访问控制功能。但要想真正了解这些构造的工作原理,首先要了解 API 请求的生命周期。

每一个进入 API Server 的 API 请求都需要成功地通过一系列挑战,如图 7-1 所示,然后服务器才会接受(并随后采取行动)该请求。这些测试中的每一项都属于三组中的一组:认证、访问控制和准入控制。
image.png
图 7-1 Kubernetes API 请求流

这些挑战的数量和复杂程度取决于 Kubernetes API Server 的配置方式,但最佳实践要求生产集群以某种形式或方式实现这三个方面。

服务 API 请求的前两个阶段(身份验证和访问控制)侧重于我们对用户的了解。在本章中,我们将从 API Server 的角度提供对用户是什么的理解,并最终提供如何利用用户资源为集群提供安全的 API 访问。

用户

用户一词与你和我(甚至可能与你的持续交付工具)如何连接并获得对 Kubernetes API 的访问有关。在最常见的情况下,用户通常是从一些外部地方连接到 Kubernetes API 的,通常是通过 kubectl 命令行接口的方式。然而,由于 Kubernetes API 构成了与集群的所有交互的基础,这些控制措施也适用于所有类型的访问 — 你的自定义脚本和控制器、Web 用户界面等等。这提供了一个一致的、安全的位置,从这里开始。

你可能已经注意到,直到现在,我们在提到用户时,都没有使用大写的 “U”。许多 Kubernetes 的新手往往会惊讶地发现,在 API 提供的众多资源中,用户其实并不是一个顶层支持的资源。他们并不能直接通过 Kubernetes API 的方式进行操作,最常见的是,他们是在外部用户身份管理系统中定义的。

这是有充分理由的 — 它站在支持良好的用户管理卫生的角度。如果你和绝大多数部署了 Kubernetes 的组织一样,你几乎可以肯定已经有了某种形式的用户管理。无论是以企业范围内的活动目录集群的形式,还是以一次性的轻量级目录访问协议(LDAP)服务器的形式,你管理用户的方式应该在你的组织中保持一致,无论消费它的系统如何。Kubernetes 通过提供连接性来利用这些现有系统,从而支持这一设计宗旨,从而在您的基础设施中实现一致和有效的用户管理。

:::info 没有这样的系统并不意味着你将无法使用 Kubernetes。它可能只是意味着你可能需要利用不同的机制来验证用户,我们将在下一节中发现。 :::

认证

在撰写本文时,Kubernetes 支持多种方式对 API 进行认证。与任何认证机制一样,这作为任何一种程序化访问的第一道关卡。我们在这里评估的问题是:“这个用户是谁?” 和 “他们的凭证是否符合我们的期望?” 在 API 流程的这一点上,我们还不关心是否应该根据用户的角色批准请求,甚至不关心请求是否符合我们的标准。这里的问题很简单,答案也是二进制的。“这是一个真正的用户吗?”

就像许多设计良好的基于 REST 的 API 一样,Kubernetes 可以采用多种策略来验证用户。我们可以认为这些策略中的每一种都属于三大类之一:

  • 基本认证
  • X.509 客户端证书
  • 不记名令牌

用户最终到达获取凭证的方式取决于集群管理员启用的身份提供者,但该机制将坚持这几大类之一。虽然这些机制在实现方式上有很大的不同,但我们将看到每个机制最终是如何向 API Server 提供验证用户真实性和访问级别所需的数据(通过 UserInfo 资源的方式)。

基本认证

基本认证(Basic Authentication)可能是 Kubernetes 集群可用的认证插件中最原始的一个。基本认证是一种机制,API 客户端(通常是 kubectl)将 HTTP 授权头设置为用户名和密码组合的 Base64 哈希。由于 Base64 只是一个哈希值,并没有为传输的凭证提供任何级别的加密,因此必须将基本认证与 HTTPS 结合使用。

要在 API Server 上配置基本认证,管理员需要提供一个静态文件,其中包括用户名、密码、用户 ID 和这个用户应该关联的组列表。其格式如下:

  1. password,username,uid,"group1,group2,group3"
  2. password,username,uid,"group1,group2,group3"
  3. ...

请注意,这些行的格式与 UserInfo 资源的字段相匹配。

这个文件通过 --basic-auth-file 命令行参数提供给 Kubernetes API Server。由于 API Server 目前并不监控这个文件的变化,所以每当用户被添加、删除或更新时,都需要重新启动 API Server,以使这些变化生效。由于这一限制,通常不建议在生产集群中使用基本认证。这个文件当然可以由外部实体(例如,配置管理工具)来管理,以便获得类似生产的配置,但经验表明,这很快就会变得不可持续。

抛开这些缺点不谈,应该指出的是,基本认证可以成为快速、直接测试 Kubernetes 集群的优秀工具。在没有更复杂的身份验证配置的情况下,基本认证允许管理员快速试验访问控制等功能。

X.509 客户端证书

大多数 Kubernetes 安装程序通常启用的认证机制是 X.509 客户端证书。原因可能有很多,但几乎可以肯定的是,它们是安全的,无处不在的,并且可能相对容易生成。如果可以访问签名 CA,可以在短时间内创建新用户。

当以生产质量的方式安装 Kubernetes 时,我们不仅要确保用户发起的请求是安全传输的,而且要确保服务到服务的通信是加密的。X.509 客户端证书对于这种用例来说非常有意义。那么,如果这已经是一个要求,为什么不把它也用来验证用户呢?

这正是很多安装工具的工作原理。例如,kubeadm,社区支持的安装程序,会创建一个自签名的根 CA 证书,然后用这个证书来签署服务组件的各种证书以及它创建的单一管理证书。

为所有用户创建单一证书并不是在 Kubernetes 内管理用户的最佳方式,但对于启动和运行来说,它可以做到这一点。当需要加入更多的用户时,管理员可以从这个签名机构签署额外的客户证书。

:::info 由于 kubeadm 的目的是为用户提供一个简单的平台来建立一个集群,同时也是一个建立生产级集群的工具,因此它具有高度的可配置性,例如,用户需要使用自己的 CA,他们可以配置 kubeadm 来为服务和用户认证需求签署证书。例如,用户需要使用自己的 CA,他们可以配置 kubeadm 为服务和用户认证需求签署证书。 :::

有各种工具可以帮助管理员创建和管理客户端证书。一些比较流行的选择是 OpenSSL 客户端工具和 Cloudflare 的一个名为 cfssl 的实用工具。如果你已经熟悉这些工具,你就会知道命令行选项有时会有点麻烦。我们在这里重点介绍一下 cfssl,因为在我们看来,它的工作流程比较容易掌握。

我们假设你已经有了一个现有的签名 CA。第一步是创建一个 CSR,它将被用来生成客户端证书。同样,我们需要将用户的身份映射到 UserInfo 资源。我们可以通过签名请求来实现。在这里,我们规范的 Common Name CN 映射到用户名,所有的 O 组织字段映射到用户所属的组:

  1. cat > joesmith-csr.json <<EOF
  2. {
  3. "CN": "joesmith",
  4. "key": {
  5. "algo": "rsa",
  6. "size": 2048
  7. },
  8. "names": [
  9. {
  10. "C": "US",
  11. "L": "Boston",
  12. "O": "qa",
  13. "O": "infrastructure",
  14. "OU": "Acme Sprockets Company",
  15. "ST": "MA"
  16. }
  17. ]
  18. }

在本例中,用户 joesmith 同时是 qainfrastructure 的成员。

我们可以按照以下方式生成证书:

  1. cfssl gencert \
  2. -ca=ca.pem \
  3. -ca-key=ca-key.pem \
  4. -config=ca-config.json \
  5. -profile=kubernetes \
  6. joesmith-csr.json | cfssljson -bare admin

在 API Server 上启用 X.509 客户端证书认证就像指定 --client-ca-file= 一样简单,其值将指向磁盘上的证书文件。

尽管 cfssl 简化了创建客户端证书的任务,但这种认证方式仍然有点笨重。就像基本的身份验证一样,当用户入职、删除或需要更改时(例如,将用户添加到一个新的组),会有一些缺点。如果选择证书作为身份验证的选项,管理员应该最低限度地确保这个过程以某种方式自动化,并且这个自动化包括一个随着时间推移而轮换证书的过程。

如果预期的终端用户数量较少,或者大多数用户将通过某种中介(例如,持续交付工具)与集群进行交互,X.509 客户端证书可能是一个适当的解决方案。然而,如果不是这种情况,您可能会发现一些基于令牌的选项更加灵活。

OpenID Connect

OIDC 是建立在 OAuth 2.0 之上的认证层。通过这个认证提供商,用户独立地与一个可信的身份提供商进行认证。如果该用户成功认证,提供商就会通过一系列的网络请求,向用户提供一个或多个令牌。

:::info 因为这种代码和令牌的交换有些复杂,而且与 Kubernetes 如何认证和授权用户并不真正相关,所以我们把重点放在理想状态上,即用户已经通过认证,并且 id_tokenrefresh_token 都已经被授予。 :::

这些令牌以 RFC 7519 JSON 网络令牌(JWT)格式提供给用户。这个开放标准允许在多方之间表示用户的要求。更简单的说,通过微不足道的可被人类解析的 JSON,我们可以共享信息,比如用户名、用户 ID 以及这个用户可能属于的组。这些令牌是用基于哈希的消息认证码(HMAC)进行认证的,并没有加密。因此,再次确保包括 JWT 在内的所有通信都是加密的,最好是用 TLS 加密。

一个典型的令牌有效载荷看起来像这样:

  1. {
  2. "iss": "https://auth.example.com",
  3. "sub": "Ch5hdXRoMHwMTYzOTgzZTdjN2EyNWQxMDViNjESBWF1N2Q2",
  4. "aud": "dDblg7xO7dks1uG6Op976jC7TjUZDCDz",
  5. "exp": 1517266346,
  6. "iat": 1517179946,
  7. "at_hash": "OjgZQ0vauibNVcXP52CtoQ",
  8. "username": "user",
  9. "email": "user@example.com",
  10. "email_verified": true,
  11. "groups": [
  12. "qa",
  13. "infrastructure"
  14. ]
  15. }

该 JSON 文件中的字段称为 “声明”,用于识别用户的各种属性。尽管这些声明中有许多是标准化的(如 issiatexp),身份提供者也可以添加他们自己的自定义声明。幸运的是,API Server 允许我们指明这些声明如何映射回我们的 UserInfo 资源。

要在 API 服务器上启用 OIDC 身份验证,我们需要在命令行中添加 --oidc-issuer-url--oidc-client-id 参数。这两个参数分别是身份提供者的 URL 和客户端配置的 ID,这两个值都是由你的提供者给出的。另外两个我们可能要配置的选项,虽然这不是强制性的,是 --oidc-username-claim(默认:sub)和 --oidc-group-claim(默认: groups)。如果这些默认值与你的令牌结构相匹配,那就太好了。但即使它们不匹配,每一个都允许你将身份提供者的声明映射到各自的 UserInfo 属性上。

:::info 有一个神奇的工具可以检查 JWT 的结构。Auth0 的这个工具不仅可以让你粘贴你的令牌来探索它的内容,还提供了一个深入的开源 JWT 签名和验证库的参考。 :::

这种类型的身份验证与我们看过的其他类型的身份验证有些不同,因为它涉及到一个中间人。通过基本的身份验证和 X.509 客户端证书,Kubernetes API Server 能够执行身份验证所需的所有步骤。如图 7-2 所示,通过 OIDC,最终用户针对我们相互信任的身份提供者进行身份验证,然后使用她收到的令牌随后向 API Server 证明她的身份。流程看起来像图 7-2 中的插图。
image.png
图 7-2 Kubernetes OIDC 流程

  1. 用户对 Kubernetes API Server 应用进行认证和授权。
  2. 认证前端将用户的凭证传递给身份提供者。
  3. 如果身份提供者能够验证用户,提供者会返回一个访问代码。这个访问代码会被返回给身份提供者,并换取一个身份令牌和(通常)一个刷新令牌。
  4. 用户将这些令牌添加到 kubeconfig 配置中。
  5. 现在,kubeconfig 文件包含了 OIDC 身份信息,kubectl 试图在每个 Kubernetes API 请求中注入令牌作为承载令牌。如果令牌过期,kubectl 将首先尝试通过与发行方交换过期的身份令牌来获取新的身份令牌。
  6. Kubernetes API Server 会根据令牌凭证向身份提供者请求用户信息,确保这个令牌是合法的。
  7. 如果令牌被验证,身份提供者返回用户信息,Kubernetes API Server 允许原来的 Kubernetes API 请求继续流转。

Webhook

在某些情况下,管理员已经可以访问能够生成承载令牌的系统。你或许可以想象这样一个场景:一个内部系统授予用户一个长寿命的令牌,他或她可以使用这个令牌对环境中的任何数量的系统进行认证。它可能不像 OIDC 那样复杂或符合标准,但只要我们能够通过编程挑战该令牌的真实性,Kubernetes 就能够验证用户的身份。

在 Webhook 认证到位后,API Server 会提取入站请求中存在的任何承载令牌,随后向认证服务发出一个客户端 POST 请求。这个请求的主体将是一个 JSON 序列化的 TokenReview 资源,嵌入原始的承载令牌。

  1. {
  2. "apiVersion": "authentication.k8s.io/v1beta1",
  3. "kind": "TokenReview",
  4. "spec": {
  5. "token": "some-bearer-token-string"
  6. }
  7. }

:::info 如果在验证用户时因为某种原因出现了错误,服务也可能会以错误字符串字段作为验证的同级响应。 :::

相反,如果响应是认证成功,提供者应该以最小的方式响应,用一个嵌入的 UserInfo 资源对象提供关于用户的数据。这个对象有用户名、uid、组的字段,甚至还有一个用于服务可能想要传递的额外数据。

  1. {
  2. "apiVersion": "authentication.k8s.io/v1beta1",
  3. "kind": "TokenReview",
  4. "status": {
  5. "authenticated": true,
  6. "user": {
  7. "username": "janedoe@example.com",
  8. "uid": "42",
  9. "groups": [
  10. "developers",
  11. "qa"
  12. ],
  13. "extra": {
  14. "extrafield1": [
  15. "extravalue1",
  16. "extravalue2"
  17. ]
  18. }
  19. }
  20. }
  21. }

一旦 API 发起请求并收到响应,API Server 就会根据认证服务提供的指导,批准或拒绝 Kubernetes API 请求。

:::info 几乎所有基于令牌的认证机制都需要注意的一点是,令牌的验证往往涉及额外的请求和响应。例如,在 OIDC 和 Webhook 认证的情况下,如果身份提供者没有及时响应,这种额外的往返验证令牌可能会成为 API 请求的性能瓶颈。在使用这些插件的情况下,请确保你有低延迟和高性能的提供商。 :::

dex 项目

当这些服务都不适合你的用例时,会发生什么?你可能已经注意到,常用的目录服务并不包括在 Kubernetes 原生支持的认证插件列表中。例如,在撰写本文时,没有活动目录、LDAP 或其他的连接器。

当然,你可以随时编写自己的认证代理,与这些系统对接,但这很快就会成为另一个需要开发、管理和维护的基础设施。

使用 dex,这是一个来自 CoreOS 的项目,可以作为一个 OIDC 代理。dex 为各种常见的后端提供了一个符合标准的 OIDC 前端。它支持 LDAP、活动目录、SQL、SAML,甚至支持 SaaS 提供商,如 GitHub、GitLab 和 LinkedIn。想象一下,当你收到 Kubernetes 管理员的邀请时,你会有多高兴: I'd like to add you to my professional Kubernetes cluster network on LinkedIn.

:::info 需要注意的是,在 Kubernetes 集群中配置的认证机制并不是相互排斥的。事实上,我们建议同时启用多个插件。

例如,作为管理员,你可以同时配置 TLS 客户端证书和 OIDC 认证。虽然日常使用多种机制可能并不合适,但当你需要调试一个失败的二级 API 认证机制时,这样的配置可能会证明是有价值的。在这种情况下,你可以利用一个著名的(希望是受保护的)证书来获取关于失败的额外数据。

请注意,当多个身份验证插件同时激活时,第一个成功验证用户的插件将绕过身份验证过程。:::

kubeconfig

有了我们所描述的所有认证机制,我们需要制作一个 kubeconfig 文件来记录我们如何进行认证的细节,kubectl 使用这个配置文件来决定在哪里以及如何向 API Server 发出请求。这个文件通常位于你的主目录~/.kube/config 下,但也可以在命令行用 --kubeconfig 参数或通过 KUBECONFIG 环境变量明确指定。

你是否在 kubeconfig 中嵌入你的证书取决于你使用的认证机制,甚至可能取决于你的安全立场。请记住,如果您确实将凭证嵌入到这个配置文件中,那么任何能够访问这个文件的人都可能会使用它们。把这个文件当作高度敏感的密码来对待,因为它实际上就是。

:::tips 对于不熟悉 kubeconfig 文件的人来说,了解它的三个顶层结构是很重要的:用户、集群和上下文。对于用户,我们命名一个用户,并提供他或她将通过该机制认证到一个集群。集群属性提供了连接到集群所需的所有数据。最起码,这包括 API Server 的 IP 或完全合格的域名,但也可能包括自签名证书的 CA 捆绑等项目。而 contexts 是我们将用户与集群关联为一个单一命名的实体。上下文作为 kubectl 连接和认证到 API Server 的手段。 :::

你的所有集群的所有证书都可以用一个 kubeconfig 配置来表示。最重要的是,这可以通过一些 kubectl 命令来操作。

  1. $ export KUBECONFIG=mykubeconfig
  2. $ kubectl config set-credentials cluster-admin --username=admin \
  3. --password=somepassword
  4. User "cluster-admin" set.
  5. $ kubectl config set-credentials regular-user --username=user \
  6. --password=someotherpassword
  7. User "regular-user" set.
  8. $ kubectl config set-cluster cluster1 --server=https://10.1.1.3
  9. Cluster "cluster1" set.
  10. $ kubectl config set-cluster cluster2 --server=https://192.168.1.50
  11. Cluster "cluster2" set.
  12. $ kubectl config set-context cluster1-admin --cluster=cluster1 \
  13. --user=cluster-admin
  14. Context "cluster1-admin" created.
  15. $ kubectl config set-context cluster1-regular --cluster=cluster1 \
  16. --user=regular-user
  17. Context "cluster1-regular" created.
  18. $ kubectl config set-context cluster2-regular --cluster=cluster2 \
  19. --user=regular-user
  20. Context "cluster2-regular" created.
  21. $ kubectl config view
  22. apiVersion: v1
  23. clusters:
  24. - cluster:
  25. server: https://10.1.1.3
  26. name: cluster1
  27. - cluster:
  28. server: https://192.168.1.50
  29. name: cluster2
  30. contexts:
  31. - context:
  32. cluster: cluster1
  33. user: cluster-admin
  34. name: cluster1-admin
  35. - context:
  36. cluster: cluster1
  37. user: regular-user
  38. name: cluster1-regular
  39. - context:
  40. cluster: cluster2
  41. user: regular-user
  42. name: cluster2-regular
  43. current-context: ""
  44. kind: Config
  45. preferences: {}
  46. users:
  47. - name: cluster-admin
  48. user:
  49. password: somepassword
  50. username: admin
  51. - name: regular-user
  52. user:
  53. password: someotherpassword
  54. username: user

在这里,我们已经创建了两个用户定义、两个集群定义和三个上下文。现在,只需多一条 kubectl,我们就可以用一条额外的命令来重置上下文。

$ kubectl config use-context cluster2-regular
Switched to context "cluster2-regular".

这使得从一个群集到下一个群集,切换群集和用户,甚至在同一个群集上冒充不同的用户(这在管理员的工具箱中是相当有用的)变得异常简单。

虽然这是一个利用基本认证的非常简单的例子,但用户和集群可能会被配置成各种选项。而这些配置可能会变得相对复杂。也就是说,这是一个强大的工具,通过一些命令行操作就可以变得简单。利用对你的用例最有意义的上下文。

服务账号

到目前为止,在本章中,我们已经讨论了用户如何使用 API 进行身份验证。而且,在这段时间里,我们只真正关注了认证,因为它适用于集群外部的用户。也许这就是你,从你的控制台甚至通过 Web 界面点击执行 kubectl 命令。

但还有一个重要的用例需要考虑,这涉及到 Pod 内部运行的进程如何访问 API。起初,你可能会问自己,为什么在 Pod 上下文中运行的进程可能需要访问 API。

一个 Kubernetes 集群是一个由控制器集合组成的状态机。这些控制器中的每一个都负责调和用户指定资源的状态。因此,在最基本的情况下,我们需要为我们打算实现的任何自定义控制器提供 API 访问。但从控制器访问 Kubernetes API 并不是唯一的用例。一个 Pod 可能需要自我意识甚至是对整个集群的意识,有无数的原因。

Kubernetes 处理这些用例的方式是使用 ServiceAccount 资源。

$ kubectl create sa testsa
$ kubectl get sa testsa -oyaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: testsa
  namespace: default
secrets:
- name: testsa-token-nr6md

你可以把 ServiceAccount 看成是所有 Pod 资源的命名间隔用户账户。

在上面的输出中,请注意,当我们创建 ServiceAccount 时,一个名为 testa-token-nr6md 的 Secret 也被自动创建。就像我们前面讨论的终端用户认证一样,这是将作为每个 API 请求的承载令牌。这些凭证被安装到 Pod 中,放在一个知名的位置,各种 Kubernetes 客户端都可以访问。

$ kubectl run busybox --image=busybox -it -- /bin/sh
If you dont see a command prompt, try pressing enter.
/ # ls -al /var/run/secrets/kubernetes.io/serviceaccount
total 4
drwxrwxrwt    3 root     root           140 Feb 11 20:17 .
drwxr-xr-x    3 root     root          4096 Feb 11 20:17 ..
drwxr-xr-x    2 root     root           100 Feb 11 20:17 \
    ..2982_11_02_20_17_08.558803709
lrwxrwxrwx    1 root     root            31 Feb 11 20:17 ..data ->
..2982_11_02_20_17_08.558803709
lrwxrwxrwx    1 root     root            13 Feb 11 20:17 ca.crt -> ..data/ca.crt
lrwxrwxrwx    1 root     root            16 Feb 11 20:17 namespace -> \
    ..data/namespace
lrwxrwxrwx    1 root     root            12 Feb 11 20:17 token -> ..data/token

尽管我们正在尝试验证一个进程,但我们再次使用 JWT,其中的要求看起来很像我们在终端用户令牌场景中看到的那样。回想一下,API Server 的目标之一就是将这个用户的数据映射到 UserInfo 资源上,这种情况也不例外。

{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "testsa-token-nr6md",
  "kubernetes.io/serviceaccount/service-account.name": "testsa",
  "kubernetes.io/serviceaccount/service-account.uid":
      "23fe204f-0f66-11e8-85d0-080027da173d",
  "sub": "system:serviceaccount:default:testsa"
}

每一个启动的 Pod 都有一个相关的 ServiceAccount:

apiVersion: v1
kind: Pod
metadata:
  name: testpod
spec:
  serviceAccountName: testpod-sa

:::info 如果在 Pod 清单中没有指定,则使用默认的 ServiceAccount。这个默认的 ServiceAccount 在整个命名空间的基础上可用,并在命名空间是时自动创建。 :::

从安全的角度来看,有很多情况下,为 Pod 提供对 Kubernetes API 的访问是不合适的。虽然无法阻止 Pod 拥有关联的 ServiceAccount,但在下一章中,我们将探讨如何保证这些用例的安全。

总结

在本章中,我们介绍了 Kubernetes 中最常见的部署的终端用户认证机制。希望其中有一个或多个脱颖而出,成为你有兴趣在环境中启用的东西。如果没有,还有一些其他的机制(例如,静态令牌文件、认证代理和其他)可能会被实现。其中一个或多个几乎肯定会符合您的需求。

虽然您应该在前期进行尽职调查,以安全和可扩展的方式入职用户,但请记住,就像 Kubernetes 中几乎所有的东西一样,这些配置可能会随着时间的推移而变化。使用今天对您的组织有意义的解决方案,知道您可能会在未来无缝地采用其他功能。