跳至主内容
博客

让 AI SRE 安全地访问你的 Kubernetes 集群:不开公网,不交 kubeconfig

把集群接进 AI SRE,我们要三件事同时成立:不开公网、权限收得紧、不做能力表。最后的做法是让 Agent 跑真的 kubectl,但由你的集群主动连出来,权限全部交给 Kubernetes 自己判。

Flashduty 工程团队

让 AI SRE 安全地访问你的 Kubernetes 集群:不开公网,不交 kubeconfig

一个 Pod 反复重启。AI SRE 能读告警,能读代码,能读监控图,唯独看不见这个 Pod 现在什么状态、刚才发生过什么事件。它只能猜,或者让你自己去敲 kubectl,把结果贴回对话框。

要把集群接进来,目标有四条:

  • Agent 能用 kubectl 的全部表达力查你的集群
  • 凭据不出集群
  • 不开任何入站端口
  • 权限收得紧,而且不靠一张我们手工维护的能力表

最后一条最难。集群里有什么取决于你装了什么,CRD 的长尾是无限的,手工列举的表第一天就是不全的。真正难的是通用和收紧同时成立:接口越通用,能力面越大,越难把权限收住。

现成的三种接法

做法一:给它一份 kubeconfig,直连集群。

问题首先在安全上:凭据离开了集群。而且多数生产集群的 apiserver 不对公网开,要么你开公网、再给我们的 IP 段加白名单,要么把 Agent 塞进你的内网,这两件事实际推的时候基本被拒。

也不够简单。托管集群(EKS、GKE、AKS)的 kubeconfig 里根本没有凭据,它是一段「去调用本地某个命令换令牌」的说明,比如 aws-iam-authenticatorgke-gcloud-auth-plugin。这个文件复制到别处就不工作了,所谓「拷一份过来」在托管集群上物理上不成立。

它唯一拿满分的是通用,kubectl 能干的它都能干。这一点我们后来留下来了。

做法二:上一个 Kubernetes MCP 服务端。

问题首先在通用上。MCP 给 Agent 的是一组事先定义好的工具,而工具表是人写出来的。kubectl get pods -o jsonpath='{.items[?(@.status.phase!="Running")].metadata.name}' 这种查询,或者你自己集群里的某个 CRD,表里没有就是没有。我们对工具表有多难堆全这件事有发言权,平台自己的操作能力就是因为这个从 MCP 换成了 CLI,那次的账算在从 MCP 到 CLI 里。

安全上的问题更隐蔽。今年 5 月 mcp-server-kubernetesCVE-2026-46519 就是例子:它的「只读模式」开关只在列出工具的时候生效,真执行的时候没生效,知道工具名就能直接调删除。公告里那句话值得记:这个绕过绕不开 Kubernetes 已经给它的权限。也就是说,真正拦住它的从头到尾都是集群自己的 RBAC,不是那个开关。

做法三:我们自己造一套 Kubernetes 工具。

哪怕只做有限的只读能力,也有两层问题。第一层和做法二一样:工具表是人写的,覆盖不了全部操作,我们得永远追着 Kubernetes 的 API 和你集群里的 CRD 跑。第二层更隐蔽:每一张工具 schema 都要常驻上下文窗口,而且模型没见过这些工具,怎么调、参数什么含义,都得靠工具描述现教,教得再好也比不上它本来就会的。kubectl 不一样,它的用法早就在训练数据里喂熟了,jsonpath 这种查询它写得比人顺。造一张能力表,等于把一个说得很流利的人,改成只能用几句背下来的句子。

目前厂商和社区能拿到的接入方式,大体也不出这三种。结论很清楚:接口要保留 kubectl 的全部表达力,但不能让它直连集群,也不能把权限判定放在我们这一侧。

我们的做法:真的 kubectl,但不直连

Agent 跑的是真的 kubectl,没有任何包装层。变的只是它那份 kubeconfig 里的地址,指向的不是你的 apiserver,是我们的一个地址。

Agent 不需要真的拿到集群凭据,它只需要 kubectl 相信自己拿到了。

请求怎么进你的集群?让你的集群先连出来。你在集群里装一个很小的程序,它启动就往外连我们,连上就一直挂着。之后所有请求,都顺着这条已经建好的连接往回走。

于是「网络能不能通」塌缩成一句话:你的集群能不能上网。没有防火墙工单,没有 VPN,没有跳板机,没有 IP 白名单。

一次 kubectl 请求逐跳走过的路:请求从左往右流,连接却是集群从右侧主动建出来的,所以你不需要开任何入站端口

逐跳是这样:

