这篇整理的是按用户维护 RBAC 清单的做法。需要说明的是,这套还没落到我那个 demo 仓库里:仓库里目前仍是前一篇那套脚本,下面的 YAML 是我打算怎么改的形状,不是已经跑起来的系统。按组(group-based)等授权方式留到后面再写。

背景:mirrord 的 RBAC 现状#

mirrord 是开发者本地调试工具,让开发者的本地进程可以通过集群中的 agent pod 访问 in-cluster 的网络资源(mirrord 是什么、完整 demo 见 用 mirrord 把本地进程接入 K8s 集群)。作为集群管理员,如果你想要:

  • 开发者能在指定 namespace 运行 mirrord
  • 开发者不能访问其他 namespace
  • 开发者不能删除应用或改动配置
  • 权限变化可以审计和回滚

那么你需要给每个开发者颁发 identity(身份凭证)授权清单(RoleBinding)

mirrord 的两层 RBAC 模型#

mirrord 的权限拆分挺有意思,分成两部分:

层级 资源 作用域 目的
命名空间权限 RoleBindingClusterRole/mirrord-developer 某个 namespace 赋予开发者在该 namespace 内 create/delete jobs、port-forward 等能力
cluster-scoped 权限 ClusterRoleBindingClusterRole/mirrord-impersonator 集群范围 赋予开发者 impersonate ServiceAccount 的能力(WebSocket 隧道认证必需)

这么分的原因是:impersonate serviceaccounts 是 cluster-scoped 虚拟资源,namespace-scoped 的 RoleBinding 无法满足(这条 cluster-scoped 约束的完整原委,见 VS Code 跑 mirrord 撞上 WebSocket 403)。

当前的脚本式操作流程#

现有的实现中(这套脚本和按 namespace 授权的 demo 来自前一篇 给 mirrord 开发者按 namespace 签发 kubeconfig),管理员手工运行:

# 第 1 步:为开发者生成身份凭证
bash rbac/admin/scripts/issue-developer-kubeconfig.sh alice

# 第 2 步:授予 namespace 权限
bash rbac/admin/scripts/grant-namespace-access.sh alice team-a-dev

# 第 3 步:收回权限时
bash rbac/admin/scripts/revoke-namespace-access.sh alice team-a-dev

这些脚本做的本质上就是:

  1. 生成证书 + kubeconfig
  2. 创建 RoleBindingClusterRoleBinding
  3. 删除这些 binding

问题

  • 授权变更不经过代码审查
  • 无法追踪谁在什么时间做了什么
  • 无法轻松回滚
  • 分布在多个脚本中,容易遗漏

方案:按用户维护 RBAC 清单#

如果你想让 mirrord 授权管理变成 GitOps,比较直接的做法是 把授权关系写成声明式的 YAML,存进 Git,让 Argo CD 或 Flux 去 reconcile。

核心思路#

每个开发者的授权 = 一对 binding YAML 文件 + git 里的声明

# rbac/live/alice.yaml
---
# RoleBinding: alice 在 team-a-dev 里能做什么
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mirrord-developer-alice
  namespace: team-a-dev
  labels:
    mirrord-rbac-demo/user: alice
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-developer
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice              # ← 身份来自 kubeconfig

---
# ClusterRoleBinding: alice 能否 impersonate serviceaccounts
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: mirrord-impersonator-alice
  labels:
    mirrord-rbac-demo/user: alice
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-impersonator
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

目录结构建议#

mirrord-demo/
├── rbac/
│   ├── base/
│   │   ├── 01-mirrord-developer-clusterrole.yaml       # 基础定义(不变)
│   │   ├── 01b-mirrord-impersonator-clusterrole.yaml  # 基础定义(不变)
│   │   └── kustomization.yaml
│   ├── live/
│   │   ├── alice.yaml                                  # alice 的权限绑定
│   │   ├── bob.yaml                                    # bob 的权限绑定
│   │   ├── charlie.yaml                                # charlie 的权限绑定
│   │   └── kustomization.yaml                          # 声明所有 user YAML
│   ├── admin/
│   │   ├── scripts/
│   │   │   ├── issue-developer-kubeconfig.sh          # 保留:生成 kubeconfig(一次性)
│   │   │   └── lib.sh
│   │   └── manifests/
│   │       ├── 02-namespaces.yaml
│   │       ├── 03-test-workloads.yaml
│   │       └── ...
│   ├── developer/
│   │   └── scripts/
│   │       ├── whoami.sh
│   │       └── run-mirrord.sh
│   └── validate-rbac.sh
├── .argocd/                           # ← 新增:Argo CD 配置
│   └── mirrord-rbac-app.yaml
└── ...

