The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

透過 MicroK8s 認識 Kubernetes 的 Service Account (服務帳戶)

Kubernetes 的 Service Account (服務帳戶) 是一個由 Kubernetes 管理的帳戶類型,在管理上可說是特別方便,但是在剛接觸到這種帳戶類型時,不是很容易理解應用的情境。這篇文章是我閱讀了好多份文件之後,整理出來的完整脈絡,相信可以對服務帳戶有一定程度的理解。

Controlling Access to the Kubernetes API

帳號類型

Kubernetes 的帳號有兩種類型,分別為:

  1. 使用者帳戶 (Normal Users)

    任何人想要連接並存取 Kubernetes 叢集,都需要先建立一個 "使用者帳戶" 並將憑證資訊提供給用戶端 (如: kubectl),以便通過 Kubernetes 的 API server 的認證 (Authentication)。

    我覺得這個名字應該稱為 User Accounts 會比較好理解,但是 Kubernetes 官網稱一般使用者 (Normal Users)。

  2. 服務帳戶 (Service Accounts)

    任何跑在 Pod 裡面的容器想要存取 Kubernetes 的 API 伺服器 (kube-apiserver),就需要先有一個 "服務帳戶" 綁定在 Pod 身上,然後以便通過 Kubernetes 的 API 伺服器的身份認證 (Authentication)。

體驗命名空間預設的服務帳戶 (Service Account)

當你建立 namespace 的時候,預設就會幫你建立好一個名為 default 的服務帳戶:

  1. 建立 dev 命名空間

    kubectl create namespace dev
    kubectl label namespace dev name=dev
    
  2. 取得 dev 命名空間下的 serviceaccounts

    kubectl get serviceaccounts --namespace=dev
    
    NAME      SECRETS   AGE
    default   1         9s
    
  3. 取得 dev 命名空間下的 default 服務帳戶內容

    kubectl get serviceaccounts default -n=dev -o yaml
    
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      creationTimestamp: "2022-08-23T14:55:51Z"
      name: default
      namespace: dev
      resourceVersion: "1434536"
      selfLink: /api/v1/namespaces/dev/serviceaccounts/default
      uid: 3b750bc5-fd6c-43b0-9c64-4a4700f522ae
    secrets:
    - name: default-token-xpqc7
    

    他會自動綁定一個 secrets 保存該服務帳戶的 Token 資訊

  4. 取得 dev 命名空間下的 default 服務帳戶綁定的 secrets 內容

    kubectl get secrets default-token-xpqc7 -n=dev -o yaml
    
    apiVersion: v1
    data:
      ca.crt: DATA+OMITTED
      namespace: ZGV2
      token: TOKEN+OMITTED
    kind: Secret
    metadata:
      annotations:
        kubernetes.io/service-account.name: default
        kubernetes.io/service-account.uid: 3b750bc5-fd6c-43b0-9c64-4a4700f522ae
      creationTimestamp: "2022-08-23T14:55:51Z"
      name: default-token-xpqc7
      namespace: dev
      resourceVersion: "1434535"
      selfLink: /api/v1/namespaces/dev/secrets/default-token-xpqc7
      uid: 868a7d4f-74b8-4be4-8c0e-b9d5a3e678b2
    type: kubernetes.io/service-account-token
    

