Skip to content

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 clientkubectl 完全符合该定义——它运行在用户终端,无可信执行环境(TEE),内存可被 gcorepstack 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: truedirectAccessGrantsEnabled: 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(如 dexoauth2-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

运维建议:从“能用”到“可信”的五条铁律

  1. 禁止在任何 kubeconfig 或启动参数中出现 --oidc-client-secret
    → 使用 grep -r "client-secret" /etc/kubernetes/manifests/ 定期扫描;CI/CD 流水线加入 yq eval 'has("oidc-client-secret")' 检查。

  2. 强制启用 refresh_token rotation + binding
    → Keycloak:Realm Settings → Tokens → Refresh Token Max Reuse = 1;
    → Dex:issuer: {refresh_token_rotation: true}
    → 防止 refresh_token 被窃取后长期有效。

  3. RBAC 绑定必须基于 IdP group claim,而非 username

    yaml
    kind: 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

    → 避免因用户重命名导致权限漂移。

  4. 审计日志必须包含 user.extra 字段
    → 在 kube-apiserver --audit-policy-file 中启用 requestReceived 阶段,并记录 user.extra.groups

    yaml
    rules:
    - level: RequestResponse
      verbs: ["get", "list", "watch"]
      resources: [{"group": "", "resources": ["pods"]}]
      omitStages: []
  5. 为 ServiceAccount 设置独立 OIDC issuer(非用户 IdP)
    → 用户走 Keycloak device flow,vLLM inference Pod 使用 serviceaccount-issuer + --service-account-issuer-webhook-config-file,实现 human/machine credential 完全隔离。

延伸阅读

最后的技术判断:将 kubectl 视为 public client 并非向现实妥协,而是对分布式系统信任模型的诚实回归。当你的 GPU 节点运行着 vLLM 推理服务,当你的 Pod 跨越混合云边界调度——身份不应是集群的附加组件,而应是网络层之上的第一道数据平面策略。今天跳过的 OIDC device flow 配置,明天就会成为 SOC2 审计中那个无法关闭的高危项。Day-zero 不是起点,而是你承诺不再妥协的刻度线。