工作流变化#

授权和撤销走的是同一条路:改 rbac/live/<user>.yaml、提 PR、评审、merge,剩下的交给 Argo CD reconcile。原来那三个脚本里「直接对集群动手、没有记录」的部分就没有了。

flowchart LR P["PR 改 rbac/live/alice.yaml"] --> R["评审 + merge"] R --> S["Argo CD 检测到变化"] S --> A["创建或删除 binding"]

按用户维护 RBAC 的具体步骤#

第 1 步:生成 kubeconfig(一次性,脚本不变)#

开发者身份来自客户端证书。这部分仍然使用现有脚本(CSR + API 签名):

# 管理员执行
bash rbac/admin/scripts/issue-developer-kubeconfig.sh alice

# 输出:rbac/.credentials/alice.kubeconfig
# 这个文件包含:
#   - alice.crt(签好的证书)
#   - alice.key(私钥)
#   - 集群 CA 和 server 地址

# 把 kubeconfig 安全地发给 alice(不走 Git)

安全建议

  • 不要把 kubeconfig 提交进 Git
  • 不要把 alice.key 放进 Git
  • 通过 1password、vault 或 secure email 分发
  • 或者更好的做法:让开发者自己生成私钥和 CSR,管理员只签名 .crt,私钥永不离开开发者机器

第 2 步:维护 Git 中的授权清单#

根据需要修改 rbac/live/alice.yaml 中的 RoleBinding 和 ClusterRoleBinding。

示例 1:授予 alice 在 team-a-dev 的权限

编辑或创建 rbac/live/alice.yaml

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mirrord-developer-alice
  namespace: team-a-dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-developer
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: mirrord-impersonator-alice
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-impersonator
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

提交 PR:

git add rbac/live/alice.yaml
git commit -m "feat(rbac): grant alice access to team-a-dev namespace"
git push origin feat/alice-team-a-dev

示例 2:授予 alice 在多个 namespace 的权限

如果 alice 需要访问 team-a-devteam-b-dev,把多个 RoleBinding 写在同一个文件里:

---
# team-a-dev
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mirrord-developer-alice
  namespace: team-a-dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-developer
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

---
# team-b-dev
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mirrord-developer-alice
  namespace: team-b-dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-developer
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

---
# Cluster-scoped impersonation(只需一个)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: mirrord-impersonator-alice
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: mirrord-impersonator
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: alice

第 3 步:用 Kustomize 聚合多个用户#

用户多了以后逐个 apply 很繁琐,用一个 kustomization 把 rbac/live/ 下的文件聚合起来,Argo CD 指向这个目录就行。创建 rbac/live/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:
  - ../base

resources:
  - alice.yaml
  - bob.yaml
  - charlie.yaml

commonLabels:
  app.kubernetes.io/part-of: mirrord-rbac

第 4 步:配置 Argo CD 自动同步#

创建 .argocd/mirrord-rbac-app.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: mirrord-rbac
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/meirongdev/mirrord-demo
    targetRevision: main
    path: rbac/live
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

prune: true 是这套能「删 YAML 等于收权限」的关键:文件从 Git 里消失,Argo CD 才会把对应的 binding 从集群里删掉。

用户身份的管理#

按用户维护 RBAC 的前提是有一个稳定的身份方案。前一篇那套脚本是管理员本地生成私钥再签发,一条命令搞定,但私钥要过管理员的手,开发者也没法自己轮换。更稳妥的是让开发者自己生成私钥和 CSR,管理员只签名:

# 开发者本地:生成私钥和 CSR,只把 .csr 交出去
openssl genrsa -out alice.key 2048
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice"

# 管理员核实身份之后签名
kubectl apply -f - <<EOF
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: mirrord-rbac-demo-alice
spec:
  request: $(base64 < alice.csr | tr -d '\n')
  signerName: kubernetes.io/kube-apiserver-client
  usages:
    - client auth
EOF

kubectl certificate approve mirrord-rbac-demo-alice
kubectl get csr mirrord-rbac-demo-alice -o jsonpath='{.status.certificate}' | base64 --decode > alice.crt

