AI Agent 接入 Kubernetes 时,如何正确配置最小权限 ServiceAccount、短期 Token、RBAC Role 和 Kyverno 准入策略,防止 MCP 服务器漏洞或提示注入导致权限滥用。
RBAC 是天花板,其他都是装饰
每篇关于给 LLM Agent 授予集群访问权限的指南——包括我自己写的 kubectl MCP server 文章——都是顺带提一句:"用最小权限的 ServiceAccount 运行",然后就跳到工具部分了。本文要讲的就是每个人都会跳过的那个部分。
先看简版。一个会操作 Kubernetes 的 AI Agent,应该拥有自己专属的 ServiceAccount,绑定到一个只包含读动词的 namespaced Role,不能访问 Secrets、pods/exec 或 impersonation,用短期令牌认证(几分钟,不是几个月),其 API 活动在审计日志中隔离,绑定关系被 admission policy 冻结。下面是完整的 YAML、令牌配置、auth can-i 测试矩阵,以及防止天花板悄悄上移的 Kyverno policy。
为什么这么偏执?因为工具白名单、prompt 和审批门都是应用层控制。如果它们背后的凭证可以删除 namespace,那你的 MCP server 里一个 bug——或者一次 prompt injection 让模型"创意发挥"地调用某个工具——就会继承那个权限。RBAC 是由 API server 强制执行的;这是 Agent 无法靠幻觉越过的唯一一层。正如我在 agent harness 文章里说的,权限就是 Agent 的 IAM。以下就是那个 IAM 应该长成的样子。
为什么 Agent SA 不等于 CI bot SA
你已经让很多非人类主体访问 API server 了:CI 流水线、controller、operator。直觉反应是克隆其中一个 ServiceAccount。但不要这样做。Agent 在两个方面不同于它们,这两点会改变 RBAC 设计:
Agent 自己生成请求。CI 任务执行的是审查过的流水线文件里的命令。Agent 在运行时决定调用什么,受到进入其上下文的任何文本影响——日志、告警、PR 评论。它的请求流本质上是不可信输入,所以必须假设请求者处于困惑或被操纵状态来设定天花板。
Agent 读取一切能读取的东西。Controller 只读它管理的三种资源类型。Agent 在排查故障时会愉快地获取任何可读内容——而如果 Secrets 可读,secret 内容就会进入模型上下文、对话记录,还可能进入第三方 API。这是一次有额外步骤的凭证泄露,所以 Agent 的 secrets 管理第一步就是"Agent 的 SA 不能读 Secrets,百分之百禁止"。
设计规则:每个 Agent 一个 ServiceAccount、一个 namespace、只读、默认拒绝。永远不要复用人类的 kubeconfig,永远不要为了"暂时方便"绑定 cluster-wide 的 view,永远不要跨 Agent 共享一个 SA——共享身份会毁掉你需要在凌晨三点排查奇怪事件时用到的审计跟踪。
ServiceAccount 和 Role
诊断 Agent 需要的全部权限,仅此而已。注意资源列表里缺了什么——首先是 secrets:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ops-agent-readonly
namespace: staging
automountServiceAccountToken: false # 没有任何东西应该隐式挂载这个
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ops-agent-readonly
namespace: staging
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "events",
"configmaps", "endpoints", "resourcequotas"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ops-agent-readonly
namespace: staging
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ops-agent-readonly
subjects:
- kind: ServiceAccount
name: ops-agent-readonly
namespace: staging
三个值得专门点出的刻意选择:
pods/log 需要显式授权。日志访问是一个 subresource——单独授权 pods 不会包含日志,反之亦然。诊断 Agent 靠日志为生,所以要授权;但记住日志也可能包含 secrets,这也是在敏感 namespace 中让 Agent 通过带脱敏功能的日志搜索 MCP server 而非直接访问 pods/log 的理由。
configmaps 是需要权衡的。Agent 经常需要 ConfigMaps 来排查配置漂移问题,但团队往 ConfigMaps 里塞凭证的频率比任何人都愿意承认的都要高。如果你们团队也这样,从规则里删掉它,让 Agent 去问人。
不用 ClusterRole。Role 把 Agent 限制在 staging 范围。如果 Agent 后续需要第二个 namespace,创建第二个 RoleBinding 指向同样的共享 ClusterRole 风格规则——不要因为 YAML 行数少就选择集群范围授权。
真正会造成伤害的 subresources
危险的权限大多是 subresources 和 RBAC 元动词,而且用通配符很容易误授权:
pods/exec、pods/attach、pods/portforward——在 pod 里开一个 shell 等效于用 pod 自己的凭证执行任意代码。没有任何自主 Agent 应该获得这些。
secrets 的 get 或 list——仅 list 就能返回完整的 Secret 对象,包含 base64 编码。两个动词在这里都是高危的。
serviceaccounts/token——为其他 ServiceAccount 铸造令牌的能力就是即服务形式的权限提升。
impersonate、escalate、bind——这些 RBAC 元动词允许一个身份成为别人或扩大自己的角色。如果你在 Agent 的 Role 上看到其中任何一个,当作安全事件处理。
经验法则:永远不要在 Agent Role 中使用通配符。resources: ["*"] 会静默包含上述所有 subresource,在今天和每一次集群升级之后都是如此。
短期令牌,不是永久密钥
经典错误是创建一个 ServiceAccount token Secret,然后把令牌粘贴到一个在 Agent 机器上躺一年的 kubeconfig 里。从 Kubernetes 1.24 开始,正确的模式是 TokenRequest API——会过期且绑定到特定 audience 的令牌:
# 为 Agent SA 铸造 1 小时令牌(需要 TokenRequest API,v1.24+)
TOKEN=$(kubectl create token ops-agent-readonly -n staging --duration=1h)
# 构建 MCP server 将使用的 kubeconfig——其他任何东西都不会用到
kubectl config set-credentials ops-agent \
--token="$TOKEN"
kubectl config set-context mcp-readonly@cluster \
--cluster=cluster --user=ops-agent --namespace=staging
生产环境不要手动做这个:启动 MCP server 的包装器在启动时铸造新令牌,过期时重新铸造。如果 Agent 运行在集群内部,用带 expirationSeconds: 3600 的 projected token volume 代替——kubelet 会帮你轮换。无论哪种方式,你购买的核心属性是一样的:泄露的凭证只值一个小时,不是一年。令牌只出现在一个地方——MCP server 的环境变量里——永远不会进入模型的上下文、prompt 或记忆。
验证天花板:auth can-i 测试矩阵
未经测试的 RBAC 就是你在猜。kubectl auth can-i --as 让你不需要持有令牌就能检查 Agent 的精确权限:
AS="system:serviceaccount:staging:ops-agent-readonly"
# 必须全部返回 "yes"
kubectl auth can-i get pods -n staging --as="$AS"
kubectl auth can-i get pods/log -n staging --as="$AS"
kubectl auth can-i list deployments -n staging --as="$AS"
# 必须全部返回 "no"
kubectl auth can-i get secrets -n staging --as="$AS"
kubectl auth can-i create pods/exec -n staging --as="$AS"
kubectl auth can-i delete pods -n staging --as="$AS"
kubectl auth can-i get pods -n prod --as="$AS"
kubectl auth can-i '*' '*' -A --as="$AS"
把这个作为脚本放到 CI 里,任何意外回答都会导致失败,紧邻着 RBAC manifest。这是你的 ops agent 做过的最便宜的评估:确定性、亚秒级,在 Agent 触发之前就能捕获"有人好心扩大了 Role"的回归。
写路径用不同的身份
迟早 Agent 会从诊断毕业到修复。不要扩大 ops-agent-readonly。创建第二个 ServiceAccount——ops-agent-write——绑定到你能 defend 的最小变更表面(比如 deployments 的 patch 权限用于重启和副本变更),然后把所有调用路由到一个人类在环的审批流程,比如 human-in-the-loop approval gates。两个身份意味着整天无人值守运行的读循环在物理上无法修改任何东西,而写身份可以在事件中一条命令禁用。更进一步,完全绕过直接写操作,让 Agent 打开 pull request 而不是执行 kubectl——这样写 SA 就属于你的 GitOps controller,你本来就信任它。
像对待不可信用户一样审计 Agent
专属 SA 使审计日志有用:Agent 发出的每个请求都带有 user.username: system:serviceaccount:staging:ops-agent-readonly。给它专属的 audit policy 条目,使 Agent 活动的捕获保真度高于普通系统噪声:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse # Agent 做的任何事都记录完整 body
users: ["system:serviceaccount:staging:ops-agent-readonly",
"system:serviceaccount:staging:ops-agent-write"]
- level: Metadata # 其他一切保持低成本
把这些条目发送到其他遥测数据相同的地方,并在两种情况下告警:来自 Agent SA 的任何 403 Forbidden(Agent 正在尝试超过其天花板——可能是 bug,可能是 injection),以及写 SA 在审批变更窗口外的任何请求。这是追踪 Agent 自身工具调用的 API server 端配合:Agent 的日志告诉你它打算做什么;审计日志告诉你它实际做了什么。
用 admission policy 冻结天花板
最后一个失效模式是漂移:六个月后的凌晨两点,某人把 cluster-admin 绑定到 Agent SA 上"临时"调试。Kyverno policy 把这变成硬错误而不是定时炸弹:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: freeze-agent-rbac
spec:
validationFailureAction: Enforce
rules:
- name: only-approved-roles-for-agent-sas
match:
any:
- resources:
kinds: ["RoleBinding", "ClusterRoleBinding"]
preconditions:
any:
- key: "ops-agent-readonly"
operator: AnyIn
value: "{{ request.object.subjects[].name || `[]` }}"
- key: "ops-agent-write"
operator: AnyIn
value: "{{ request.object.subjects[].name || `[]` }}"
validate:
message: "Agent ServiceAccounts may only bind their approved Roles (change via GitOps review)."
deny:
conditions:
all:
- key: "{{ request.object.roleRef.name }}"
operator: AnyNotIn
value: ["ops-agent-readonly", "ops-agent-write"]
任何将 Agent SA 授予批准列表外角色的绑定都会在 admission 阶段被拒绝,集群范围,无论谁 apply 的。现在 Agent 权限变更有且只有一个路径:在 Git 里编辑 Role,通过 review,让 CI auth can-i matrix 确认新天花板。
MCP server 里的工具白名单是 UX 功能。RBAC 层才是安全边界。每个 Agent 用自己的 ServiceAccount;在显式资源列表上授予读动词,不含 Secrets、不含 exec、不含通配符;用一小时内会死的令牌认证;把写路径拆分到第二个带门的身份;监控审计日志里的 403;用 admission policy 冻结绑定,防止天花板漂移。这样做了之后,当你的 Agent 幻觉出一条破坏性命令时,说不的是 API server——不是你的 prompt。