體驗 Pod 如何使用服務帳戶 (Service Account)

  1. 我們先在 dev 命名空間中建立一個 Pod,但不特別指定 default 服務帳戶

    kubectl run microbot --image=dontrebootme/microbot:v1 -n dev
    
  2. 事實上,所有的 Pod 預設會加入 default 服務帳戶

    namespace 建立 Pod 的時候,如果沒有特別指定 spec.serviceAccountName 的話,Kubernetes 也會預設幫你加上相同命名空間下的 default 服務帳戶。因此,每個 Pod 其實都一定會綁定一個服務帳戶。

    kubectl get microbot -n dev -o yaml
    

    此時你會看到如下的 YAML 檔,有個 serviceAccountName: default 已經被自動設定進去了:

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        run: microbot
      name: microbot
      namespace: dev
    spec:
      containers:
      - image: dontrebootme/microbot:v1
        imagePullPolicy: IfNotPresent
        name: microbot
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          name: kube-api-access-dfs8b
          readOnly: true
      dnsPolicy: ClusterFirst
      enableServiceLinks: true
      nodeName: microk8s-vm
      preemptionPolicy: PreemptLowerPriority
      priority: 0
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      serviceAccount: default
      serviceAccountName: default
      ...
    

    而且,在 Pod 裡面預設會掛載一個 /var/run/secrets/kubernetes.io/serviceaccount 目錄!

  3. 查看 Pod 裡面的 /var/run/secrets/kubernetes.io/serviceaccount 目錄內容

    kubectl exec microbot -it -n dev -- sh
    
    ls -laF /var/run/secrets/kubernetes.io/serviceaccount
    

    image

    看起來他會自動把 serviceaccounts 所包含的 secrets 內容全部掛載到這個目錄下!

  4. 在 Pod 的容器中對 kube-apiserver 發送 HTTP 要求

    apk update
    apk add curl
    
    curl --insecure https://kubernetes.default.svc.cluster.local:443/api/
    

    在沒有帶入 TOKEN 的情況下,會是以 system:anonymous 匿名使用者存取:

    {
      "kind": "Status",
      "apiVersion": "v1",
      "metadata": {
    
      },
      "status": "Failure",
      "message": "forbidden: User \"system:anonymous\" cannot get path \"/api/\"",
      "reason": "Forbidden",
      "details": {
    
      },
      "code": 403
    }
    
  5. 帶入 default 服務帳戶的 TOKEN 來存取 kube-apiserver

    CACERT='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
    TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/
    

    此時你會看到 API 要求已經通過驗證:

    {
      "kind": "APIVersions",
      "versions": [
        "v1"
      ],
      "serverAddressByClientCIDRs": [
        {
          "clientCIDR": "0.0.0.0/0",
          "serverAddress": "172.25.239.227:16443"
        }
      ]
    }
    

以上就是在 Pod 裡面使用「服務帳戶」的標準方法! 👍

替服務帳戶 (Service Account) 加入角色權限

事實上,這個「通過身份驗證」的 TOKEN 雖然可以呼叫 kube-apiserver 的部分 API,但事實上目前這個預設的服務帳戶並無法存取任何 K8s 叢集中的資源,我們還必須透過 RBAC 機制,先建立一個 Role 並指派權限進去,再透過 RoleBinding 將此帳號綁定,才能賦予他足夠的權限存取資源。

  1. 先試圖取得 PodList 清單

    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
    

    你會得到以下錯誤訊息:

    {
      "kind": "Status",
      "apiVersion": "v1",
      "metadata": {},
      "status": "Failure",
      "message": "pods is forbidden: User \"system:serviceaccount:dev:default\" cannot list resource \"pods\" in API group \"\" in the namespace \"dev\"",
      "reason": "Forbidden",
      "details": {
        "kind": "pods"
      },
      "code": 403
    }
    

    此時我們會知道我們的 Usersystem:serviceaccount:dev:default,且 API group"",命名空間(namespace)為 dev,資源類型(kind)為 pods

    除此之外,你也可以透過 kubectl auth can-i 命令,快速查出特定使用者是否具有特定資源權限,相當實用!

    kubectl auth can-i get pods -n=dev --as=system:serviceaccount:dev:default
    

    他會很簡單的回你 yesno,以目前的狀態來說,應該會回 no 才對! 👍

  2. 建立 Role 物件

    kubectl create role read-pods -n=dev --verb='get,list' --resource=pods
    

    或透過 --dry-run=client -o yaml 參數產生相對應的 YAML 檔內容:

    kubectl create role read-pods -n=dev --verb='get,list' --resource=pods --dry-run=client -o yaml
    
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      creationTimestamp: null
      name: read-pods
      namespace: dev
    rules:
    - apiGroups:
      - ""
      resources:
      - pods
      verbs:
      - get
      - list
    
  3. 建立 RoleBinding 物件

    kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods
    

    或透過 --dry-run=client -o yaml 參數產生相對應的 YAML 檔內容:

    kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods --dry-run=client -o yaml
    
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      creationTimestamp: null
      name: read-pods
      namespace: dev
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: read-pods
    subjects:
    - apiGroup: rbac.authorization.k8s.io
      kind: User
      name: system:serviceaccount:dev:default
    

    上述語法也可以在 subjects: 欄位寫成這樣,其實都是一樣的,服務帳戶 (kind: ServiceAccount) (name: default) 就是 一般帳戶 (kind: User) (name: system:serviceaccount:dev:default),只是名稱表達的方式不同而已:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: read-pods
      namespace: dev
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: read-pods
    subjects:
    - kind: ServiceAccount
      name: default
    

    先檢查一下 system:serviceaccount:dev:default 服務帳戶是否已經獲得 podsget 權限:

    kubectl auth can-i get pods -n=dev --as=system:serviceaccount:dev:default
    kubectl auth can-i list pods -n=dev --as=system:serviceaccount:dev:default
    
  4. 再取得一次 PodList 清單

    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
    

    此時應該就能夠取得完整的 PodList 清單與詳細資訊了:

    {
      "kind": "PodList",
      "apiVersion": "v1",
      "metadata": {
        "selfLink": "/api/v1/namespaces/dev/pods/",
        "resourceVersion": "1446386"
      },
      "items": [
        {
          "metadata": {
            "name": "microbot",
            "namespace": "dev",
            ...
          }
          ...
        }
        ...
      ]
    }
    