这几行我踩过三个坑。spec.usagescertificates.k8s.io/v1 里是必填的,漏写会被 API server 直接拒掉(spec.usages: Required value);对 kubernetes.io/kube-apiserver-client 这个 signer 只需要 client auth,写 server auth 之类会被签发控制器拒签。heredoc 的分隔符要用不带引号的 <<EOF,写成 << 'EOF' 会关掉 $(...) 替换,request 里塞进去的就成了字面字符串,报 illegal base64 data。最后 | tr -d '\n' 不能省,Linux 的 base64 按 76 列折行,而证书数据必须是单行。

开发者拿 alice.crt 配上自己的 alice.key 和集群 CA 拼出 kubeconfig,私钥全程没离开过他的机器。

权限边界#

mirrord-developermirrord-impersonator 这两个 ClusterRole 具体包含哪些规则,前一篇 《给 mirrord 开发者按 namespace 签发 kubeconfig》 里逐条列过,这里不重复贴。要点是它只给 mirrord 跑起来必需的动作:读工作负载、起停 agent Job、port-forward、impersonate 目标 pod 的 ServiceAccount。删 Deployment、改 ConfigMap、访问别的 namespace 都不在里面。

开发者可以自己核一遍拿到的是什么:

export KUBECONFIG=rbac/.credentials/alice.kubeconfig

# 看自己的身份
kubectl auth whoami
# Output: alice

# 看自己在 team-a-dev 能做什么
kubectl auth can-i get pods -n team-a-dev              # yes
kubectl auth can-i delete deployments -n team-a-dev    # no
kubectl auth can-i list secrets -n team-a-dev          # yes
kubectl auth can-i impersonate users -n team-a-dev     # no

# 看自己在 team-b-dev 能做什么
kubectl auth can-i get pods -n team-b-dev              # no (no binding)

证书签发后无法吊销,能回收的只有授权#

这是我做这套改造时想清楚的一件事,也是「把授权放进 Git」真正的理由。已经 kubectl certificate approve 之后想反悔,会发现:

  • approve 不可逆:CSR 的 Approved / Denied 是一次性、互斥的终态。一旦 approve,签发控制器会立刻把证书写进 .status.certificate,没法再改成 Denied,也没有「反 approve」。
  • 删掉 CSR 对象 ≠ 吊销证书kubectl delete csr <name> 只删集群里那个资源对象,已经签发出去的 .crt 仍然有效到过期。Kubernetes 的客户端证书认证没有 CRL / 吊销机制,这是它一个有名的限制。

所以真正要「取消」的不是身份(证书决定 CN=alice 是谁),而是授权(RBAC binding 决定 alice 能干什么)。删掉 binding 后,这份证书还能登录、kubectl auth whoami 还认得出 alice,但任何操作都会 403,这才是有效的回收。回到前面的流程:删掉 rbac/live/alice.yaml、merge,Argo CD 靠 prune 把 binding 收走,回收就完成了。

想做的事 是否有效 说明
把 approve 改成 deny 不行 condition 是终态,不可逆
kubectl delete csr 没用 只删对象,已签发的证书照样有效到过期
RoleBinding / ClusterRoleBinding 有效 身份还在但没权限,实际等于回收
轮换集群 CA 有效,但是核弹 所有用户证书一起失效,影响整个集群
等证书过期 有效 取决于签发时设的有效期

这也反过来说明了「证书有效期别设太长」和「用短期凭证 + 可自助轮换」的价值:既然没法吊销,过期就是唯一的兜底。

小结#

改造本身就三件事:把每个人的 RoleBinding + ClusterRoleBinding 写成一个 YAML 文件放进 rbac/live/,用一个 kustomization 聚合,再让 Argo CD 带 prune 指向这个目录。身份那一半仍然走 CSR 脚本,不进 Git。之后加人、改 namespace、收权限都是同一个动作:改那个文件、提 PR。

相比原来那三个脚本,换来的是变更有 PR 记录、回滚是 git revert、以及集群状态和 Git 声明持续对齐。代价是多一个 Argo CD Application 要维护,以及授权生效从「跑完脚本立刻生效」变成了「merge 之后等一次 sync」。

按用户维护适合我这种用户数不多、权限关系基本是「是或否」的场景。人再多就该按组走 IdP 了,那是另一篇的事。

相关文章#

参考资料#