iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
DevOps

30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記系列 第 18

【Day18】Kubernetes 權限基礎:Security Context 容器安全與 ServiceAccount

  • 分享至 

  • xImage
  •  

前情提要

昨天我們把 WordPress 跟 MariaDB 成功部署起來,透過 Volumes 保存資料,並讓前端和後端能透過 Service 正常連線。雖然功能上都沒問題,但回頭想想,這些 Pod 到底是「用誰的身份」在跑?又能不能存取 API 或主機上的特定資源?其實我們一路下來都是用 預設的權限與安全設定,這在測試環境沒什麼,但在真實環境裡就會變成一個隱憂。

所以今天要換個角度,來看 Kubernetes 裡的兩個安全基礎:Security ContextServiceAccount。前者決定 Pod / Container 能不能以 root 身份執行、能用哪些 Linux capabilities;後者則是應用在叢集裡的「身份證」,影響它能不能去呼叫 Kubernetes API、能存取什麼資源。

Security Context

在 Docker 裡,我們可以指定 Container 的使用者 ID、是否允許 root、要增加或刪除哪些 Linux Capabilities。Kubernetes 也提供類似的機制,就被叫做 Security Context

Security Context 用來定義 Pod 或 Container 的特權與存取控制設定,像是以哪個使用者身分運行、是否允許提權、能否修改核心參數、是否使用特權模式等等。它可以設定在 Pod 層級(影響該 Pod 中所有的容器)或 Container 層級(只影響指定容器,並會覆蓋 Pod 層級的相同設定)。

常見的設定欄位包括:

  • runAsUser / runAsGroup:指定容器的進程要以哪個使用者 ID (UID) / 群組 ID (GID) 執行。
  • runAsNonRoot:布林值。設為 true 會強制容器必須以非 root 帳號執行,否則 Pod 會無法啟動。
  • privileged:布林值。設為 true 即為「特權模式」,容器將能存取主機上所有的裝置(如 /dev),並繞過許多核心層級的限制。權限極大,應謹慎使用。
  • allowPrivilegeEscalation:布林值。控制容器內的進程是否可以獲得比其父進程更多的權限。例如,透過 setuidsetgid 位元的執行檔來提權。為了安全,通常建議設為 false
  • fsGroup:設定一個特殊的群組 ID,讓 Pod 中所有容器掛載的 Volume 都會屬於這個群組。這對於共享儲存的權限管理非常有用。
  • readOnlyRootFilesystem:布林值。設為 true 會將容器的根目錄檔案系統以「唯讀」方式掛載。這是個極佳的安全實踐,可防止惡意程式修改容器內的系統檔案。應用程式需要寫入的路徑則需額外掛載 Volume。
  • capabilities:細緻化 root 權限,只給容器需要的部分能力。例如只給予網路管理 (NET_ADMIN) 能力,但移除其他危險權限。
  • seccompProfile:限制容器內可以使用的系統呼叫 (System Calls),降低核心被攻擊的風險。
  • appArmorProfile:使用 AppArmor 來限制程式的權限,例如可以讀取、寫入或執行哪些檔案。

換句話說,Security Context 是容器級別的「權限管理員」,決定它能夠對作業系統做到什麼程度。

如要開啟容器使用 privileged 權限,需要於 kube-apiserver 啟動時加入參數 --allow-privileged=true。由於現行的 Kubernetes 版本預設已啟用此功能,故後續只要在 Pod 或 Container 中加入 Security Context 設定即可。

gh

ServiceAccount

另一個安全關鍵是 ServiceAccount。在 Kubernetes 裡,有兩種帳號:User Account(人類用)和 ServiceAccount(應用程式用)。

這個概念跟雲端服務的 IAM 角色很像。譬如說,我的應用程式需要存取 GCP 的 Vertex AI 服務,我就需要建立一個 Service Account,並賦予這個 Service Account Vertex AI User 的角色,應用程式就能以這個身份去呼叫 Vertex AI 的 API。

在 Kubernetes 的概念也一樣。舉例來說,監控系統 Prometheus 需要讀取 Kubernetes API 來取得叢集的 Metrics,CI/CD 工具 Jenkins 需要權限把應用程式部署到叢集裡,這些場景就需要透過 ServiceAccount 來進行身份驗證與授權。

