昨天我們把 WordPress 跟 MariaDB 成功部署起來,透過 Volumes 保存資料,並讓前端和後端能透過 Service 正常連線。雖然功能上都沒問題,但回頭想想,這些 Pod 到底是「用誰的身份」在跑?又能不能存取 API 或主機上的特定資源?其實我們一路下來都是用 預設的權限與安全設定,這在測試環境沒什麼,但在真實環境裡就會變成一個隱憂。
所以今天要換個角度,來看 Kubernetes 裡的兩個安全基礎:Security Context 和 ServiceAccount。前者決定 Pod / Container 能不能以 root 身份執行、能用哪些 Linux capabilities;後者則是應用在叢集裡的「身份證」,影響它能不能去呼叫 Kubernetes API、能存取什麼資源。
在 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
:布林值。控制容器內的進程是否可以獲得比其父進程更多的權限。例如,透過 setuid
或 setgid
位元的執行檔來提權。為了安全,通常建議設為 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 設定即可。
另一個安全關鍵是 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 (特權模式),因此這邊我們來看看有無特權模式差在哪:
建立一個沒有任何 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
目錄下的裝置檔案:
建立一個特權模式的 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 同等的硬體存取能力,風險極高。
runAsUser
與 fsGroup
建立一個未指定任何使用者資訊的 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
從下面的結果可以看到:
runAsUser: 1000
,容器內的進程被強制以 UID 1000
的身份啟動。另一個 fsGroup: 2000
的設定,它讓 Kubernetes 自動將掛載進來的 sec-ctx-vol
Volume 的擁有群組設為 2000
,並賦予寫入權限。這確保了即使我們的進程 (UID 1000) 不是 root,也能成功地在 /data/demo
目錄下寫入檔案。我們遵循「最小權限原則」,在不使用 root 的情況下,精準地控制檔案系統權限。runAsUser
,預設以 root
(UID=0) 身份執行,因此建立的檔案擁有者是 root:root
。在某些應用場景(例如 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
如果不想要給予完整的 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
目錄下的內容,直接驗證了特權容器能存取主機所有裝置的高度風險,確認了這是在生產環境中應極力避免的設定。
接著,runAsUser
與 fsGroup
的實戰則展示了具體的最小權限實踐方式:我們看到了應用程式完全可以透過指定 UID 在非 root 身份下運行,同時 fsGroup
的設定確保了它對儲存卷依然擁有必要的寫入權限,達成了安全與功能的平衡。
對於需要部分特權的場景,capabilities
的設定提供了比 privileged: true
更精細的控制,它證明我們能只授予應用程式必要的單一權限(如 NET_ADMIN
),而非全盤開放。
然而,單純依賴開發者自律來設定 Security Context
顯然存在風險。正如 privileged
模式的實作所示,賦予容器過高的權限,幾乎等同於交出了主機節點的控制權,這在生產環境中是難以接受的。為了解決這個問題,Kubernetes 提供了 Pod Security Admission (PSA)。
這是一個在 Namespace 層級運作的內建安全控制器,管理者可以為不同環境設定預先定義好的安全等級(如 baseline
或 restricted
)。當我們部署 Pod 時,PSA 會自動校驗其 Security Context
,並根據策略決定是 強制拒絕(enforce)、僅發出警告(warn),還是 記錄事件(audit)。這種叢集級別的策略管理偏向 CKS(認證 Kubernetes 安全專家)的範疇,因此我們暫不深入,待後續有機會再來探討。
今天工作比較忙,整個趕不完,先發文定義一下今天的學習的東西,會再慢慢更新實戰內容上來。