體驗建立新的服務帳戶

有了上述對於 default 服務帳戶的理解,相信也就不難理解自訂服務帳戶的用法,以下是體驗的步驟:

  1. dev 命名空間建立自訂的服務帳戶 monitor

    kubectl create serviceaccount monitor -n dev
    
  2. 設定 RoleRoleBinding

    kubectl create role monitor-pods -n=dev --verb='get,list,watch' --resource='pods,pods/status'
    kubectl create rolebinding monitor-pods -n dev --serviceaccount='dev:monitor' --role=monitor-pods
    
  3. 使用 YAML 建立 Pod 並指定 serviceAccountName: monitor 服務帳戶

    apiVersion: v1
    kind: Pod
    metadata:
      name: "microbot"
      namespace: dev
      labels:
        app: "microbot"
    spec:
      containers:
      - name: microbot
        image: "dontrebootme/microbot:v1"
      serviceAccountName: monitor
    

    用 PowerShell 快速套用的方法:

    @'
    apiVersion: v1
    kind: Pod
    metadata:
      name: "microbot"
      namespace: dev
      labels:
        app: "microbot"
    spec:
      containers:
      - name: microbot
        image: "dontrebootme/microbot:v1"
      serviceAccountName: monitor
    '@ | kubectl apply -f -
    
  4. 從 Pods 內的容器呼叫 API 伺服器

    kubectl exec microbot -it -n dev -- sh
    
    apk update
    apk add curl
    
    CACERT='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
    TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
    

總結

有了這一層理解,你大概就知道如何從你在 Pod 的應用程式中存取 Kubernetes 叢集中的資源。

以前我會覺得要從 Pod 讀取 ConfigMapsSecrets 都要透過 volumeMounts (目錄或檔案) 或 env (環境變數) 的方式掛載進去,但事實上透過 volumeMountsenv 的方式並無法套用 RBAC 授權,當你想限制應用程式對叢集資源的存取範圍時,還要透過變更 YAML 並找有力人士套用更新,這也太不可靠了吧。況且,每次變更組態設定都要重新部署 Deployment 或重新啟動 Deployment 才能生效,也沒有很方便。

現在透過服務帳戶的方式,你可以直接從應用程式存取 Kubernetes 資源,還能透過 RBAC 機制限制存取的範圍,可謂是兼具彈性安全,我覺得相當不錯! 👍

相關連結