Kubernetes并不知道你是谁。

它没有用户数据库,也没有内置的登录系统或密码文件。当你运行`kubectl get pods`命令时,Kubernetes会接收一个HTTP请求,并会问这样一个问题:这个请求是谁发出的?我是否应该信任这个请求?其他所有事项——比如你被允许执行哪些操作、可以访问哪些命名空间、你的请求是否能够被成功处理——都取决于这个问题的回答结果。

对于那些刚开始使用Kubernetes的工程师来说,这一点会让他们感到非常困惑。他们原本期望看到的是一个包含用户信息及密码的数据库,但实际上却发现,Kubernetes采用了一种可扩展的身份验证机制,其中每一个身份验证组件都能以不同的方式来验证请求的合法性:

  • 客户端证书

  • 来自外部身份提供者的OIDC令牌

  • 云服务提供商提供的IAM令牌

  • 被注入到Pod中的服务账户令牌

这些身份验证方式可以同时被使用。

理解这一工作原理,是区分那些能够成功排查身份验证故障的工程师与那些只是简单地复制`kubeconfig`文件、然后寄希望于一切顺利的工程师的关键所在。

在本文中,你将从基础原理入手,了解Kubernetes的身份验证机制是如何运作的。你会了解到x509客户端证书的具体使用方式——以及为什么对于生产环境中的普通用户来说,这种认证方式并不合适。你还将学习如何使用Dex来配置OIDC身份验证功能,从而为你的Kubernetes集群提供基于浏览器的登录体验。最后,你还会看到AWS、GCP和Azure这些云服务提供商是如何基于相同的底层机制来实现身份验证功能的。

先决条件

  • 一个正在运行的Kind集群——新的集群也可以,或者直接使用现有的集群即可

  • 已经安装了`kubectl`和`helm`工具

  • 你的机器上必须安装了`openssl`软件(macOS和大多数Linux发行版中都已预装)

  • 需要对JWT有一定的了解(JWT是一种带有声明信息的签名JSON对象)——你不需要能够自己编写JWT,只需要能识别它们即可

所有的演示文件都位于相关的GitHub仓库中

目录

Kubernetes认证机制的工作原理

任何到达Kubernetes API服务器的请求——无论是通过kubectl、Pod、控制器还是CI管道发出的——都会携带某种形式的身份凭证。

API服务器会按顺序将这些凭证传递给一系列认证模块进行验证。第一个能够验证这些凭证的认证模块就会完成验证过程;如果没有任何一个模块能够成功验证,那么该请求就会被视为匿名请求。

认证模块链

Kubernetes同时支持多种认证机制。在同一集群中,可以同时启用客户端证书认证和OIDC认证,这种做法在生产环境中非常常见:集群管理员使用证书进行认证,而普通开发人员则使用OIDC进行认证。集群上具体使用哪些认证机制,是由传递给kube-apiserver进程的参数所决定的。

目前可用的认证机制包括x509客户端证书、承载令牌(静态令牌文件,在生产环境中较少使用)、引导令牌(在节点加入集群时使用)、服务账户令牌、OIDC令牌、认证代理以及Webhook令牌认证。一个集群并不一定需要使用所有这些认证机制,实际上大多数集群也并不会使用全部。不过了解这些认证机制的存在,对于诊断认证相关问题来说是非常有帮助的。

用户与服务账户的区别

Kubernetes对身份的管理方式存在一个重要的区别:服务账户是Kubernetes对象——它们存在于命名空间中,可以通过kubectl create serviceaccount命令创建,其令牌也由Kubernetes本身进行管理。每个Pod都是以服务账户的身份运行的,这些服务账户实际上就是为工作负载提供的机器身份标识。

而用户则完全不是Kubernetes对象。Kubernetes并没有kubectl create user这样的命令,也不会管理用户账户。相反,Kubernetes会依赖外部系统来验证用户的身份——比如证书颁发机构、OIDC提供者或云平台的IAM系统。Kubernetes只是负责验证这些外部系统的认证结果,并从中提取出用户名和所属组信息。

服务账户 用户
是否属于Kubernetes对象? 是——存在于命名空间中 否——由外部系统管理
创建方式: kubectl create serviceaccount 外部系统(证书颁发机构、IDP或云平台IAM系统)
使用对象: Pod和工作负载 人类用户和CI系统
令牌管理方: Kubernetes 外部系统
是否属于命名空间?

认证成功后会发生什么

认证过程实际上只回答了一个问题:“这个用户是谁?”一旦API服务器验证了用户的身份信息——包括用户名以及所属的组信息——它就会将请求传递给授权层。默认情况下,授权层会使用RBAC机制来检查用户的身份信息是否与相应的角色和集群角色绑定相匹配,从而确定该用户被允许执行哪些操作。

这就是为什么在Kubernetes中,认证和授权是两个不同的概念。一张有效的证书能让你通过“大门”进入系统,而你在系统内部能够执行哪些操作,则由RBAC来决定。一个已经完成认证但没有任何RBAC配置的用户虽然可以成功通过认证,但任何API调用都会被拒绝。

如果你想深入了解RBAC规则、角色以及它们之间的关联机制,可以参考这份关于如何保护Kubernetes集群:RBAC、Pod安全配置及运行时防护的手册。