kubectl(会话执行环境)
  → 我们的公网地址 /safari/k8s/proxy/<集群>/...
  → 校验:令牌、集群归属、请求形状
  → 顺着集群拨出来的那条连接送回去
  → 集群里的转发器,原样重放给 apiserver
  → apiserver 用它自己的 RBAC 判定准不准

几个数字:单次请求超时 60 秒,请求体上限 1 MiB,响应体上限 6 MiB,连接每 54 秒心跳一次。

三种做法摆在一起:

交出 kubeconfig装一个 MCPKubernetes App
你要开的入站端口apiserver 得能被访问到看实现0
离开集群的凭据整份 kubeconfig看实现没有
能表达的查询kubectl 全集工具表里有的kubectl 全集
CRD 长尾支持要等工具作者加传输层原样透传
托管集群能用吗不能看实现
我们在你集群里跑什么什么都不跑看实现一个 2.7 MB 的二进制,没有 shell

一个必须写明的取舍:这条连接是一问一答的形状,所以 kubectl logs -fexecport-forwardwatch 都不支持。要看变化就轮询。

一个什么都不留的转发器

集群里那个程序是纯转发器,我们把它做到只剩一个静态二进制。

docker run --rm --entrypoint sh \
  registry.flashcat.cloud/public/flashduty-k8s-agent:v0.1.0 -c 'echo hi'

exec: "sh": executable file not found in $PATH

bash、sh、ash、dash、busybox、kubectl、curl、wget、cat、ls、env、python、perl,一个都没有。整个镜像 2.7 MB,2 层。

为什么做到这个程度:就算我们这边被整个攻陷,攻击者在你集群里也拿不到一个能执行东西的壳。这条不用信我们,镜像是公开的,上面那条命令你自己跑一遍就知道。

一份 kubeconfig,多个集群

多集群在这个设计里几乎是白捡的。既然 Agent 用的是原生 kubectl,多集群就是 kubectl 本来就有的 context 概念,我们一个新词都不用发明。

每一轮对话开始时,我们按这个会话当前能看到的集群,重写一份 kubeconfig 放进它的执行环境。一个集群一条 context,每条自带自己的地址和自己的令牌,不是共用一个凭据,集群之间在凭据层面就是隔开的。Agent 要切集群,就是它早就会的那一句:

kubectl --context <集群别> get pods -n payment

两个设计细节值得写。

第一,只有一个集群时才设默认 context,两个及以上,我们故意不设默认。注入给模型的说明里明写着:不要猜默认 context,先跑 kubectl config get-contexts,不确定就问。这是从机制上堵掉「因为 prod 恰好是默认,所以命令打在了 prod 上」。

第二,这份文件每一轮都重写一遍,包括在什么都不允许时写成一份空文件。所以权限一收回,下一轮就没有那条 context 了,不会留下残影,已停用的集群也永远不会被写进去。

权限交给 K8s 自己判

我们做过一版:4 个专门的写操作工具,每次执行前弹一次人工确认。后来整套删了。

理由前面其实已经讲过一半:凡是在 apiserver 之前做的判断,本质上都只是建议。Kubernetes 自己就有一套完整的权限系统,谁、对什么资源、做什么动作、在哪个 namespace。在它上面再糊一层,我们不会更安全,只会多一套要维护、要审计的东西,而且新的那套更弱。

所以现在权限只有一个地方定义:一份由控制台生成、由你自己 apply 的 RBAC YAML。三档:

  • 只读:绑定 Kubernetes 内置的 view ClusterRole,常见资源的 get、list、watch
  • 读取 + 有限修改:只多两样,改副本数和驱逐 Pod
  • 完全权限:绑定内置的 cluster-admin,真的不设限

只读档值得一说。它最早是我们手写的资源清单,后来换成直接绑定内置的 view,原因有两个:手写清单悄悄漏掉过 configmaps,而且永远追不上 CRD 的长尾。view 由上游维护,永远不含 secrets;它还带一条 aggregationRule,operator 把读权限标记成 view-safe 的 CRD 会自动包含进来,不用等任何人加。生成出来的核心就是这一行绑定:

roleRef:
  kind: ClusterRole
  name: view  # 内置只读面,不含 secrets;view-safe 的 CRD 自动聚合
  # …完整的三档 YAML 由控制台生成,配置说明见文末的产品文档

