Kubernetes客户端认证(二)—— 基于ServiceAccount的JWTToken认证

发布时间 2023-04-11 19:59:12作者: 人艰不拆_zmc

1、概述

  在 Kubernetes 官方手册中给出了 “用户” 的概念,Kubernetes 集群中存在的用户包括 “普通用户” 与 “ServiceAccount”, 但是 Kubernetes 没有普通用户的管理方式,通常只是将使用集群根证书签署的有效证书的用户都被视为合法用户。

  那么对于使得 Kubernetes 集群有一个真正的用户系统,就可以根据上面给出的概念将 Kubernetes 用户分为“内部用户”与“外部用户”。如何理解内部与外部用户呢?实际上就是由 Kubernetes 管理的用户,即在 kubernetes 定义用户的数据模型这种为 “内部用户” ,正如 ServiceAccount;反之,非 Kubernetes 托管的用户则为 “外部用户”, 这种概念也更好的对 kubernetes 用户的阐述。

  对于外部用户来说,实际上 Kubernetes 给出了多种用户概念,例如:

  • 拥有 kubernetes 集群证书的用户
  • 拥有 Kubernetes 集群 token 的用户(--token-auth-file指定的静态 token)
  • 用户来自外部用户系统,例如 OpenID,LDAP,QQ connect, google identity platform 等

      本文不过多介绍Kubernetes外部用户认证,主要讲解Kubernetes内部用户ServiceAccount的认证方式,即大部分Pod默认的认证方式(Pod和APIServer之间如果没有配置基于CA根证书签名的双向数字证书方式进行认证的话,则默认通过Token方式进行认证)。

注意:在之前博文中讲解过拥有kubernetes集群证书的用户的认证方式,详情见《Kubernetes客户端认证——基于CA证书的双向认证方式》。

2、基于ServiceAccount的JWTToken认证

2.1 ServiceAccount定义

ServiceAccount(服务帐户)与Namespace绑定,关联一套凭证,Pod创建时挂载Token,从而允许与API Server之间调用。ServiceAccount同样是Kubernetes中的资源,与Pod、ConfigMap类似,且作用于独立的命名空间,也就是ServiceAccount是属于命名空间级别的,创建命名空间时会自动创建一个名为default的ServiceAccount。

使用下面命令可以查看ServiceAccount。

[root@node1 ~]# kubectl get sa
NAME                SECRETS   AGE
default             1         356d

同时Kubernetes还会为ServiceAccount自动创建一个Token,使用下面命令可以查看到。

[root@node1 ~]# kubectl describe sa default 
Name:                default
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   default-token-p4l9w
Tokens:              default-token-p4l9w
Events:              <none>

在Pod的定义文件中,可以用指定帐户名称的方式将一个ServiceAccount赋值给一个Pod,如果不指定就会使用默认的ServiceAccount。当API Server接收到一个带有认证Token的请求时,API Server会用这个Token来验证发送请求的客户端所关联的ServiceAccount是否允许执行请求的操作。

注意:

  • 1.21以前版本的集群中,Pod中获取Token的形式是通过挂载ServiceAccount的Secret来获取Token,这种方式获得的Token是永久的。该方式在1.21及以上的版本中不再推荐使用,并且根据社区版本迭代策略,在1.25及以上版本的集群中,ServiceAccount将不会自动创建对应的Secret。

    1.21及以上版本的集群中,直接使用TokenRequest API获得Token,并使用投射卷(Projected Volume)挂载到Pod中。使用这种方法获得的Token具有固定的生命周期,并且当挂载的Pod被删除时这些Token将自动失效。详情请参见Token安全性提升说明

  • 如果您在业务中需要一个永不过期的Token,您也可以选择手动管理ServiceAccount的Secret。尽管存在手动创建永久ServiceAccount Token的机制,但还是推荐使用TokenRequest的方式使用短期的Token,以提高安全性。

2.2 创建ServiceAccount

使用如下命令就可以创建ServiceAccount:

[root@node1 ~]# kubectl create serviceaccount sa-example
serviceaccount/sa-example created
[root@node1 ~]# kubectl get sa
NAME                SECRETS   AGE
default             1         356d
sa-example          1         6s

可以看到已经创建了与ServiceAccount相关联的Token。

[root@node1 ~]# kubectl describe sa sa-example
Name:                sa-example
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   sa-example-token-42nbt
Tokens:              sa-example-token-42nbt
Events:              <none>

查看Secret的内容,可以发现ca.crt、namespace和token三个数据。

