认证只是 Kubernetes API 请求的第一个挑战。正如我们在第 7 章中介绍的那样,每个请求还有两个额外的测试:访问控制和准入控制。尽管认证是确保只有受信任的用户才能在集群上进行更改的关键组件,但正如我们在本章中所探讨的那样,认证也成为对这些用户可能做的事情进行细粒度控制的推动者。
除了验证用户的真实性和确定访问级别之外,我们还希望确保每个请求都符合我们的业务需求。每个组织都有一些实施的标准。这些政策和程序帮助我们理解将应用程序引入生产环境所需的复杂基础架构。在本章中,我们将看看 Kubernetes 是如何站在准入控制器的角度来支持这一点的。
REST
正如我们已经介绍过的,Kubernetes API 是一个 RESTful API。RESTful API 的优势属性有很多(例如,可扩展性和可移植性),但其简单的结构使我们能够确定 Kubernetes 内部的访问级别。
对于可能不熟悉 REST 的读者来说,其语义是直接的:资源使用动词进行操作。就像在传统语言中一样,如果我们要求某人 “删除 Pod”,我们用一个名词和一个动词来完成。REST API 的功能也是如此。
为了说明这个概念,我们来看看 kubectl 是如何精确地请求一个 Pod 的信息的。通过简单地使用 -v 选项增加日志级别,我们可以深入了解 kubectl 代表我们进行的 API 调用。
$ kubectl -v=6 get po testpodI0202 00:28:31.933993 17487 loader.go:357] Config loaded from file/home/ubuntu/.kube/configI0202 00:28:31.994930 17487 round_trippers.go:436] GEThttps://10.0.0.1:6443/api/v1/namespaces/default/pods/testpod 200 OK
在这个简单的 Pod 信息请求中,我们可以看到 kubectl 对 pods/testpod 资源发出了 GET 请求(这是动词)。你可能还会注意到 URL 路径中还有其他元素,比如 API 的版本,以及我们要查询的命名空间(本例中为默认)。这些元素为我们的请求添加了额外的上下文,但足以说明资源和动词是这里的主要角色。
以前接触过 REST 的人都会熟悉四个最基本的动词。创建、读取、更新和删除(CRUD)。这四个动作分别直接映射到 HTTP 动词 POST、GET、PUT 和 DELETE,反过来,这四个动词构成了互联网上通常出现的绝大多数 HTTP 请求。
你可能也会注意到,这些动词看起来有点像我们在处理 Kubernetes 资源时使用的动词,你会发现是对的。比如说,我们当然可以创建、删除、更新,甚至收集 Pod 的信息。就像 HTTP 一样,这四个动词构成了我们与 Kubernetes 资源交互的最基本元素,但在我们的情况下,我们并不局限于这四个动词。在 Kubernetes API 中,除了 get、update、delete 和 patch,在处理资源时,我们还可以使用动词 list、watch、proxy、redirect 和 deletecollection。这些都是 kubectl(以及任何客户端)在幕后代表我们使用的动词。
Kubernetes 中的资源是我们熟悉的结构 — Pods、服务和部署等,我们通过这些动词来操作。
授权
仅仅因为用户是经过认证的,并不意味着我们应该给所有用户同等的访问权限。例如,我们可能希望 Web 开发团队的成员能够操纵服务于 Web 请求的 Deployment,但不能操纵作为这些 Deployment 的计算单位的底层 Pod。或者说,即使在 Web 团队内部,我们也可能有一个组可以创建资源,而另一个组不能。简而言之,我们希望根据用户是谁和/或她是哪个组的成员来决定哪些操作是允许的。
这个过程被称为授权,它是 Kubernetes 为每个 API 请求测试的下一个挑战。在这里,我们要问:“这个用户是否被允许执行这个操作?”
与身份验证一样,授权是 API Server 的责任。可以使用 kube-apiserver 可执行文件中恰如其分的 --authorization-mode 参数来配置 API Server 以实现各种授权模块。
API Server 按照逗号分隔的 — 授权模式参数定义的顺序将每个请求传递给这些模块。每个模块又可以对决策过程进行权衡或选择放弃。在弃权的情况下,API 请求将简单地转移到下一个模块进行评估。但是,如果某个模块确实做出了决定,则授权终止,并反映授权模块的决定。如果模块拒绝该请求,用户会收到相应的 HTTP 403(Forbidden)响应,如果允许该请求,则该请求进入API流程的最后一步:准入控制器评估。
在撰写本文时,有六个授权模块可以配置。最简单直接的是 AlwaysAllow 和 AlwaysDeny 模块,就像名字一样,这些模块分别允许或拒绝一个请求。这两个模块其实只适合测试环境。
节点授权模块负责将我们想要应用的授权规则应用到工作节点发出的 API 请求中。就像终端用户一样,每个节点上的 kubelet 进程都会执行各种 API 请求。例如,当你执行 kubectl get nodes 时,所呈现的 Node 状态是可能的,因为 kubelet 已经通过 PATCH 请求向 API Server 提供了它的状态。
PATCH https://k8s.example.com:6443/api/v1/nodes/node1.example.com/status 200 OK
显然,kubelet 不应该访问像我们的 Web 服务 Pod 这样的资源。这个模块将 kubelet 的能力限制在维持一个功能性工作节点所需的请求子集上。
基于角色的访问控制
Kubernetes 中最有效的用户授权手段是使用 RBAC 模块。这个模块是基于角色的访问控制的简称,可以在运行时实现动态的访问控制策略。
习惯了其他框架的这种授权方式的人,现在可能已经在呻吟了。这些框架中的一些框架实现 RBAC 的方式往往是一个复杂和令人费解的过程。当定义访问级别很繁琐的时候,如果有的话,提供粗粒度的访问控制也是很诱人的。更糟糕的是,当这些控制的配置是静态的或不灵活的时候,你几乎可以保证它们不会被有效地实施。
幸运的是,Kubernetes 让 RBAC 策略的定义和实现变得异常简单。简而言之,Kubernetes 将 UserInfo 对象的属性映射到用户应该可以访问的资源和动词。
角色和集群角色
在 RBAC 模块中,对资源执行操作的授权是通过 Role 或 ClusterRole 资源类型来定义的。(我们稍后将深入探讨这些资源之间的区别。)首先,我们先只关注 Role 资源。前面的例子(用户对 Deployment 有读写权限,但对 Pod 只有读权限)的实现可能是这样的。
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: web-rw-deployment
namespace: some-web-app-ns
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["extensions", "apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
在此角色配置中,我们创建了一个策略,允许对 Pod 应用读类型操作,并允许部署的完全读写访问权。该角色将强制执行发生在部署的子 Pod 上的所有更改(例如,滚动更新或缩放)。
每条规则的 apiGroups 字段简单地向 API Server 指明了它应该执行的 API 命名空间。这反映了在资源定义的 apiVersion 字段中定义的 API 命名空间)。
在接下来的资源和动词这两个字段中,我们会遇到我们前面讨论过的那些 REST 结构。而且,在 RBAC 的情况下,我们明确允许具有这个 web-rw-deployment 角色的用户进行这些类型的 API 请求。由于 rule 是一个数组,我们可以添加任意多的合适的权限组合。所有这些权限都是相加的。对于 RBAC,我们只能授予动作,否则这个模块默认拒绝。
Role 和 ClusterRole 在功能上是相同的,只是在范围上有所不同。在刚才的例子中,你可能会注意到,这个策略是绑定在 some-web-app-ns 命名空间的资源上的。这意味着这个策略只应用于该命名空间中的资源。
如果我们想授予一个具有跨命名空间功能的权限,我们就使用 ClusterRole 资源。这个资源,以同样的方式,授予细粒度的控制,但是是在整个集群的基础上。
你可能会好奇为什么有人要实现这样的策略。ClusterRole 通常用于两个主要的用例 — 轻松授予集群管理员广泛的自由度,或者授予 Kubernetes 控制器非常具体的权限。
:::warning 第一种情况很简单。我们经常希望管理员拥有广泛的访问权限,这样他们就可以轻松地调试问题。当然,我们可以为最终创建的每一个命名空间制定一个 Role 策略,但只用 ClusterRole 授予这种访问权限可能更方便。由于这些权限影响深远,我们要谨慎使用这个构造。 :::
大多数 Kubernetes 控制器都有兴趣观察跨命名空间的资源,然后适当地调节集群状态。我们可以使用 ClusterRole 策略来确保控制器只对他们关心的资源有访问权。
:::info 所有 Kubernetes 控制器(如 Deployment 或 StatefulSet)都有相同的基本结构。它们是一个状态机,监视 Kubernetes API 的变化(添加、修改和删除),并寻求从当前状态调和到用户指定的期望状态。 :::
想象一下,我们想根据用户指定的服务或 Ingress 资源上的注释来创建 DNS 记录。我们的控制器需要观察这些资源,并在发生某种变化时采取行动。如果让这个控制器访问其他资源和不合适的动词(例如,在 Pod 上的 DELETE),将是不安全的。我们可以使用 ClusterRole 策略来提供正确的访问级别,如下所示:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "watch", "list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get", "watch", "list"]
而这正是 external-dns Kubernetes 孵化器项目的工作原理。
有了这个 ClusterRole 策略,external-dns 控制器就可以观察 Service 和 Ingress 资源的添加、修改,甚至是删除,并对其采取相应的行动。而且,最重要的是,这些控制器无法访问 API 的任何其他方面。
:::warning 在授予用户使用 RBAC 的访问权限时,一定要了解所有的影响。始终寻求只授予必要的权限,因为这将大大降低你的安全风险。同时也要明白,有些权限会授予其他资源隐含的 — 也许是无意的 — 权利。特别是,您应该知道,授予 Pod 创建权限,有效地授予对更敏感和相关资源的读取权限,如 Secret。因为 Secret 可能会被挂载或通过环境变量暴露在 Pod 上,Pod 创建权限允许 Pod 所有者读取这些未加密的 Secret。 :::
角色绑定和集群角色绑定
你会注意到,Role 和 ClusterRole 都没有指定它们的规则要针对哪些用户或组。单纯的策略是没有用的,除非它们被应用到一个用户或组。要将这些策略与用户、组或服务账户相关联,我们可以使用 RoleBinding 和 ClusterRoleBinding 资源。这里唯一的区别是我们是要绑定一个 Role 还是 ClusterRole。同样,RoleBinding 是有名字间隔的。
RoleBinding 和 ClusterRoleBinding 将策略与主题相关联:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: web-rw-deployment
namespace: some-web-app-ns
subjects:
- kind: User
name: "joesmith@example.com"
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: "webdevs"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: web-rw-deployment
apiGroup: rbac.authorization.k8s.io
在这个例子中,我们将 some-web-app-ns 命名空间中的 web-rw-deployment 角色关联到了 joesmith@example.com,以及一个名为 webdevs 的组。
正如您在第 7 章中所回顾的那样,每一种认证机制的目标都有两个 — 首先,确保用户的凭证符合我们的期望;其次,获取关于已认证用户的信息。这些信息是通过恰当地命名为 UserInfo 资源来传达的。我们在这里指定的字符串值反映了认证过程中获得的用户信息。
当涉及到授权时,我们可以对三种主题类型应用策略。User、Group 和 ServiceAccount。就 User 和 Group 而言,它们分别由 UserInfo 的 username 和 group 定义。
:::info
这些字段的值都是字符串,对于 username 来说是字符串,对于 group 来说是字符串列表,用于包含的比较是简单的字符串匹配。这些字符串值由你决定,它们可以是你的授权系统提供的任何唯一的字符串值,用来识别用户或组。
:::
ServcieAccount 是用适当命名的服务账户 subject 类型明确指定的:
...
subjects:
- kind: ServiceAccount
name: testsa
namespace: some-web-app-ns
请记住,ServiceAccount 为所有运行中的 Pod 进程提供 Kubernetes API 凭证。每个 Pod 都有一个相关联的 ServiceAccount,不管我们是否在 Pod 清单中指定使用哪个 serviceAccountName。如果不加以注意,这可能会带来重大的安全问题。
这种担忧可以通过 RBAC 策略在很大程度上得到缓解。由于 RBAC 策略是默认拒绝的,我们建议任何需要 API 功能的 Pod 都有自己的(或可能是共享的)ServiceAccount,并有相关的细粒度 RBAC 策略。只授予这个 ServiceAccount 正常运行所需的操作和资源。
回顾 ClusterRole external-dns 的例子。因为控制器状态机从 Pod 上下文向 Kubernetes API 发出请求,所以我们可以使用带有 ServiceAccount 主体的 ClusterRoleBinding 来启用这个功能:
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
授权测试
随着 Kubernetes 集群上用户、组和工作负载数量的增加,复杂性也在增加。虽然 RBAC 是一种简单的机制,通过它可以将授权策略应用于主体集合,但访问权限的实施和调试有时会很困难。幸运的是,kubectl 提供了一个方便的资源来验证我们的策略,而不需要在集群上进行任何真正的改变。
要测试访问权限,只需使用 kubectl 设置你要验证的用户(或你需要检查的组的一部分用户)的上下文:
$ kubectl use-context admin
$ kubectl auth can-i get pod
yes
在这里,我们可以看到,管理员用户可以 GET 访问默认命名空间中的 Pod 资源。
在创建了一个限制性更强的策略,即阻止用户创建任何 Namespace 资源(这些资源在集群上有范围)后,我们使用 can-i来确认这个策略:
$ kubectl use-context basic-user
$ kubectl create namespace mynamespace
Error from server (Forbidden): namespaces is forbidden: User "basic-user"
cannot create namespaces at the cluster scope
$ kubectl auth can-i create namespace
no
请注意,当用户第一次尝试创建 Namespace
mynamespace时,她收到了相应的Forbidden。
:::info
在本章中,我们不涉及基于属性的访问控制(ABAC)模块。这个模块在语义上与 RBAC 模块非常相似,不同的是,这些策略是通过每个 Kubernetes API Server 上的配置文件静态定义的。就像我们讨论过的其他一些基于文件的配置项一样,这个策略不是动态的。管理员每次想要修改这个策略时,都需要重新启动 kube-apiserver 进程。这方面使得它对于更强大和生产就绪的 RBAC 模块来说是不切实际的。
:::
总结
在本章中,我们介绍了 Kubernetes API 的 RESTful 性质,以及它的结构如何适合于策略执行。我们还探讨了授权如何与我们已经介绍过的关于身份验证的内容直接相关。
授权是部署一个安全的多租户分布式系统所必需的关键组件之一。通过 RBAC,Kubernetes 为我们提供了强制执行非常粗粒度的、全面的策略和那些极其特定于用户或组的策略的能力。而且,因为 Kubernetes 使这些策略的定义和维护变得如此微不足道,所以即使是最基本的部署也没有理由不利用它们。这是让用户、管理员和审计人员都满意的完美第一步。
