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

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

![Controlling Access to the Kubernetes API](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/19090beadee8195f7355-186299640-f1b0c589-1e82-43f1-8997-36d148d4b526.png)

### 帳號類型

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` 命名空間
    
    ```bash
    kubectl create namespace dev
    kubectl label namespace dev name=dev
    ```
    
2.  取得 `dev` 命名空間下的 `serviceaccounts`
    
    ```bash
    kubectl get serviceaccounts --namespace=dev
    ```
    
    ```
    NAME      SECRETS   AGE
    default   1         9s
    ```
    
3.  取得 `dev` 命名空間下的 `default` 服務帳戶內容
    
    ```bash
    kubectl get serviceaccounts default -n=dev -o yaml
    ```
    
    ```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` 內容
    
    ```bash
    kubectl get secrets default-token-xpqc7 -n=dev -o yaml
    ```
    
    ```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` 服務帳戶
    
    ```bash
    kubectl run microbot --image=dontrebootme/microbot:v1 -n dev
    ```
    
2.  事實上，所有的 Pod 預設會加入 `default` 服務帳戶
    
    在 `namespace` 建立 Pod 的時候，如果沒有特別指定 `spec.serviceAccountName` 的話，Kubernetes 也會預設幫你加上相同命名空間下的 `default` 服務帳戶。因此，每個 Pod 其實都一定會綁定一個服務帳戶。
    
    ```bash
    kubectl get microbot -n dev -o yaml
    ```
    
    此時你會看到如下的 YAML 檔，有個 `serviceAccountName: default` 已經被自動設定進去了：
    
    ```yaml
    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` 目錄內容
    
    ```bash
    kubectl exec microbot -it -n dev -- sh
    ```
    
    ```bash
    ls -laF /var/run/secrets/kubernetes.io/serviceaccount
    ```
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/21345bec1a331ba95581-186197379-83996484-ad05-4093-88e7-27196e96efc2.png)
    
    看起來他會自動把 `serviceaccounts` 所包含的 `secrets` 內容全部掛載到這個目錄下！
    
4.  在 Pod 的容器中對 `kube-apiserver` 發送 HTTP 要求
    
    ```bash
    apk update
    apk add curl
    
    curl --insecure https://kubernetes.default.svc.cluster.local:443/api/
    ```
    
    在沒有帶入 TOKEN 的情況下，會是以 `system:anonymous` 匿名使用者存取：
    
    ```json
    {
      "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`
    
    ```bash
    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 要求已經通過驗證：
    
    ```json
    {
      "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` 清單
    
    ```bash
    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
    ```
    
    你會得到以下錯誤訊息：
    
    ```json
    {
      "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
    }
    ```
    
    > 此時我們會知道我們的 **User** 為 `system:serviceaccount:dev:default`，且 **API group** 為 `""`，命名空間(`namespace`)為 `dev`，資源類型(`kind`)為 `pods`
    
    除此之外，你也可以透過 `kubectl auth can-i` 命令，快速查出**特定使用者**是否具有**特定資源**的**權限**，相當實用！
    
    ```bash
    kubectl auth can-i get pods -n=dev --as=system:serviceaccount:dev:default
    ```
    
    他會很簡單的回你 `yes` 或 `no`，以目前的狀態來說，應該會回 `no` 才對！ 👍
    
2.  建立 `Role` 物件
    
    ```bash
    kubectl create role read-pods -n=dev --verb='get,list' --resource=pods
    ```
    
    或透過 `--dry-run=client -o yaml` 參數產生相對應的 YAML 檔內容：
    
    ```bash
    kubectl create role read-pods -n=dev --verb='get,list' --resource=pods --dry-run=client -o yaml
    ```
    
    ```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` 物件
    
    ```bash
    kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods
    ```
    
    或透過 `--dry-run=client -o yaml` 參數產生相對應的 YAML 檔內容：
    
    ```bash
    kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods --dry-run=client -o yaml
    ```
    
    ```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`)，只是名稱表達的方式不同而已：
    
    ```yaml
    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` 服務帳戶是否已經獲得 `pods` 的 `get` 權限：
    
    ```bash
    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` 清單
    
    ```bash
    curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
    ```
    
    此時應該就能夠取得完整的 `PodList` 清單與詳細資訊了：
    
    ```json
    {
      "kind": "PodList",
      "apiVersion": "v1",
      "metadata": {
        "selfLink": "/api/v1/namespaces/dev/pods/",
        "resourceVersion": "1446386"
      },
      "items": [
        {
          "metadata": {
            "name": "microbot",
            "namespace": "dev",
            ...
          }
          ...
        }
        ...
      ]
    }
    ```
    

### 體驗建立新的服務帳戶

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

1.  在 `dev` 命名空間建立自訂的服務帳戶 `monitor`
    
    ```bash
    kubectl create serviceaccount monitor -n dev
    ```
    
2.  設定 `Role` 與 `RoleBinding`
    
    ```bash
    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` 服務帳戶
    
    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: "microbot"
      namespace: dev
      labels:
        app: "microbot"
    spec:
      containers:
      - name: microbot
        image: "dontrebootme/microbot:v1"
      serviceAccountName: monitor
    ```
    
    用 PowerShell 快速套用的方法：
    
    ```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 伺服器
    
    ```bash
    kubectl exec microbot -it -n dev -- sh
    ```
    
    ```bash
    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 讀取 `ConfigMaps` 或 `Secrets` 都要透過 `volumeMounts` (目錄或檔案) 或 `env` (環境變數) 的方式掛載進去，但事實上透過 `volumeMounts` 或 `env` 的方式並無法套用 RBAC 授權，當你想限制應用程式對叢集資源的存取範圍時，還要透過變更 YAML 並找有力人士套用更新，這也太不可靠了吧。況且，每次變更組態設定都要重新部署 Deployment 或重新啟動 Deployment 才能生效，也沒有很方便。

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

### 相關連結

-   [Configure Service Accounts for Pods | Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
-   [Working with Service Account In Kubernetes](https://medium.com/the-programmer/working-with-service-account-in-kubernetes-df129cb4d1cc)
-   [Kubernetes Tips: Using a ServiceAccount](https://betterprogramming.pub/k8s-tips-using-a-serviceaccount-801c433d0023)
-   [Kubernetes API Concepts | Kubernetes](https://kubernetes.io/docs/reference/using-api/api-concepts/)
-   [API Access Control | Kubernetes](https://kubernetes.io/docs/reference/access-authn-authz/)
-   [Using RBAC Authorization | Kubernetes](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