[root@node1 ~]# kubectl describe secret sa-example-token-42nbt
Name:         sa-example-token-42nbt
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: sa-example
              kubernetes.io/service-account.uid: 5c67e9c7-0b12-44ae-a97e-f2ee4cd061b0

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1066 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6InpKM2o1WXg2b1pwQ0lyTjNWVU4wUjUzMTZuamt0LVZEQlNqbG5HLWNxdUUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNhLWV4YW1wbGUtdG9rZW4tNDJuYnQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoic2EtZXhhbXBsZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjVjNjdlOWM3LTBiMTItNDRhZS1hOTdlLWYyZWU0Y2QwNjFiMCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnNhLWV4YW1wbGUifQ.FlL0EAeNYOqNpCmBG5QU0qAbGIVHjTP3hVMOL8nmiawmZsitSDupKrdbpIjFgS3VOTR8GZfZCnCnVI7fE0ZkZzt6kW7ILTRIiNbmedScI4w_FiSbGI-MX48HJAIgF0hrdJ3_Rc30-Or-fMxlligcO08OpSQrBT20J4LR6NM4B-r_s83b7Rwm7F95GWc5rmYZje1uRJfRuzqBwV4PLT5Ph7u_XDn8WS1XXWagq5qioySFV1Xg9Lb9nHXVNmTL2g0E2_RDqUFj-tsns8Xj3B9Yvm4CH5uS6PbV0UsdFlm-mCOF4vinDHSP4mH6CSBWyzRnl9vF7hbfNzW96rIKKSJ9CA 

注意:只有在1.23及之前版本的集群中,ServiceAccount才会自动创建Secret。

2.3 在Pod中使用ServiceAccount

Pod中使用ServiceAccount非常方便,只需要指定ServiceAccount的名称即可。

apiVersion: v1
kind: Pod
metadata:
  name: sa-example
spec:  
  serviceAccountName: sa-example
  containers:
  - image: nginx:alpine             
    name: container-0               
    resources:                      
      limits:
        cpu: 100m
        memory: 200Mi
      requests:
        cpu: 50m
        memory: 100Mi

创建并查看这个Pod,可以看到Pod挂载了sa-example-token-c7bqx,即Pod使用这个Token来做认证。

[root@node1]# kubectl apply -f test-sa.yaml 
pod/sa-example created

[root@node1 test]# kubectl get pods
NAME                                              READY   STATUS    RESTARTS   AGE
sa-example                                        1/1     Running   0          32s

[root@node1]#  kubectl describe pod sa-example
...
Containers:
  sa-example:
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from sa-example-token-42nbt (ro)

注意: 通过这个方式,您可以了解Pod的认证机制,但在实际使用中,出于安全性考虑,1.21及以上版本的集群中Pod中默认挂载的Token是临时的。

进入Pod内部,还可以看到对应的文件,如下所示:

[root@node1 ~]# kubectl exec -it sa-example /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # cd /run/secrets/kubernetes.io/serviceaccount
/run/secrets/kubernetes.io/serviceaccount # ls
ca.crt     namespace  token

如上,在容器应用中,就可以使用ca.crt和Token来访问APIServer。

2.4 基于token访问APIServer

下面来验证一下认证是否能生效。在Kubernetes集群中,默认为API Server创建了一个名为kubernetes的Service,通过这个Service可以访问API Server。

[root@node1 ~]# kubectl get svc
NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                
kubernetes                   ClusterIP   10.233.0.1      <none>        443/TCP             356d

进入Pod,使用curl命令直接访问会得到如下返回信息,表示并没有权限。

[root@node1 ~]# kubectl exec -it sa-example /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # curl https://kubernetes
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
/ # 

使用ca.crt和Token做认证,先将ca.crt放到CURL_CA_BUNDLE这个环境变量中,curl命令使用CURL_CA_BUNDLE指定证书;再将Token的内容放到TOKEN中,然后带上TOKEN访问API Server。 

/ # export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
/ # TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
/ # curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/default/pods
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:default:sa-example\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403
}/ # 

可以看到,已经能够通过认证了,但是API Server返回的是cannot get path \"/\"",表示没有权限访问,这说明还需要得到授权后才能访问,接下来为default命名空间下的ServiceAccount  sa-example关联Role使其能够获取Pod资源。

首先创建Role,Role的定义非常简单,指定namespace,然后就是rules规则。如下面示例中的规则就是允许对default命名空间下的Pod进行GET、LIST操作。

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default                          # 命名空间
  name: role-example
rules:
- apiGroups: [""]
  resources: ["pods"]                         # 可以访问pod
  verbs: ["get", "list"]                      # 可以执行GET、LIST操作

有了Role之后,就可以将Role与具体的用户绑定起来,实现这个的就是RoleBinding了。如下所示:

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: rolebinding-example
  namespace: default
subjects:                                 # 指定用户
- kind: ServiceAccount                    # ServiceAccount
  name: sa-example
  namespace: default
roleRef:                                  # 指定角色
  kind: Role
  name: role-example
  apiGroup: rbac.authorization.k8s.io