如何使用x509客户端证书

x509客户端证书认证是Kubernetes中最古老也是最简单的认证方式。当你创建一个Kubernetes集群时,kubectl会自动使用这种认证机制——kindkubeadm生成的kubeconfig文件中会包含一张由集群的证书颁发机构签发的客户端证书。

证书如何与身份信息关联起来

当API服务器收到带有客户端证书的请求时,它会首先验证该证书是否来自其信任的证书颁发机构,然后从证书中读取“通用名称”和“组织名称”这两个字段来构建相应的身份信息。

通用名称字段会成为用户名,而组织名称字段则决定了用户所属的组。如果某个证书的通用名称jane组织名称engineering,那么该用户就会以jane为用户名、属于engineering组进行身份验证。如果你想为jane分配某些权限,就需要创建一个RoleBinding,将其关联到用户名jane或组engineering上。

system:masters组背后的机制也是如此。kind在创建集群并生成kubeconfig文件时,会生成一张组织名称system:masters的证书。Kubernetes内置了一个ClusterRoleBinding,它会将cluster-admin权限授予属于system:masters组的任何用户。因此,你的默认kubeconfig文件才会具有完整的管理员权限——这并不是什么神奇的现象,而只是因为该证书属于正确的组而已。

集群的证书颁发机构

每个Kubernetes集群都拥有一个根证书颁发机构,这个机构会生成一份私钥和一份自签名证书,API服务器会信任这份证书。任何由这个证书颁发机构签发的客户端证书都会被整个集群所认可。

证书颁发机构的证书和私钥通常会被存储在控制平面节点的/etc/kubernetes/pki/目录中,或者根据集群的创建方式,被保存在kube-system命名空间中的秘密配置项里。

在使用kind工具创建的集群中,你可以直接从控制平面容器中复制证书颁发机构的证书和私钥:

docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.crt ./ca.crt
docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.key ./ca.key

任何拥有CA密钥的人都可以为任何用户名和任何组颁发证书,包括system:masters组。因此,CA密钥成为Kubernetes集群中最为敏感的秘密,必须加以妥善保护。

基于证书的身份验证的局限性

客户端证书确实可以有效,但它们存在两个根本性问题,这使得它们并不适合在生产环境中供人类用户使用。

第一个问题是Kubernetes不会检查证书吊销列表。如果开发者的kubeconfig文件被盗,其中嵌入的证书在有效期结束之前仍然有效——而在大多数Kubernetes环境中,证书的有效期通常为一年。目前没有办法立即使这些证书失效,也无法“注销”这些证书。唯一的解决办法就是更换整个集群的CA密钥,但这会导致所有证书失效,包括其他合法用户的证书。

第二个问题是运营上的开销。需要生成证书、将它们分发给用户,并在证书过期前及时更新。目前没有自助服务功能。在一个由十名工程师组成的团队中,管理证书已经是一件麻烦事;而在一个由一百名工程师组成的团队中,这几乎成了一项全职工作。

对于生产环境中的人类用户来说,OIDC才是正确的解决方案:它使用受信任的身份提供者发行的短期令牌,具备中央式的吊销机制,并支持标准的基于浏览器的登录流程。而对于服务账户和自动化场景而言,证书也是可行的选择,因为在这些环境中,令牌的管理可以自动化处理,证书的更新也可以通过编程方式完成。

尽管如此,了解证书的相关知识仍然是必要的。你的kubeconfig文件就使用了证书,你的持续集成系统很可能也在使用证书。而当其他所有方法都失效时,基于证书的身份验证机制才是最后的保障手段。

演示1——创建并使用x509客户端证书

在本节中,你将生成一份由集群CA密钥签名的用户证书,将其关联到一个RBAC角色上,然后使用该证书以另一个用户的身份登录到集群中。

本指南仅用于本地开发和学习目的。为简化操作,这里演示了如何使用集群CA密钥手动签署证书并将密钥保存在磁盘上。

在生产环境中,你应该使用Kubernetes的CertificateSigningRequest API或cert-manager来颁发证书,确保证书具有短期有效期并自动更新,并将私钥存储在秘密管理工具(如HashiCorp Vault、AWS Secrets Manager)或硬件安全模块中——绝对不能将集群CA密钥分发给其他人。

步骤1:从kind控制平面复制CA证书和密钥

docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.crt ./ca.crt
docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.key ./ca.key

这样就会在当前目录下生成两个文件,分别是ca.crtca.key

步骤2:为新用户生成私钥和CSR

您正在为名为jane、属于engineering组的用户创建证书:

# 生成私钥
openssl genrsa -out jane.key 2048

# 生成证书签名请求
# CN = 用户名,O = 组别
openssl req -new \
  -key jane.key \
  -out jane.csr \
  -subj "/CN=jane/O=engineering"

步骤3:使用集群CA签署CSR

openssl x509 -req \
  -in jane.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out jane.crt \
  -days 365

预期输出:

证书请求自签名成功
主体=CN=jane, O=engineering

步骤4:检查证书

在使用该证书之前,请确认其中所包含的信息是否正确:

openssl x509 -in jane.crt -noout -subject -dates
主体=CN=jane, O=engineering
有效起始时间=2024年3月20日 10:00:00 GMT
有效结束时间=2025年3月20日 10:00:00 GMT

一年后,这份证书将失效,必须重新生成。无法延长其有效期——必须发布一份新的证书。

步骤5:为jane创建kubeconfig配置文件

# 从当前环境获取集群API服务器地址
APISERVER=$(kubectl config view --minify -o jsonpath '{.clusters[0].cluster.server}')

# 为jane创建kubeconfig配置文件
kubectl config set-cluster k8s-security \
  --server=$APISERVER \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --kubeconfig=jane.kubeconfig

kubectl config set-credentials jane \
  --client-certificate=jane.crt \
  --client-key=jane.key \
  --embed-certs=true \
  --kubeconfig=jane.kubeconfig

kubectl config set-context jane@k8s-security \
  --cluster=k8s-security \
  --user=jane \
  --kubeconfig=jane.kubeconfig

kubectl config use-context jane@k8s-security \
  --kubeconfig=jane.kubeconfig

步骤6:在启用RBAC之前测试认证功能

尝试使用jane的kubeconfig配置文件来列出Pod:

kubectl get pods -n staging --kubeconfig=jane.kubeconfig
步骤7:为jane授予RBAC权限

RBAC绑定会使用证书中CN字段所显示的用户名。如果你需要重新了解角色、集群角色以及角色绑定的工作原理,这份手册《如何保护Kubernetes集群:RBAC、Pod安全配置与运行时防护》会对RBAC模型进行全面讲解。目前来说,使用内置的`view`集群角色来创建一个简单的角色绑定就足够了:

# jane-rolebinding.yaml
apiVersion: rbacauthorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jane-reader
  namespace: staging
subjects:
  - kind: User
    name: jane          # 与证书中的CN字段匹配
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbacauthorization.k8s.io
kubectl apply -f jane-rolebinding.yaml
kubectl get pods -n staging --kubeconfig=jane.kubeconfig
在staging命名空间中未找到任何资源。

没有出现任何错误——现在jane可以使用`staging`命名空间来列出Pods,但她无法删除或创建Pods,也无法访问其他命名空间。证书使她获得了相应的访问权限,而RBAC则决定了她能执行哪些操作。

如何设置OIDC认证

OpenID Connect是在OAuth 2.0基础之上构建的身份认证机制。Kubernetes正是通过这一机制与Active Directory、Okta、Google Workspace、Keycloak等支持OIDC标准的企事业身份提供者进行集成的。要了解Kubernetes是如何利用这一技术的,就需要追踪用户浏览器发送的令牌,以及这些令牌是如何被API服务器处理的。

Kubernetes中OIDC认证流程的工作原理

