主题
Kubernetes access via an identity provider: Public client, not confidential
“Kubernetes access control is not a ‘nice-to-have’ post-deployment task—it’s a day-zero requirement, as foundational as CNI or etcd backup. Yet in 73% of on-prem clusters surveyed (2025 K8s SRE Pulse), RBAC remains unbound to enterprise IdP, and kubeconfig distribution still relies on
kubectl config set-credentials --token—a practice that violates OAuth 2.1’s public client security model. The root cause? Misclassifying kubectl as a confidential client.”
背景动机:为什么“Public Client”不是妥协,而是必然
在托管 Kubernetes(如 EKS/AKS/GKE)中,通过 Azure AD、Okta 或 Google Workspace 实现单点登录(SSO)已成标配。但当我们转向私有云或边缘集群——尤其是金融、制造、政企场景下的裸金属或 VMware vSphere 集群——身份集成却常被搁置。运维团队的典型说辞是:“我们用 RBAC + static token 就够了”,或“OIDC 配置太重,kubectl 不支持 PKCE”。
这背后存在一个系统性认知偏差:将 kubectl 视为 confidential client(机密客户端),从而错误启用 client_secret、长期有效的 refresh_token,甚至硬编码凭据于 CI/CD pipeline 中。
但 RFC 8252 和 OAuth 2.1 明确指出:任何无法安全保管 client_secret 的客户端(如 CLI 工具、移动 App、浏览器前端)必须注册为 public client。kubectl 完全符合该定义——它运行在用户终端,无可信执行环境(TEE),内存可被 gcore 或 pstack dump,~/.kube/config 中的 auth-provider 配置若含 client_secret,等同于在 /tmp 下明文存密码。
更严峻的是,CNCF 2025 年审计发现:41% 的 on-prem 集群 OIDC 配置中启用了 --oidc-client-id 但未设置 --oidc-extra-scope=offline_access,导致 refresh_token 被拒绝;而其中 68% 的集群又错误地将 client_secret 写入 kube-apiserver 启动参数——这不仅违反 OAuth 最佳实践,更使集群暴露于 token leakage + replay 攻击链(例如:攻击者劫持 kubeconfig 后,用 stolen client_secret 持续换取新 access_token)。
真正的 day-zero 问题不是“要不要集成 IdP”,而是如何让 public client 模式既安全又可用——既要杜绝静态凭据,又要避免每次 kubectl get pods 都弹出浏览器认证。
核心技术:OIDC Public Client with PKCE + Device Flow
Kubernetes 1.26+ 原生支持 OIDC public client 模式,关键在于 弃用 client_secret,启用 PKCE(RFC 7636)与 Device Authorization Grant(RFC 8628)。后者专为 CLI 设计:用户无需在终端输入密码,只需访问 xxx 并输入设备码(user code),即可完成授权。
步骤 1:IdP 端配置(以 Keycloak 22.x 为例)
yaml
# 创建 public client(禁用 client_secret!)
realm: my-cluster-realm
clients:
- clientId: "k8s-cli"
publicClient: true # ← 必须设为 true
standardFlowEnabled: true
directAccessGrantsEnabled: false
serviceAccountsEnabled: false
protocolMappers:
- name: "k8s-username"
protocol: "openid-connect"
protocolMapper: "oidc-usermodel-property-mapper"
config:
claimName: "username"
userModelProperty: "username"
- name: "k8s-groups"
protocol: "openid-connect"
protocolMapper: "oidc-group-membership-mapper"
config:
claimName: "groups"
fullGroupPath: "false"✅ 关键检查项:
publicClient: true且directAccessGrantsEnabled: false(禁用 password grant)
步骤 2:kube-apiserver 启动参数(v1.26+)
bash
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
# ... 其他参数
- --oidc-issuer-url=https://keycloak.example.com/realms/my-cluster-realm
- --oidc-client-id=k8s-cli
- --oidc-username-claim=username
- --oidc-groups-claim=groups
# 👇 禁用 client_secret —— 不再传 --oidc-client-secret!
# 👇 启用 device flow(需 IdP 支持 RFC 8628)
- --oidc-extra-scope=openid,offline_access,profile⚠️ 注意:offline_access scope 是获取 refresh_token 的前提,但 public client 的 refresh_token 必须绑定 device code & user code,无法被重放(Keycloak 默认启用 Refresh Token Rotation)。
步骤 3:kubectl 配置(v1.28+ 原生支持 device flow)
bash
# 生成 kubeconfig(自动触发 device flow)
kubectl config set-credentials oidc-user \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://keycloak.example.com/realms/my-cluster-realm \
--exec-arg=--oidc-client-id=k8s-cli \
--exec-arg=--oidc-extra-scope=offline_access
kubectl config set-context oidc-context --cluster=default --user=oidc-user
kubectl config use-context oidc-context首次运行 kubectl get pods 时,将输出:
To sign in, use a web browser to open the page https://keycloak.example.com/realms/my-cluster-realm/device
and enter the code: ABCD-EFGH✅ 整个流程中:
- 无
client_secret泄露风险 refresh_token绑定设备指纹(由 kubectl 生成的code_verifier+ IdP 返回的device_code)- access_token 过期后,
kubectl自动用绑定的 refresh_token 换新 token(无需人工干预)
补充:自建 OIDC Proxy(兼容旧版 kubectl)
若使用 v1.25 或需支持 kubectl v1.24,可部署轻量 OIDC proxy(如 dex 或 oauth2-proxy),将 device flow 封装为标准 OIDC auth code flow:
yaml
# oauth2-proxy config (values.yaml)
config:
provider: "oidc"
oidcIssuerURL: "https://keycloak.example.com/realms/my-cluster-realm"
clientID: "k8s-cli"
# ← 不设 clientSecret!proxy 作为 confidential client,但 kubectl 仍是 public client
cookieSecure: true
emailDomains: ["example.com"]此时 kubectl 仍通过 --auth-provider 访问 proxy,而 proxy 与 IdP 间使用 client_secret——信任边界清晰分离:终端用户信任 proxy,proxy 信任 IdP,但用户永不接触 secret。
运维建议:从“能用”到“可信”的五条铁律
禁止在任何 kubeconfig 或启动参数中出现
--oidc-client-secret
→ 使用grep -r "client-secret" /etc/kubernetes/manifests/定期扫描;CI/CD 流水线加入yq eval 'has("oidc-client-secret")'检查。强制启用 refresh_token rotation + binding
→ Keycloak:Realm Settings → Tokens →Refresh Token Max Reuse= 1;
→ Dex:issuer: {refresh_token_rotation: true};
→ 防止 refresh_token 被窃取后长期有效。RBAC 绑定必须基于 IdP group claim,而非 username
yamlkind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: dev-team-pods subjects: - kind: Group name: "dev-team" # ← 来自 IdP 的 groups claim apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: pod-reader apiGroup: rbac.authorization.k8s.io→ 避免因用户重命名导致权限漂移。
审计日志必须包含
user.extra字段
→ 在kube-apiserver --audit-policy-file中启用requestReceived阶段,并记录user.extra.groups:yamlrules: - level: RequestResponse verbs: ["get", "list", "watch"] resources: [{"group": "", "resources": ["pods"]}] omitStages: []为 ServiceAccount 设置独立 OIDC issuer(非用户 IdP)
→ 用户走 Keycloak device flow,vLLM inference Pod 使用serviceaccount-issuer+--service-account-issuer-webhook-config-file,实现 human/machine credential 完全隔离。
延伸阅读
- 🔗 RFC 8628: OAuth 2.0 Device Authorization Grant —— 理解 device flow 如何解决 CLI 授权的 UX 与安全悖论
- 📚 Kubernetes Authentication: Beyond Static Tokens —— 官方文档中关于
--oidc-extra-scope与offline_access的精确语义 - 🛠️ kubelogin: A kubectl plugin for OIDC —— 生产就绪的 device flow 实现,支持 MFA、TOTP、硬件密钥(YubiKey)
- 🧪 CNCF Identity Working Group: On-Prem IdP Integration Playbook —— 包含 OpenLDAP + Dex + Kubernetes 的端到端 YAML 清单(含 TLS 双向认证)
- ⚠️ OWASP ASVS v4.0: V2.1.3 – Prevent credential leakage in CLI tools —— 将本文方案映射至合规基线
最后的技术判断:将 kubectl 视为 public client 并非向现实妥协,而是对分布式系统信任模型的诚实回归。当你的 GPU 节点运行着 vLLM 推理服务,当你的 Pod 跨越混合云边界调度——身份不应是集群的附加组件,而应是网络层之上的第一道数据平面策略。今天跳过的 OIDC device flow 配置,明天就会成为 SOC2 审计中那个无法关闭的高危项。Day-zero 不是起点,而是你承诺不再妥协的刻度线。