在Kubernetes集群apply Role和Rolebind文件,现在再进入到sa-example这个Pod,使用curl命令通过API Server访问资源来验证权限是否生效。

/ # export CURL_CA_BUNDLE=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
/ # TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
/ # curl -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/default/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/namespaces/default/pods",
    "resourceVersion": "10377013"
  },
  "items": [
    {
      "metadata": {
        "name": "sa-example",
......

返回结果正常,说明sa-example现在有LIST Pod的权限了。

注意 1:本示例中,sa-example Pod通过HTTPS单向认证方式与Kube-Apiserver服务端建立连接后,进入Kube-Apiserver认证过滤器链(默认支持CA认证和Token认证两种身份验证方式,因为客户端证书为空,所以不会执行CA认证过滤器逻辑),如果请求中包含Authorization头部,并且其值是以Bearer开头的Token,则进入Token认证过滤器逻辑,对JWT Token进行验证,如果Token未过期且签名有效,则认证成功,并从中获取Pod所使用的ServiceAccount的用户信息。认证成功后,进入Kube-Apiserver授权阶段,如果请求被授权,则继续处理;否则,Kube-Apiserver会返回HTTP 401未授权错误。

注意 2:本示例中,sa-example Pod和 API Server 之间建立了一个加密的 SSL/TLS 连接(HTTPS单向认证方式),所有的通信都会经过加密传输,包括客户端请求中的 token 信息和 Kube-Apiserver 的响应。这样可以确保通信过程中的安全性,同时防止任何未经授权的访问和攻击。HTTPS单向认证步骤如下:

  1. 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端。
  2. 服务端将自己的公钥证书(server.crt)发送给客户端。
  3. 客户端通过自己的根证书(ca.crt)验证服务端的公钥证书(server.crt)的合法性,取出服务端公钥。(如果验证公钥证书失败,则中断HTTPS连接)
  4. 客户端生成密钥R,用服务端公钥去加密它形成密文,发送给服务端。
  5. 服务端用自己的私钥(server.key)去解密这个密文,得到客户端的密钥R。
  6. 服务端和客户端使用密钥R进行通信。

注意 3: Kube-Apiserver tls客户端认证配置为RequestClientCert(客户端请求可以不发送客户端证书),即可以不使用SSL/TLS加密方式或仅使用SSL/TLS单向认证方式或使SSL/TLS双向认证方式与Kube-Apiserver服务端建立连接,所以不使用SSL/TLS加密方式方式认证与Kube-Apiserver服务端简历连接也是可以的。

/ # curl -k  https://kubernetes/api/v1/namespaces/default/pods
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "pods is forbidden: User \"system:anonymous\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403
/ # TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
/ # curl -k -H "Authorization: Bearer $TOKEN" https://kubernetes/api/v1/namespaces/default/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/namespaces/default/pods",
    "resourceVersion": "10377013"
  },
  "items": [
    {
      "metadata": {
        "name": "sa-example",
......
注意 4:Kube-Apiserver服务端监听的是tls端口,如果使用http方式访问是不行的(对HTTPS服务发起HTTP请求会报请求方式错误,Client sent an HTTP request to an HTTPS server.)。
/ # TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
/ # curl -k -H "Authorization: Bearer $TOKEN" http://kubernetes/api/v1/namespaces/default/pods
curl: (52) Empty reply from server

3、总结

在 Kubernetes 中,Pod客户端和Kube-Apiserver服务端进行交互时,使用 token 进行身份验证的请求会使用 SSL/TLS 加密进行保护。具体来说,使用 token 进行身份验证的请求流程如下:

  1. 客户端使用提前生成好的 token 发起请求,将 token 附加在请求的 Authorization 头部。

  2. 客户端会首先与 API Server 建立一个加密的 SSL/TLS 连接,以保护通信的安全性。

  3. Kube-Apiserver 收到请求后,会对 token 进行验证,如果验证通过,进入Kube-Apiserver授权阶段,如果请求被授权,则继续处理;否则,Kube-Apiserver会返回HTTP 401未授权错误。

在这个过程中,客户端和 API Server 之间建立了一个加密的 SSL/TLS 连接,所有的通信都会经过加密传输,包括客户端请求中的 token 信息和 API Server 的响应。这样可以确保通信过程中的安全性,同时防止任何未经授权的访问和攻击。

需要注意的是,虽然使用 token 进行身份验证的请求会经过 SSL/TLS 加密进行保护,但是 SSL/TLS 加密并不能完全防止所有的安全威胁,因此在实际使用中,仍然需要采取其他安全措施来保障 Kubernetes 环境的安全性。

参考:ServiceAccount

参考:深入理解 Kubernetes 中的用户与身份认证授权