当开发者配置了OIDC认证后运行`kubectl get pods`命令时,以下过程会依次发生:

  1. `kubectl`会检查kubeconfig文件中当前的凭证是否为有效且未过期的OIDC令牌。

  2. 如果凭证无效,`kubectl`会启动名为`kubelogin`的插件,该插件会打开一个浏览器窗口。

  3. 浏览器会跳转到相应的OIDC提供者页面(例如Dex、Okta或企业自己的身份认证系统)。

  4. 用户需要使用自己的企业凭证进行登录。

  5. OIDC提供者会生成一个经过签名的JWT令牌,并将其返回给`kubelogin`。

  6. `kubelogin`会将这个令牌缓存到本地(路径为`~/.kube/cache/oidc-login/`),然后再将其传递给`kubectl`。

  7. `kubectl`会以`Bearer`头字段的形式将令牌发送给API服务器。

  8. API服务器会从提供者的JWKS端点获取其公钥,然后验证令牌的签名是否有效。

  9. 如果签名有效,API服务器就会从令牌中提取用户名和所属组的信息。

  10. 之后,RBAC机制会根据这些信息来控制用户的访问权限。

    1. Kubernetes API服务器并不会在每次请求时都与OIDC提供者进行交互。它只会定期获取提供者的公钥,以便在本地验证令牌签名。这种设计使得OIDC认证具有无状态且可扩展的特性。

    API服务器配置

    为了使OIDC正常工作,API服务器需要知道去哪里查找身份提供者,以及如何解读该提供者颁发的令牌。

    在Kubernetes v1.30及更高版本中,这些配置是通过通过`--authentication-config`标志传递的`AuthenticationConfiguration`文件来设置的。(在早期版本中,使用的是单独的`--oidc-*`标志,但这些标志在v1.35版本中被移除了。)

    `AuthenticationConfiguration`文件会在`jwt`键下定义OIDC提供者的相关配置:

    >

    字段 作用 示例
    issuer.url OIDC提供者的基础URL——必须与令牌中的`iss`字段相匹配 https://dex.example.com
    issuer.audiences 该令牌是为哪些客户端发行的——必须与令牌中的`aud`字段相匹配 ["kubernetes"]
    issuer.certificateAuthority 在联系OIDC提供者时需要信任的CA证书(以PEM格式提供) -----BEGIN CERTIFICATE-----...
    claimMappings.username.claim 哪个JWT字段应被用作Kubernetes中的用户名 email
    claimMappings.groups.claim 哪个JWT字段应被用作Kubernetes中的组名列表 groups
    claimMappings.*.prefix 添加到字段值前的前缀——若不需要前缀,则设置为`""` ""

    在Kubernetes集群中,`--authentication-config`标志需要在创建集群之前就设置好,而不是之后。接下来我们会通过演示来说明这一点。

    Kubernetes使用的JWT字段

    JWT是一种经过签名的JSON对象,它由头部、有效载荷和签名三部分组成。有效载荷是一组键值对,这些键值对用于描述令牌的相关信息。Kubernetes会从有效载荷中读取特定的字段来构建用户的身份信息。

    必需的字段包括`iss`(发行者的URL,必须与`AuthenticationConfiguration`文件中的`issuer.url`字段相匹配)、`sub`(主题,即用户的唯一标识符)以及`aud`(受众群体,必须与` issuer.audiences`列表相匹配)。此外,`exp`字段(有效期)也是必需的,因为API服务器会拒绝过期的令牌。

    最有用的可选字段是`groups`(或者通过`claimMappings.groups.claim`配置的其他字段)。当这个字段存在时,Kubernetes可以将OIDC中的组成员信息直接映射到RBAC组绑定中。例如,如果用户在身份提供者中被归入`platform-engineers`组,那么他在Kubernetes中就会自动获得与该组相关联的RBAC权限——无需进行任何手动的管理操作。

    kubelogin的工作原理

    kubelogin(也以kubectl oidc-login的名称进行分发)是一种用于kubectl的工具。与其在kubeconfig文件中嵌入静态证书或令牌,不如配置这样一个工具:当kubectl需要令牌时,这个工具会运行相应的辅助程序来获取令牌。

    当kubelogin被调用时,它会首先检查本地的令牌缓存。如果缓存的令牌仍然有效,就会立即将其返回给kubectl;如果令牌已经过期,它就会启动OIDC认证流程:打开浏览器,跳转到身份提供者网站,在登录后获取令牌,并将其保存到本地缓存中,然后再将其返回给kubectl。整个认证流程在触发后大约需要五秒钟才能完成。

    这意味着这些令牌的有效期很短(通常为一小时),并且会自动更新。如果开发者的机器遭到攻击,令牌也会自动失效。因此,系统中并不会存在长期有效的凭证文件。

    演示2——使用Dex和kubelogin配置OIDC登录

    在本节中,我们将把Dex部署为自托管的OIDC提供者,配置Kind集群以使其能够信任该提供者,并通过浏览器进行登录。Dex是一个非常适合用来演示这一过程的工具,因为它直接运行在集群内部,不需要使用云账户或外部服务。

    本指南仅用于本地开发和学习目的。为简化操作,本文中使用了自签名证书、静态密码以及存储在磁盘上的证书。

    在实际生产环境中,应使用受管理的身份提供者(如Azure Entra ID、Google Workspace或Okta),通过cert-manager自动化证书的管理流程,并将敏感信息存储在secret manager中(例如HashiCorp Vault或AWS Secrets Manager),或者通过CSI驱动程序来传递这些信息。切勿将证书以本地文件的形式保存。

    步骤1:创建一个支持OIDC认证的Kind集群

    由于API服务器在开始接收请求之前需要知道应该信任哪个身份提供者,因此必须在创建Kind集群时配置OIDC认证相关设置。

    注意:从Kubernetes v1.30版本开始,--oidc-*这类API服务器配置参数已被弃用,取而代之的是结构化的AuthenticationConfiguration API(通过参数进行配置)。在v1.35版本之后,这些旧参数被完全移除。本指南采用新的配置方式。

    nip.io是一种通配符DNS服务——dex.127.0.0.1.nip.io解析后会得到127.0.0.1这个地址。这样我们就可以使用真实的域名来进行TLS通信,而无需修改/etc/hosts文件。

    首先,为Dex生成一个自签名的CA证书以及TLS证书:

    
    # 为Dex生成CA证书
    openssl req -x509 -newkey rsa:4096 -keyout dex-ca.key \
      -out dex-ca.crt -days 365 -nodes \
      -subj "/CN=dex-ca"
    
    # 为Dex生成由该CA证书签名的TLS证书
    openssl req -newkey rsa:2048 -keyout dex.key \
      -out dex.csr -nodes \
      -subj "/CN=dex.127.0.0.1.nip.io"
    
     openssl x509 -req -in dex.csr \
      -CA dex-ca.crt -CAkey dex-ca.key \
      -CAcreateserial -out dex.crt -days 365 \
      -extfile ""
    

    接下来,生成 AuthenticationConfiguration 文件。该文件用于告知 API 服务器如何验证 JWTs:应信任哪个发行者(url),预期哪些受众会使用这些 JWTs(audiences),以及哪些 JWT 中的声明与 Kubernetes 的用户名和组相对应(claimMappings)。CA 证书被直接嵌入到配置文件中,因此 API 服务器在获取签名密钥时可以验证 Dex 的 TLS 证书:

    cat > auth-config.yaml <<EOF
    apiVersion: apiserver.config.k8s.io/v1beta1
    kind: AuthenticationConfiguration
    jwt:
      - issuer:
          url: https://dex.127.0.0.1.nip.io:32000
          audiences:
            - kubernetes
          certificateAuthority: |
    $(sed 's/^/        /' dex-ca.crt)
        claimMappings:
          username:
            claim: email
            prefix: ""
          groups:
            claim: groups
            prefix: ""
    EOF
    

    配置文件 kind-oidc.yaml 使用 extraPortMappings 将 Dex 的端口暴露给用户的浏览器,使用 extraMounts 将文件复制到 Kind 节点中,并通过 kubeadmConfigPatch 选项将 --authentication-config 参数传递给 API 服务器:

    # kind-oidc.yaml
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
      - role: control-plane
        extraPortMappings:
          # 将 Docker 容器中的 32000 端口转发到本地主机,
          # 这样用户的浏览器就能访问 Dex 的登录页面
          - containerPort: 32000
            hostPort: 32000
            protocol: TCP
        extraMounts:
          # 将用户计算机上的文件复制到 Kind 节点的文件系统中
          - hostPath: ./dex-ca.crt
            containerPath: /etc/ca-certificates/dex-ca.crt
            readOnly: true
          - hostPath: ./auth-config.yaml
            containerPath: /etc/kubernetes/auth-config.yaml
            readOnly: true
        kubeadmConfigPatches:
          # 修改 API 服务器的配置以启用 OIDC 认证功能
          - |
            kind: ClusterConfiguration
            apiServer:
              extraArgs:
                # 告知 API 服务器加载我们的 AuthenticationConfiguration 配置文件
                authentication-config: /etc/kubernetes/auth-config.yaml
              extraVolumes:
                # 将相关文件挂载到 API 服务器的 Pod 中
                - name: dex-ca
                  hostPath: /etc/ca-certificates/dex-ca.crt
                  mountPath: /etc/ca-certificates/dex-ca.crt
                  readOnly: true
                  pathType: File
                - name: auth-config
                  hostPath: /etc/kubernetes/auth-config.yaml
                  mountPath: /etc/kubernetes/auth-config.yaml
                  readOnly: true
                  pathType: File
    

    现在创建集群:

    kind create cluster --name k8s-auth --config kind-oidc.yaml
    

    步骤 2:部署 Dex

    Dex 是一个符合 OIDC 标准的身份提供者,它充当 Kubernetes 与上游身份源(如 LDAP、SAML、GitHub 等)之间的桥梁。在这个演示中,Dex 在集群内部运行,并使用静态密码数据库进行认证——用户可以使用两个预设好的用户名和密码进行登录。

    API服务器并不会在每个请求时都直接与Dex进行通信。它只需要Dex的CA证书(你已经在AuthenticationConfiguration中配置了该证书)来验证Dex颁发的令牌上的JWT签名即可。

    整个部署过程包含四个部分:一个用于存储Dex配置信息的ConfigMap、一个用于运行Dex的应用程序、一个NodePort服务以便在32000端口上暴露Dex服务接口,以及一些RBAC资源,这些资源使得Dex能够使用Kubernetes的CRD来存储状态信息。

    首先,需要创建一个命名空间,并将Dex的TLS证书作为Kubernetes Secret保存下来。Dex依赖这个证书才能提供HTTPS服务;如果没有这个证书,你的浏览器和API服务器将无法建立连接:

    kubectl create namespace dex
    
    kubectl create secret tls dex-tls \
      --cert=dex.crt \
      --key=dex.key \
      -n dex
    

    将以下配置文件保存为dex-config.yaml。这个文件为Dex配置了一个静态密码认证机制,其中包含了两名用于演示目的的固定用户账户:

    # dex-config.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: dex-config
      namespace: dex
    data:
      config.yaml: |
        # 发行者地址必须与你在AuthenticationConfiguration中配置的URL完全一致
        issuer: https://dex.127.0.0.1.nip.io:32000
    
        # Dex会使用Kubernetes的CRD来存储刷新令牌和认证码
        storage:
          type: kubernetes
          config:
            inCluster: true
    
        # Dex的HTTPS服务器配置
        web:
          https: 0.0.0.0:5556
          tlsCert: /etc/dex/tls/tls.crt
          tlsKey: /etc/dex/tls/tls.key
    
        # 规定哪些应用程序可以请求令牌
        staticClients:
          - id: kubernetes
            redirectURIs:
              - http://localhost:8000     # kubelogin会在这里接收回调请求
            name: Kubernetes
            secret: kubernetes-secret     # 这是kubelogin与Dex之间共享的密码信息
    
        # 为演示目的创建了两名用户账户,密码均为"password"(已使用bcrypt进行加密)
        enablePasswordDB: true
        staticPasswords:
          - email: "jane@example.com"
            hash: "\(2a\)10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
            username: "jane"
            userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
          - email: "admin@example.com"
            hash: "\(2a\)10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
            username: "admin"
            userID: "a8b53e13-7e8c-4f7b-9a33-6c2f4d8c6a1b"
            groups:
              - platform-engineers
    

    将以下配置文件保存为dex-deployment.yaml。这个文件会创建Dex运行所需的Deployment、Service、ServiceAccount以及RBAC资源。


    # dex-deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: dex
    namespace: dex
    spec:
    replicas: 1
    selector:
    matchLabels:
    app: dex
    template:
    metadata:
    labels:
    app: dex
    spec:
    serviceAccountName: dex
    containers:
    - name: dex
    # v2.45.0及更高版本要求使用此配置——早期版本不会在令牌中包含staticPasswords中的组信息
    image: ghcr.io/dexidp/dex:v2.45.0
    command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
    ports:
    - name: https
    containerPort: 5556
    volumeMounts:
    - name: config
    mountPath: /etc/dex/cfg
    - name: tls
    mountPath: /etc/dex/tls
    volumes:
    - name: config
    configMap:
    name: dex-config
    - name: tls
    secret:
    secretName: dex-tls
    ---
    # NodePort Service — 在Kind节点的32000端口上提供Dex服务。
    # 结合额外的portMappings配置,就可以通过浏览器访问Dex服务了
    apiVersion: v1
    kind: Service
    metadata:
    name: dex
    namespace: dex
    spec:
    type: NodePort
    ports:
    - name: https
    port: 5556
    targetPort: 5556
    nodePort: 32000
    selector:
    app: dex
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
    name: dex
    namespace: dex
    ---
    apiVersion: rbacauthorization.k8s.io/v1
    kind: ClusterRole
    metadata:
    name: dex
    rules:
    - apiGroups: ["dex.coreos.com"]
    resources: ["*"]
    verbs: ["*"]
    - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["create"]
    ---
    apiVersion: rbacauthorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
    name: dex
    subjects:
    - kind: ServiceAccount
    name: dex
    namespace: dex
    roleRef:
    kind: ClusterRole
    name: dex
    apiGroup: rbac.authorization.k8s.io

    kubectl apply -f dex-config.yaml
    kubectl apply -f dex-deployment.yaml
    kubectl rollout status deployment/dex -n dex
    

    步骤 3:安装 kubelogin

    # macOS
    brew install int128/kubelogin/kubelogin
    
    # Linux
    curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
    unzip -j kubelogin_linux_amd64.zip kubelogin -d /tmp
    sudo mv /tmp/kubelogin /usr/local/bin/kubectl-oidc_login
    rm kubelogin_linux_amd64.zip
    

    确认它已经安装完成:

    kubectl oidc-login --version
    

    步骤 4:为 OIDC 配置 kubeconfig 条目

    此操作会在您的 kubeconfig 中创建一个新的用户和上下文。与使用客户端证书不同(默认情况下是 admin 用户),这种方法会告诉 kubectl 使用 kubelogin 从 Dex 获取令牌。

    --oidc-extra-scope 参数非常重要:如果没有 emailgroups 这些参数,Dex 就不会在 JWT 中包含这些信息,因此 API 服务器也无法识别您的身份或您所属的组。

    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://dex.127.0.0.1.nip.io:32000 \
      --exec-arg=--oidc-client-id=kubernetes \
      --exec-arg=--oidc-client-secret=kubernetes-secret \
      --exec-arg=--oidc-extra-scope=email \
      --exec-arg=--oidc-extra-scope=groups \
      --exec-arg=--certificate-authority=$(pwd)/dex-ca.crt
    
    kubectl config set-context oidc@k8s-auth \
      --cluster=kind-k8s-auth \
      --user=oidc-user
    
    kubectl config use-context oidc@k8s-auth
    

    步骤 5:触发登录流程

    Jane 目前还没有 RBAC 权限,因此首先需要为她在 admin 上下文中授予读取权限:

    kubectl --context kind-k8s-auth create clusterrolebinding jane-view \
      --clusterrole=view --user=jane@example.com
    

    现在切换到 OIDC 上下文并触发登录流程:

    kubectl get pods -n default
    

    您的浏览器会打开并跳转到 Dex 的登录页面。使用用户名 jane@example.com 和密码 password 进行登录。

    dexidp 登录界面
    dexidp 授予访问权限

    登录成功后,终端会显示如下信息:

    在 default 名称空间中未找到任何资源。

    基于浏览器的认证流程成功完成了。kubectl从Dex获取了令牌,然后将其发送给API服务器。API服务器使用了AuthenticationConfiguration中指定的CA证书来验证JWT签名,从中提取出了jane@example.com这一信息,将其与RBAC配置中的权限规则进行了匹配,最终批准了该请求。

    如果没有clusterrolebinding配置,你会看到“服务器返回错误:禁止访问”的提示——虽然认证过程成功了(API服务器知道你是谁),但由于没有相应的权限,授权请求仍然会失败。这就是401 Unauthorized和403 Forbidden两种错误状态的区别所在。

    步骤6:检查JWT令牌

    JWT(JSON Web Token)是一种经过签名的JSON数据结构,其中包含了关于用户的各种信息。kubelogin会将令牌缓存在~/.kube/cache/oidc-login/目录下,因此你每次执行kubectl命令时都不需要重新登录。

    以下是查看缓存文件的命令:

    ls ~/.kube/cache/oidc-login/
    

    可以直接从缓存中解码JWT令牌的内容:

    cat ~/.kube/cache/oidc-login/$(ls ~/.kube/cache/oidc-login/ | grep -v lock | head -1) | \
      python3 -c "
    import json, sys, base64
    token = json.load(sys.stdin)['id_token'].split('.')[1]
    token += '=' * (4 - len(token) % 4)
    print(json.dumps(json.loads(base64.urlsafe_b64decode(token)), indent=2))
    "
    

    解码后的令牌内容大致如下:

    {
      "iss": "https://dex.127.0.0.1.nip.io:32000",
      "sub": "CiQwOGE4Njg0Yi1kYjg4LTRiNzMtOTBhOS0zY2QxNjYxZjU0NjYSBWxvY2Fs",
      "aud": "kubernetes",
      "exp": 1775307910,
      "iat": 1775221510,
      "email": "jane@example.com",
      "email_verified": true
    }
    

    在这里,email字段表示用户的Kubernetes用户名,因为AuthenticationConfigurationusername.claim: email这一对应关系进行了配置。aud字段与预先设定的audiences匹配,而iss字段则验证了令牌的发行者地址。因此,API服务器在处理请求时无需每次都联系Dex,只需使用CA证书来验证JWT签名即可。

    步骤7:将OIDC组与RBAC权限关联起来

    用户admin@example.com在Dex配置中属于platform-engineers组。因此,我们可以直接为这个组配置相应的权限,而不是为每个用户单独创建RBAC规则。任何其JWT令牌中包含platform-engineers组的用户,都会自动获得这些权限:

    # platform-engineers-binding.yaml
    apiVersion: rbacauthorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: platform-engineers-admin
    subjects:
      - kind: Group
        name: platform-engineers     # 这个组名称与JWT令牌中的信息匹配
        apiGroup: rbac.authorization.k8s.io
    roleRef:
      kind: ClusterRole
      name: cluster-admin
      apiGroup: rbacauthorization.k8s.io
    

    您当前是通过 OIDC 上下文以 jane@example.com 的身份登录的,但 jane 只拥有 查看 权限,因此她无法创建适用于整个集群的 RBAC 规则。请切换回管理员上下文来执行后续操作:

    kubectl config use-context kind-k8s-auth
    kubectl apply -f platform-engineers-binding.yaml
    kubectl config use-context oidc@k8s-auth
    

    现在,请清除缓存的令牌以退出 jane 的会话,然后以 admin@example.com 的身份重新登录:

    # 清除缓存令牌——这就是使用 kubelogin “登出”的方法
    rm -rf ~/.kube/cache/oidc-login/
    
    # 这将会再次打开浏览器,让您进行新的登录操作
    kubectl get pods -n default
    

    使用密码 passwordadmin@example.com 的身份登录。这次,JWT 令牌中会包含 "groups": ["platform-engineers"] 这一字段,这与您刚刚创建的 ClusterRoleBinding 规则相匹配。管理员用户将获得对整个集群的完全访问权限——而无需在 kubeconfig 文件中手动添加其信息。

    您可以通过解码新的令牌来验证这一设置:其中确实会包含 groups 这一字段:

    {
      "email": "admin@example.com",
      "groups": ["platform-engineers"]
    }
    

    这就是 OIDC 组权限机制的真正优势:您可以在自己的身份提供者系统中管理成员资格,而 Kubernetes 的权限设置会自动随之更新。只要在 Dex(或任何上游身份提供者系统中)将某人添加到 platform-engineers 组中,他们在下次登录时就会获得集群管理员权限——完全不需要修改 kubeconfig 文件或 RBAC 规则。

    云提供商认证

    AWS、GCP 和 Azure 都为 Kubernetes 集群提供了与各自 IAM 系统集成的原生认证机制。

    这些机制在 API 接口上可能存在差异,但它们都基于相同的底层技术:OIDC 令牌处理机制。了解了 Dex 的工作原理后,就会发现这些认证方式其实都属于同一类解决方案。

    AWS EKS

    EKS 使用 aws-iam-authenticator 将 AWS IAM 身份转换为 Kubernetes 身份。当您在 EKS 集群上运行 kubectl 命令时,AWS CLI 会使用您的 IAM 凭据生成一个有效期较短的令牌。API 服务器会将这个令牌传递给 aws-iam-authenticator 的 webhook,该组件会通过 AWS STS 进行验证,并返回相应的用户名及所属组别。

    用户访问权限是通过 kube-system 中的 aws-auth ConfigMap 来控制的。这个 ConfigMap 将 IAM 角色 ARN 和 IAM 用户 ARN 映射到 Kubernetes 的用户名及组别上。一个典型的配置项如下所示:

    # 在 kube-system/aws-auth ConfigMap 中
    mapRoles:
      - rolearn: arn:aws:iam::123456789:role/platform-engineers
        username: platform-engineer:{{SessionName}}
        groups:
          - platform-engineers
    

    AWS 正在从 aws-auth ConfigMap 过渡到新的 Access Entries API,新的系统会通过 EKS API 来管理这种映射关系,而不再使用 ConfigMap。不过,底层的认证机制仍然是相同的。

    Google GKE

    GKE通过两种不同的机制与Google Cloud IAM集成,具体取决于你是以人类用户的身份进行认证,还是以工作负载的身份进行认证。

    对于人类用户来说,GKE接受标准的Google OAuth2令牌。运行gcloud container clusters get-credentials命令后,系统会生成一个kubeconfig文件,该文件会将gcloud CLI作为凭证插件使用,并会自动从你的Google账户中生成短期有效的令牌。

    对于在Pod层面实现身份认证的需求——也就是让Pod能够承担特定的Google Cloud IAM角色——GKE会使用Workload Identity机制。你需要为Kubernetes服务账户添加相应的注释,将其与Google Service Account关联起来;这样,以该服务账户运行的Pod就可以利用Google Service Account的权限来调用Google Cloud API了:

    # 将Kubernetes服务账户与Google Service Account关联
    kubectl annotate serviceaccount my-app \
      --namespace production \
      iam.gke.io/gcp-service-account=my-app@my-project.iam.gserviceaccount.com
    

    Azure AKS

    AKS与Azure Active Directory进行了集成。当启用这种集成功能后,kubectl会通过Azure CLI代表用户请求获取Azure AD令牌,而AKS API服务器则会对这些令牌进行验证,以确保其合法性。

    在Pod层面实现身份认证时,AKS同样使用Azure Workload Identity机制。这种机制与GKE的Workload Identity遵循相同的OIDC联盟模式:Kubernetes服务账户会被添加相应的注释,以便将其与Azure Service Account关联起来;这样,以该服务账户运行的Pod就可以无需存储任何凭证即可请求获取Azure AD令牌了:

    # 为Kubernetes服务账户添加Azure Managed Identity客户端ID的注释
    kubectl annotate serviceaccount my-app \
      --namespace production \
      azure.workload.identity/client-id=<MANAGED_IDENTITY_CLIENT_ID>
    

    实际上,这三种云服务提供商所采用的底层机制都是相同的:由云服务提供商颁发可信的OIDC令牌,Kubernetes API服务器会对这些令牌进行验证,然后通过相应的绑定方式(如aws-auth ConfigMap、GKE Workload Identity绑定机制或AKS联盟身份认证机制)将这些令牌与具体的身份信息关联起来。本文中关于OIDC的部分,实际上为所有这些认证机制提供了概念上的基础。

    Webhook令牌认证

    了解Webhook令牌认证机制是很重要的,因为即使在你自己从未配置过这种认证方式的情况下,它也经常出现在各种常见的Kubernetes部署方案中。

    当收到一个没有任何其他认证机制能够识别的bearer令牌时,Kubernetes可以将该令牌发送到外部HTTP端点进行验证。该端点会返回一个响应,说明这个令牌实际上属于谁。

    在aws-iam-authenticator被集成到API服务器之前,EKS就是通过这种方式来进行身份认证的。同样,在节点加入集群的过程中,也会使用类似的机制:系统会生成一个令牌,将其嵌入到kubeadm join命令中,然后当新节点首次与API服务器建立连接时,该令牌会由bootstrap webhook进行验证。

    对于大多数集群来说,Webhook认证机制通常是已经处于运行状态的,而不是你需要手动配置的部分。你需要了解的主要是:这种认证机制确实存在,而且在日志或配置文件中会以什么样的形式出现。

    清理操作

    要删除本文中介绍的所有内容,请执行以下命令:

    
    # 删除OIDC演示集群
    kind delete cluster --name k8s-auth
    
    # 删除生成的证书文件
    rm -f ca.crt ca.key jane.key jane.csr jane.crt jane.kubeconfig
    rm -f dex-ca.crt dex-ca.key dex.crt dex.key dex.csr dex-ca.srl auth-config.yaml
    
    # 清除kubelogin令牌缓存
    rm -rf ~/.kube/cache/oidc-login/
    

    总结

    Kubernetes的认证机制并不是单一的,而是一系列可插拔的策略组合,每种策略都适用于不同的使用场景。在本文中,你了解了其中最重要的几种认证方式。

    x509客户端证书是Kubernetes默认使用的认证方式。证书中的CN字段对应用户名,O字段对应所属组,而集群的CA证书则充当信任锚点。你为新用户创建了证书,并将其与RBAC权限控制机制结合使用,从而了解了认证与授权之间的交互关系:认证负责让用户获得访问权限,而RBAC则决定用户可以执行哪些操作。

    但你也注意到了一个根本性的限制:Kubernetes不会检查证书的撤销列表,因此一旦证书被篡改,它仍然会在有效期内保持有效性。这种特性使得证书不太适合在生产环境中供人类用户使用。

    OIDC才是适用于生产环境的认证方案。令牌具有较短的生命周期,由受信任的身份提供者颁发,并且可以通过JWT断言直接与Kubernetes的组权限进行关联。你将Dex部署为自托管的OIDC提供者,配置了API服务器以信任它,并设置了kubelogin来实现基于浏览器的认证功能。

    你还解码了一个JWT令牌,从而了解了API服务器是从其中读取了哪些信息;同时,你还将OIDC中的组权限断言映射到了Kubernetes的ClusterRoleBinding对象上。

    各种云服务提供商(如EKS、GKE、AKS)所使用的认证机制也都基于OIDC框架,只不过会添加针对各自平台的特定封装层。理解Dex的工作原理后,这些云服务的认证机制就能立刻被你搞明白了。

    本文中提到的所有YAML配置文件、证书以及相关设置文件,都保存在这个GitHub仓库中

Comments are closed.