安装 Operator 并验证权限边界
目标
按照正确顺序安装一整套 Operator 清单(命名空间、RBAC、控制器 Deployment、领导者选举 Lease),并亲自查询证明其权限确实位于预期边界内。
为什么重要
Operator 可能是集群中权限最强的工作负载。由于它会创建和修改多个命名空间中的资源,一旦其服务账号被窃取,攻击者就会完整获得这些权限。因此,两点至关重要。第一,安装顺序就是依赖关系。控制器一启动就会 watch 自身类型,所以 CRD 必须先存在;它还会先查询列表,所以权限也必须先存在。顺序错误会表现为崩溃循环或 403,而要意识到原因不是代码、而是部署顺序,往往需要很长时间。第二,权限控制就是减少动词。verbs: ["*"] 虽然方便,但如果将子资源清理由所有者引用处理,很多情况下连 delete 都不需要。此外,在 RBAC 中,webservices 与 webservices/status 是完全不同的资源——不了解这一点,就会困在“明明给了权限,却无法写入 status”的症状中。最后,权限不是文档,而是验证对象。使用 kubectl auth can-i --as= 同时确认允许和不应允许的操作,这张表就会成为权限边界的证据。
步骤
开始前准备:实验 Pod 每次实验都会重新创建,因此前一实验的集群状态不会保留。第 2、6 步需要 CRD webservices.apps.labhub.io,第 7、8 步的权限查询以 crd-lab 命名空间为目标。因此,如果 kubectl get crd webservices.apps.labhub.io 结果为空,请重新应用 CRD,并执行 kubectl create ns crd-lab。CRD 必须包含 v1alpha1 和 v1 两个版本,存储版本必须是 v1(第 6 步会检查是否保留)。
- 创建命名空间
labhub-operator,并添加标签app.kubernetes.io/part-of=labhub-operator和pod-security.kubernetes.io/enforce=restricted。 - 在
/root/op/install/out/install-order.txt中按每个阶段一行写出安装顺序。第 1 行必须提到CustomResourceDefinition,第 2 行必须提到ServiceAccount和ClusterRole/ClusterRoleBinding,第 3 行必须提到Deployment;不要在前面的行中提前使用Deployment或컨트롤러一词(顺序判断以首次出现的行号为准)。集群中还必须存在 CRDwebservices.apps.labhub.io。 - 在
labhub-operator中创建 ServiceAccountwebservice-controller,并按以下规则创建 ClusterRolewebservice-controller。apiGroups: ["apps.labhub.io"],resources: ["webservices"],verbs: ["get","list","watch","update","patch"]apiGroups: ["apps.labhub.io"],resources: ["webservices/status","webservices/finalizers"],verbs: ["get","update","patch"]apiGroups: [""],resources: ["events"],verbs: ["create","patch"]apiGroups: ["coordination.k8s.io"],resources: ["leases"],verbs: ["get","list","watch","create","update","patch"]动词和资源中都不能使用*。然后创建 ClusterRoleBindingwebservice-controller,将 subject 指定为kind: ServiceAccount、name: webservice-controller、namespace: labhub-operator。
- 在
labhub-operator中创建 Deploymentwebservice-controller。spec.replicas为 1 或 2,spec.template.spec.serviceAccountName为webservice-controller,容器镜像为ghcr.io/labhub/webservice-controller:v0.1.0,在args中加入--leader-elect=true,分别为容器配置livenessProbe和readinessProbe,并设置resources.limits.memory。Pod 的securityContext.runAsNonRoot必须为true。 - 在
labhub-operator中创建 Leasewebservice-controller.labhub.io。spec.holderIdentity是领导者的身份字符串,spec.leaseDurationSeconds不小于 5,spec.renewTime是精确到微秒的 RFC3339 时间(date -u +%Y-%m-%dT%H:%M:%S.%6NZ)。并在/root/op/install/out/lease-note.txt中说明没有领导者选举会遇到什么问题(两个实例同时协调同一个对象)。 - 将 Deployment 的容器镜像升级为
ghcr.io/labhub/webservice-controller:v0.2.0,并等待滚动发布完成。CRD 必须仍保留至少两个版本,且status.storedVersions中必须包含v1。在/root/op/install/out/upgrade-check.txt中写明升级前后检查了什么,并且必须包含对storedVersions的检查。 - 将
/opt/lab/fixtures/operator/broken-operator.yaml复制为/root/op/install/fixed-operator.yaml,修复三处错误并应用。然后在/root/op/install/out/diagnosis.txt中说明哪里错了、为什么错,要求每行一项,至少三行。必须包括绑定主体名称问题和缺少 status 子资源权限。修复后,kubectl auth can-i update webservices/status -n crd-lab --as=system:serviceaccount:labhub-operator:webservice-controller和kubectl auth can-i list webservices --all-namespaces --as=...都必须返回yes。 - 创建
/root/op/install/out/permissions.json。顶层键为checks,每个元素包含verb、resource、expected(yes/no),并可按需包含namespace。不写namespace时,按所有命名空间范围进行检查。至少要有 6 项,其中expected为no的项目至少 2 项,并且其中一项的resource必须是secrets。例如,以下八项就足够:list webservices(yes)、watch webservices(yes)、update webservices/statusincrd-lab(yes)、create eventsincrd-lab(yes)、update webservices/finalizersincrd-lab(yes)、get secretsincrd-lab(no)、delete webservicesincrd-lab(no)、create clusterrolebindings(no)。所有项目的expected都必须与实际响应一致。
参考
- 实验 Pod 每次实验都会重新创建,因此前一实验的集群状态不会保留。但只要把声明保存为文件,就能在任何 Pod 中重新建立相同状态——这正是声明式方式的实际优势,也是必须使用清单管理 Operator 安装的原因。
- 权限检查的基准主体是
system:serviceaccount:labhub-operator:webservice-controller。使用kubectl auth can-i <동사> <리소스> -n <ns> --as=<주체>的形式。 - 第 3 步的规则中故意没有加入
delete。标准做法是将子资源清理交给所有者引用和垃圾回收器,这样还能在遭到入侵时阻止批量删除路径。第 8 步的delete webservices(no)验证了这一设计。 - 使用
resourceNames将领导者选举 Lease 的权限限制到这一个名称,是更好的加固方式,但本实验不作要求。 - 为满足 restricted 策略,最好在 Pod 中同时配置
seccompProfile.type: RuntimeDefault,在容器中配置allowPrivilegeEscalation: false和capabilities.drop: ["ALL"]。 - 常见错误 1:在第 2 步第一行提前写入“必须先添加 CRD,控制器才能启动”之类的后续阶段词语。顺序判断基于首次出现的行号,因此会导致判断颠倒。
- 常见错误 2:第 3 步只为
webservices授权,却遗漏webservices/status。在 RBAC 中,子资源是完全不同的资源。 - 常见错误 3:第 5 步只将
renewTime写成秒精度的 RFC3339。Lease 的时间字段要求六位微秒。
创建 Operator 专用命名空间
创建命名空间 labhub-operator,并添加标签 app.kubernetes.io/part-of=labhub-operator 和 pod-security.kubernetes.io/enforce=restricted。
分别需要一个用于一次性查找所有组件的归属标签,以及一个禁止 Pod 使用特权的策略标签。控制器是完全不需要特权的工作负载。
整理安装顺序
在 /root/op/install/out/install-order.txt 中按每个阶段一行写出安装顺序。第 1 行必须提到 CustomResourceDefinition,第 2 行必须提到 ServiceAccount 和 ClusterRole/ClusterRoleBinding,第 3 行必须提到 Deployment;不要在前面的行中提前使用 Deployment 或 컨트롤러 一词(顺序判断以首次出现的行号为准)。集群中还必须存在 CRD webservices.apps.labhub.io。
控制器一启动就会 watch 自身类型并查询列表。这两项必须事先准备好。顺序文档每个阶段写一行,不要在前面的行中提前提及后续阶段的名称。
创建不含通配符的最小权限
在 labhub-operator 中创建 ServiceAccount webservice-controller,并按以下规则创建 ClusterRole webservice-controller。
apiGroups: ["apps.labhub.io"],resources: ["webservices"],verbs: ["get","list","watch","update","patch"]apiGroups: ["apps.labhub.io"],resources: ["webservices/status","webservices/finalizers"],verbs: ["get","update","patch"]apiGroups: [""],resources: ["events"],verbs: ["create","patch"]apiGroups: ["coordination.k8s.io"],resources: ["leases"],verbs: ["get","list","watch","create","update","patch"]动词和资源中都不能使用*。然后创建 ClusterRoleBindingwebservice-controller,将 subject 指定为kind: ServiceAccount、name: webservice-controller、namespace: labhub-operator。
主资源和子资源在 RBAC 中是不同的资源。必须分别写出 status 和 finalizers,而 events 属于核心组。通配符既不能用于动词,也不能用于资源。
编写控制器 Deployment
在 labhub-operator 中创建 Deployment webservice-controller。spec.replicas 为 1 或 2,spec.template.spec.serviceAccountName 为 webservice-controller,容器镜像为 ghcr.io/labhub/webservice-controller:v0.1.0,在 args 中加入 --leader-elect=true,分别为容器配置 livenessProbe 和 readinessProbe,并设置 resources.limits.memory。Pod 的 securityContext.runAsNonRoot 必须为 true。
如果使用默认服务账号运行,精心创建的权限就不会生效。需要一个允许多个副本的参数、分别检查是否存活和是否准备接收流量的两个探针,以及应对缓存增长的内存上限。
创建领导者选举租约
在 labhub-operator 中创建 Lease webservice-controller.labhub.io。spec.holderIdentity 是领导者的身份字符串,spec.leaseDurationSeconds 不小于 5,spec.renewTime 是精确到微秒的 RFC3339 时间(date -u +%Y-%m-%dT%H:%M:%S.%6NZ)。并在 /root/op/install/out/lease-note.txt 中说明没有领导者选举会遇到什么问题(两个实例同时协调同一个对象)。
租约包含当前领导者是谁、有效期多长以及最后续约时间。续约时间格式精确到微秒,位数不符就会被拒绝。
升级镜像并确认保留 CRD 版本
将 Deployment 的容器镜像升级为 ghcr.io/labhub/webservice-controller:v0.2.0,并等待滚动发布完成。CRD 必须仍保留至少两个版本,且 status.storedVersions 中必须包含 v1。在 /root/op/install/out/upgrade-check.txt 中写明升级前后检查了什么,并且必须包含对 storedVersions 的检查。
控制器镜像可以通过滚动更新升级,但不能随意删除 CRD 的旧版本。请先检查曾用于存储的版本列表。还必须确认滚动发布已经结束。
修复损坏的 Operator 清单
将 /opt/lab/fixtures/operator/broken-operator.yaml 复制为 /root/op/install/fixed-operator.yaml,修复三处错误并应用。然后在 /root/op/install/out/diagnosis.txt 中说明哪里错了、为什么错,要求每行一项,至少三行。必须包括绑定主体名称问题和缺少 status 子资源权限。修复后,kubectl auth can-i update webservices/status -n crd-lab --as=system:serviceaccount:labhub-operator:webservice-controller 和 kubectl auth can-i list webservices --all-namespaces --as=... 都必须返回 yes。
指向不存在主体的绑定可以在不报错的情况下创建。此外,即使拥有主资源权限,子资源仍是独立的。请使用权限查询,而不是日志进行确认。
创建权限边界验证表
创建 /root/op/install/out/permissions.json。顶层键为 checks,每个元素包含 verb、resource、expected(yes/no),并可按需包含 namespace。不写 namespace 时,按所有命名空间范围进行检查。至少要有 6 项,其中 expected 为 no 的项目至少 2 项,并且其中一项的 resource 必须是 secrets。例如,以下八项就足够:list webservices(yes)、watch webservices(yes)、update webservices/status in crd-lab(yes)、create events in crd-lab(yes)、update webservices/finalizers in crd-lab(yes)、get secrets in crd-lab(no)、delete webservices in crd-lab(no)、create clusterrolebindings(no)。所有项目的 expected 都必须与实际响应一致。
只检查允许的操作无法证明边界。还要加入不应允许的操作,并与实际响应对照。未写命名空间的项目按所有命名空间范围进行检查。