過去,建立 ServiceAccount 時會自動產生一個「不會過期的 Secret Token」,但這在安全性和可擴展性上都有隱憂。從 Kubernetes 1.22 開始,引入了 TokenRequest API,用來產生具有效期與使用者(audience)綁定的短期 Token,更加安全。到了 1.24,ServiceAccount 預設已不再自動建立 Secret,需要我們手動透過 kubectl create token <service-account-name> 來生成。

實戰 🔥

我們先看到 Pod Security Context 這邊,那前面介紹一直提到 privileged (特權模式),因此這邊我們來看看有無特權模式差在哪:

特權模式 (Privileged Mode) 的差異

建立一個沒有任何 Security Context 設定的標準 Pod。雖然它預設以 root 使用者執行,但並非特權模式。

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: non-priviledged
  name: non-priviledged
spec:
  containers:
  - args:
    - /bin/sh
    - -c
    - sleep 3600;
    image: rockylinux:9
    name: non-priviledged
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}

把 Pod 建立起來以後,我們進入 Container 查看 /dev 目錄下的裝置檔案:

gh

建立一個特權模式的 Pod,只需在 container 層級加上 securityContext

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: priviledged
  name: priviledged
spec:
  containers:
  - args:
    - /bin/sh
    - -c
    - sleep 3600;
    image: rockylinux:9
    name: priviledged
    # 加上 Security Context
    securityContext:
      privileged: true
      procMount: Default
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}

可以看到特權模式的 Pod 能夠看到主機上幾乎所有的系統裝置檔案(如 sda, nvme0 等),而非特權模式的 Pod 只能看到最基本的虛擬裝置。這意味著特權容器幾乎取得了與主機 root 同等的硬體存取能力,風險極高。

gh

控制使用者身份與檔案權限:runAsUserfsGroup

建立一個未指定任何使用者資訊的 Pod。它會掛載一個 emptyDir 的 Volume 到 /data/demo

apiVersion: v1
kind: Pod
metadata:
  name: security-non-context-demo
spec:
  volumes:
  - name: sec-ctx-vol
    emptyDir: {}
  containers:
  - name: sec-ctx-demo
    image: gcr.io/google-samples/node-hello:1.0
    volumeMounts:
    - name: sec-ctx-vol
      mountPath: /data/demo
    securityContext:
      allowPrivilegeEscalation: false

建立另一個 Pod,在 Pod 層級 設定 securityContext,指定所有容器內的進程都必須以 UID 1000 的身份執行,並且掛載的 Volume 檔案系統群組為 GID 2000。透過 Security Context 控制容器的使用者與檔案系統權限。

apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo
spec:
  securityContext:
    runAsUser: 1000
    fsGroup: 2000
  volumes:
  - name: sec-ctx-vol
    emptyDir: {}
  containers:
  - name: sec-ctx-demo
    image: gcr.io/google-samples/node-hello:1.0
    volumeMounts:
    - name: sec-ctx-vol
      mountPath: /data/demo
    securityContext:
      allowPrivilegeEscalation: false

從下面的結果可以看到:

  1. 第一個 Pod 因為設定了 runAsUser: 1000,容器內的進程被強制以 UID 1000 的身份啟動。另一個 fsGroup: 2000 的設定,它讓 Kubernetes 自動將掛載進來的 sec-ctx-vol Volume 的擁有群組設為 2000,並賦予寫入權限。這確保了即使我們的進程 (UID 1000) 不是 root,也能成功地在 /data/demo 目錄下寫入檔案。我們遵循「最小權限原則」,在不使用 root 的情況下,精準地控制檔案系統權限。
  2. 第二個 Pod 因為沒有指定 runAsUser,預設以 root (UID=0) 身份執行,因此建立的檔案擁有者是 root:root

gh

gh

調整核心變數 (sysctl)

在某些應用場景(例如 Elasticsearch),需要調整核心參數才能正常運作。Security Context 也允許我們修改被標記為 safe 的核心參數。

建立一個未調整核心參數的 Pod,其核心參數 vm.max_map_count 為預設值:

kubectl run non-sysctl --image=rockylinux:9 --restart=Never --dry-run=client -o yaml -- /bin/sh -c "sleep 3600;" > security_context_non_sysctl.yaml
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: non-sysctl
  name: non-sysctl
spec:
  containers:
  - args:
    - /bin/sh
    - -c
    - sleep 3600;
    image: rockylinux:9
    name: non-sysctl
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}

建立另一個 Pod,並透過 initContainers 來修改核心參數。修改核心參數本身需要特權,所以 initContainers 必須設定 privileged: true

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: sysctl
  name: sysctl
spec:
  initContainers:
  - name: init-sysctl
    image: busybox:latest
    command:
    - sysctl
    - -w
    - vm.max_map_count=262144
    securityContext:
      privileged: true
  containers:
  - args:
    - /bin/sh
    - -c
    - sleep 36000;
    image: rockylinux:9
    name: sysctl
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}

這邊礙於 kind 的 Container 沒有 sysctl 指令,因此我無法 demo 結果,但是我看別人 demo 的結果會長這樣,做法是分別進入兩個 Container 檢查核心參數,可以看到 sysctl Pod 中的 vm.max_map_count 已被成功修改:

[root@master ~]# kubectl exec -it non-sysctl -- /bin/bash -c "sysctl -a | grep map"
vm.max_map_count = 65530
vm.min_unmapped_ratio = 1
vm.mmap_min_addr = 4096
vm.mmap_rnd_bits = 28
vm.mmap_rnd_compat_bits = 8

[root@master ~]# kubectl exec -it sysctl -- /bin/bash -c "sysctl -a | grep map"
fs.nfs.idmap_cache_timeout = 0
vm.max_map_count = 262144
vm.min_unmapped_ratio = 1
vm.mmap_min_addr = 4096
vm.mmap_rnd_bits = 28
vm.mmap_rnd_compat_bits = 8

精細化權限:Linux Capabilities

如果不想要給予完整的 privileged 權限,但又需要部分 root 能力時,capabilities 就是最好的選擇。它允許你「精準地」賦予容器所需的最小權限。

例如,有個應用程式需要修改系統時間 (SYS_TIME) 和設定網路介面 (NET_ADMIN),但不需要其他 root 權限。我們可以這樣設定:

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
  namespace: default
spec:
  containers:
  - command:
    - sleep
    - "4800"
    image: ubuntu
    name: ubuntu-sleeper
    securityContext:
      capabilities:
        add: ["SYS_TIME", "NET_ADMIN"]

小結

我們的實戰從對比 privileged 模式開始,透過比較 /dev 目錄下的內容,直接驗證了特權容器能存取主機所有裝置的高度風險,確認了這是在生產環境中應極力避免的設定。

接著,runAsUserfsGroup 的實戰則展示了具體的最小權限實踐方式:我們看到了應用程式完全可以透過指定 UID 在非 root 身份下運行,同時 fsGroup 的設定確保了它對儲存卷依然擁有必要的寫入權限,達成了安全與功能的平衡。

對於需要部分特權的場景,capabilities 的設定提供了比 privileged: true 更精細的控制,它證明我們能只授予應用程式必要的單一權限(如 NET_ADMIN),而非全盤開放。

然而,單純依賴開發者自律來設定 Security Context 顯然存在風險。正如 privileged 模式的實作所示,賦予容器過高的權限,幾乎等同於交出了主機節點的控制權,這在生產環境中是難以接受的。為了解決這個問題,Kubernetes 提供了 Pod Security Admission (PSA)

這是一個在 Namespace 層級運作的內建安全控制器,管理者可以為不同環境設定預先定義好的安全等級(如 baselinerestricted)。當我們部署 Pod 時,PSA 會自動校驗其 Security Context,並根據策略決定是 強制拒絕(enforce)僅發出警告(warn),還是 記錄事件(audit)。這種叢集級別的策略管理偏向 CKS(認證 Kubernetes 安全專家)的範疇,因此我們暫不深入,待後續有機會再來探討。

題外話

今天工作比較忙,整個趕不完,先發文定義一下今天的學習的東西,會再慢慢更新實戰內容上來。


上一篇
【Day17】Kubernetes 實戰演練:打造 WordPress + MariaDB 部署
系列文
30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言