Kubernetes使用者端認證(二)—— 基於ServiceAccount的JWTToken認證

2023-04-11 21:00:31

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.
/ # 

報錯原因是因為curl使用者端和Kube-Apiserver伺服器端建立的是SSL/TLS單向連線,curl和Kube-Apiserver建立連線過程中,Kube-Apiserver會將伺服器端證書返回給curl使用者端,而curl使用者端沒有指定ca.crt,因此無法驗證伺服器的合法性,因此無法建立與它的安全連線。

使用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 中的使用者與身份認證授權