有限修改档守着一条不变量:不能改变 Pod 的有效 spec。镜像、命令、环境变量、身份、挂载,一样都动不了;能做的只有改一个已经声明好的 spec 跑几份,或者驱逐一个 Pod、让控制器按原样重建。为什么卡这条线:只要能改 spec,就可以把命令改成打印挂载进来的凭据,再借只读档就有的 pods/log 读回去,整个权限模型就形同虚设。代价是 kubectl rollout restartrollout undo 也不可用,它们本质都是对工作负载的 PATCH,任何授权层都分不出真回滚和偷换模板。重启有替代,就是逐个驱逐;回滚没有替代,只能建议人来执行。

这份 YAML 是你自己读、自己装的。边界你能亲眼验证,不需要相信我们的说法。

不过只靠 RBAC 也不够,因为 RBAC 只认「动词 × 资源」,认不出那个接口实际在干什么。安全团队 Horizon3 写过一个例子:一个只有 nodes/proxy 读权限的账号,纸面上纯只读,能通过 kubelet 从只读边界走到任意命令执行,因为建连用的是 GET 请求,权限系统看到的是「读」。上游把这个标成了不修。

所以我们在自己这边还拦一道,但拦的不是权限,是请求的形状:路径按 / 切开,任何一段是 execattachportforward 就直接拒,带 watch=truefollow=true 的也拒。

两层拦截的洞开在不同位置:我们这边收窄请求形状,你的集群用 RBAC 判权限,哪一层都不完整,能靠的是重叠

两层各自的边界:

拦得住拦不住谁兜底
我们这边的形状收窄路径里带 execattachportforward,包括 kubelet 那条 .../proxy/exec/...kubelet 另外几个执行入口(例如 .../proxy/run/...)不在列表里我们压根不发 nodes/proxy 权限,到 apiserver 直接被拒
集群自己的 RBAC只读档和「读取 + 有限修改」档里都没有 nodes/proxy「全部 namespace + 完全权限」是不设限的,这条路它有只剩控制台上那条红色警告——这条线守不守得住靠人

所以我们不说 Agent 碰不到什么,只说出事的时候能出多大。没有哪一层是完整的,能靠的是两层的重叠。

安装这一步,你自己跑

控制台本来可以替你把东西装好,前提是你把集群凭据交给我们。那就绕回前面被否掉的做法一了。

所以我们选了另一头:控制台生成一条命令,你自己在集群里跑。多了一步操作,换来的是你在 apply 之前,能把要装进集群的东西整个读一遍。

既然这条命令跑在别人的生产集群上,我们把它做得相当偏执。

整个脚本体包在 main() { ... } 里。 理由写在代码注释里:万一脚本下载被截断,或者被人改坏了,它会在语法检查阶段就失败,而不是把半截脚本执行掉。

动集群之前先逐条预检权限。 kubectl auth can-i 检查八项,roles、rolebindings、clusterroles、clusterrolebindings 各自的 list 和 delete,缺哪条列哪条,然后退出,并打印一句 No cluster resources were changed.

过了预检还不直接装。kubectl apply --dry-run=server 让你的 apiserver 自己验一遍,再真装。

卸载是对称的:按精确的归属标签删,只删这个 App 装进去的东西。代码里明确禁止用「标签存在」这种宽松写法,注释写着理由,那会匹配到集群里装的每一个 App。共享的那个 namespace 也不动,因为可能还有别的 App 在用。

安装和卸载命令的链接 10 分钟过期。

在控制台里,Kubernetes App 和 GitHub、GitLab App 摆在同一个目录下,但接进来的方式不一样:那两个是走授权流程换凭据,我们写过AI SRE 怎么读你的代码仓库;集群这个是你的集群自己连出来。

效果和边界

最后交付出来的东西:kubectl 的完整查询能力可用,入站端口为零,没有凭据离开集群,权限边界是一份你自己读、自己 apply 的 YAML。

边界也要说清楚:

  • 不支持流式操作。logs -fexecport-forwardwatch 都不行,要看变化就轮询。
  • 只读档读的是内置 view 的面:常见资源都在,secrets 永远不在。CRD 要看它的 operator 有没有把读权限标记成 view-safe:标记了自动可读,没标记的读不到,要读只能上完全权限档。
  • 「读取 + 有限修改」档动不了 Pod 的 spec,镜像、命令、环境变量都改不了,只能改副本数和驱逐 Pod。rollout restartrollout undo 因此也不可用,重启靠逐个驱逐替代,回滚只能提给人做。
  • 「全部 namespace + 完全权限」是真的不设限,控制台上那条红色警告是认真的。
  • 吊销只切断连接,集群里已经装上的东西不会自己消失。

功能本身的说明在这条更新里,怎么配在产品文档里。想试的话,去控制台 AI SRE 的插件里创建一个 Kubernetes App,先只给一个 namespace 的只读,看看它能查出什么。

相关阅读