今日目的:搞懂 kubeadmin 與 developer 的權限邊界、SCC 如何決定 Pod 能不能建立起來;並用實測檢驗兩個廣為流傳的說法——「不預先授權 SCC 就會部署失敗」,以及「OpenShift 容器的 GID 恆為 0」。
先備知識:主線接回 Day 2 的帳號那條線,跟 Day 4 沒有直接關係;指令沿用 Day 3 建立的
& $OC加$kubeconfig呼叫模式。
Day 3 講的是系統事後查不到的狀態,Day 4 講的是不報錯卻悄悄做錯事。今天要碰的是相反的情況。
OpenShift 擋下一個 Pod 的時候,會把每一個候選 SCC 的判定結果、被拒的確切理由、UID 的合法區間,一字不漏印在事件訊息裡。資訊沒有被藏起來,問題是這則訊息長到沒有人會讀完,真正有用的那一行被埋在一長串「not usable」中間。
在標準 Kubernetes 叢集中,只要映像檔語法無誤、資源足夠,kubectl apply 通常能順利啟動 Pod。同一份 Manifest 部署到 OpenShift,從 Docker Hub 這類外部 Registry 拉的 Pod 卻常卡在 FailedCreate。
原因是 OpenShift 預設套用了比原生 Kubernetes 更嚴格的存取控制,這篇要拆的就是其中兩層:帳號的權限邊界(kubeadmin vs developer),以及決定 Pod 能不能真正建立起來的 SCC(Security Context Constraints)。這兩層拆完之後,會拿它們當工具去量兩件事:SCC 授權的時序到底影不影響部署成敗,以及「容器 GID 一定是 0」這句話成立的條件在哪裡。兩個答案都跟流傳的說法不同,也是今天真正要帶走的東西。
這是這 30 天遇到的第一道硬性阻擋。它的形狀跟系列後面會出現的另一種准入控制很像:都在 API Server 的准入階段攔下請求,資源從來沒有被真正建立過,不是啟動之後才失敗。差別在檢查的重點,SCC 問的是「你想用什麼權限跑」。前四天處理的是工具與環境,從今天開始才碰到防線本身。
錯誤訊息裡的線索,讀起來像什麼、實際代表什麼,落差可以很大:
| 錯誤訊息裡的線索 | 讀起來像什麼 | 實際代表什麼 |
|---|---|---|
provider "anyuid": ... not usable by user or serviceaccount |
隨機出現在一長串清單裡的一行 | ServiceAccount 根本沒有這個 SCC 的授權,連 UID 比對的資格都沒有 |
provider restricted-v2: ...must be in the ranges: [1000740000, 1000749999] |
一段看不懂的技術性拒絕 | 這是真正跑到 UID 比對這一步的 SCC,區間就是這個 Namespace 專屬的 UID 分配 |
unable to validate against any security context constraint |
籠統的一句「權限不足」 | 十幾個候選 SCC 全部跑過一輪比對之後的最終結論 |
這篇的實測環境是 OpenShift 4.21.14、Kubernetes 1.34.6。後面 SCC 清單裡會出現 restricted-v3、nested-container、hostmount-anyuid-v2、nonroot-v2 這些名字,跟版本高度相關,換一個版本候選清單不會完全一樣。
這篇會預設你知道下面這幾個東西。不用很熟,知道它們大概是什麼就夠了:
default。如果這幾個名詞對你來說還太陌生,建議先補 Kubernetes 的 RBAC 與 Pod 生命週期基礎再回來,這篇不會從頭解釋它們。反過來說,如果你已經在 OpenShift 上被 FailedCreate 擋過,那從這裡直接往下讀就可以。
開場那個 Pod 卡住的問題,最終要靠調整 SCC 授權解決。但 SCC 授權是叢集範圍的操作,不是每個帳號都能執行,所以動手之前先分清楚誰有權限做這件事。
CRC 初始化後,預設提供兩組不同權限層級的帳號:
| 帳號 | 密碼類型 | 權限範疇 | 主要用途 |
|---|---|---|---|
| developer | 固定密碼(developer) | Namespace 範圍 | 日常開發部署、建立 Project |
| kubeadmin | crc start 時動態生成 | 叢集範圍 | SCC 授權、Operator 安裝、跨 Namespace 資源管理 |
🪟 Windows/CRC 限定:developer 用固定密碼是 CRC 本地環境的便利設計,讓單機測試不必另外管理密碼。正式或雲端受管的 OpenShift/Kubernetes 不會有這種寫死、公開已知的密碼,這張表只適用於 CRC Local 這類本地開發情境。
用 developer 身份執行需要叢集權限的操作,例如變更 SCC 授權:
& $OC adm policy add-scc-to-user anyuid -z default -n myapp
會得到:
Error from server (Forbidden): rolebindings.rbac.authorization.k8s.io "system:openshift:scc:anyuid" is forbidden: user "developer" (groups=["system:authenticated:oauth" "system:authenticated"]) is attempting to grant RBAC permissions not currently held:
{APIGroups:["security.openshift.io"], Resources:["securitycontextconstraints"], ResourceNames:["anyuid"], Verbs:["use"]}
這則訊息講的比單純的「權限不足」精確得多。真正擋下 developer 的是 RBAC 的權限升級防護(privilege escalation prevention):你不能把一個自己都沒有的權限授予別人,這裡是「使用 anyuid 這個 SCC」的權限,對象就算只是同一個 Namespace 底下的 ServiceAccount 也一樣。
「這是叢集範圍操作」只是表面理由。實際的規則是,只有真正持有該權限的帳號才能把它往下發,也就是 kubeadmin 或被明確授權的自動化身份。
自動化維運跟開發現場要嚴格區分操作身份:
$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Select-Object -First 1).FullName
$kubeconfig = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"
# 1. 以 developer 身份登入(一般應用開發)
& $OC login -u developer -p developer https://api.crc.testing:6443 --insecure-skip-tls-verify=true
& $OC whoami
# 2. 切換至叢集管理者(用 CRC 產生的 kubeconfig 免密碼存取)
& $OC --kubeconfig $kubeconfig whoami
實際跑出來的身分:
PS> & $OC login -u developer -p developer https://api.crc.testing:6443 --insecure-skip-tls-verify=true
Login successful.
PS> & $OC whoami
developer
PS> & $OC --kubeconfig $kubeconfig whoami
system:admin
system:admin 就是具備 cluster-admin 權限的最高管理身份,透過 kubeconfig 存取完全不需要密碼。這個「不需要密碼」等一下會變成一個陷阱,先記著。
crc console --credentials 印出的是給人看的兩行文字,不是給程式解析的結構化資料。常見的省事寫法在 PowerShell 5.1 裡第一步就跑不動:
PS> crc console --credentials | grep kubeadmin | awk '{print $NF}'
grep : The term 'grep' is not recognized as the name of a cmdlet, function, script file, or operable program.
grep、awk 不是 PowerShell 內建指令,乾淨的 PowerShell 5.1 一律沒有。這跟 Day 4 開場「同一段指令換到 PowerShell 就壞掉」是同一個母題,只是這次連跑都跑不動,比「跑起來但結果錯」還前面一步。
(如果機器上另外裝了 Git for Windows 並把 Unix 工具目錄加進 PATH,這兩個指令可能可以跑,但那個 awk.exe 對 $NF 的行為是否一致,我沒有測過那個組合。)
就算撇開能不能執行,$NF 取最後一個欄位這個邏輯本身也站不住腳。實際輸出長這樣,密碼夾在句子中間,最後一個欄位是 API Endpoint URL:
To login as a regular user, run 'oc login -u developer -p developer https://api.crc.testing:6443'.
To login as an admin, run 'oc login -u kubeadmin -p <REDACTED> https://api.crc.testing:6443'
改用正則表達式取值可靠一些,但要注意型別:
$line = crc console --credentials 2>$null | Select-String "kubeadmin"
if ($line -match '-p\s+(\S+)') { $kubePass = $Matches[1] }
這台機器上只有一行輸出含 kubeadmin,$line 是純量 MatchInfo,-match 直接生效、$Matches 正確設定,所以這段今天能跑。
但這正是 Day 4 講過的陷阱。如果 crc 的輸出格式未來多一行也提到 kubeadmin,Select-String 會回傳陣列,對陣列做 -match 是過濾操作,回傳子集合而不是布林值,$Matches 也不會被設定。結果是 $kubePass 悄悄留著上一次殘留的值,而且不會報錯。
保險寫法是明確只取第一筆:
$line = crc console --credentials 2>$null | Select-String "kubeadmin" | Select-Object -First 1
if ($line.Line -match '-p\s+(\S+)') { $kubePass = $Matches[1] }
兩種寫法在今天這個輸出格式下結果一樣,差別只在防不防得住輸出行數變多的未來。
這整段在本系列裡終究只是個練習。所有自動化腳本一律走 kubeconfig 免密碼登入,只有遇到那種只吃明文密碼參數、不接受 kubeconfig 的第三方工具,才需要走這條路。
背後是同一組原則:CI/CD 或一般開發帳號不給 cluster-admin,developer 限制在自己拿得到的 Namespace 範圍內,SCC 授權跟 ClusterRoleBinding 這類操作統一由 kubeadmin 或有明確授權的自動化身份執行。這條原則落地後長什麼樣子,後面會實際看到:每一條 system:openshift:scc:<scc-name> 格式的 RoleBinding,就是它留下的紀錄。
SCC 是 OpenShift 用來限制 Pod 執行權限的核心機制,控制的層面包含:
發展脈絡大致是這樣:SCC 出現在 OpenShift 3.0(2015 年 6 月,比 Kubernetes 1.0 還早兩週),而 Kubernetes 的 PodSecurityPolicy 正是以 SCC 為藍本的簡化版,2016 年 2 月進入上游。PSP 後來在 1.21 標記棄用、1.25 正式移除,改由 Pod Security Standards(PSS/PSA)接手。OpenShift 4.11 之後兩套並存:SCC 仍是實際做細粒度存取控制、橋接 SELinux 的機制,另有一個 controller 讀取 namespace 內 ServiceAccount 的 SCC 權限,把它翻譯成對應的 PSS profile 標籤貼回 namespace(SCC 是輸入,PSS 標籤是輸出)。
(棄用跟移除是兩件事,中間隔了四個版本;PSP 走完這段路之後消失,它的源頭 SCC 反而留了下來。)
| SCC 名稱 | 限制等級 | 核心行為規範 |
|---|---|---|
| restricted-v2 | 極高(這個叢集預設套用的就是這個) | 強制分配動態 UID(禁止 Root)、禁止 HostPath、限制 Capabilities |
| restricted-v3 | 極高,方向跟 v2 不同 | UID 區間寫死在 SCC 物件裡(1000–65534,不看 Namespace annotation),另外要求 hostUsers 設為 false |
| anyuid | 中等 | 允許容器以任意 UID(含 Root UID 0)執行,但仍限制宿主機存取 |
| hostmount-anyuid | 較低 | 允許指定 UID,且允許掛載 HostPath 目錄 |
| privileged | 完全開放(最高風險) | 移除所有容器安全限制,具備等同宿主機 Root 的完全掌控能力 |
「核心行為規範」這欄不是形容詞,每一項都對得上 SCC 物件本體的欄位(HOSTDIR 是 allowHostDirVolumePlugin,HOSTNET 是 allowHostNetwork,PRIV 是 allowPrivilegedContainer):
NAME HOSTDIR HOSTNET PRIV RUNASUSER
anyuid false false false RunAsAny
hostmount-anyuid true false false RunAsAny
privileged true true true RunAsAny
anyuid 的 HOSTDIR/HOSTNET 都是 false,所以它雖然放行 Root UID,宿主機那一側仍然是關的;hostmount-anyuid 的 HOSTDIR=true 才真的開了 HostPath;privileged 三項全 true。
順帶一提,oc get scc a,b,c 這種逗號分隔多個資源名稱的寫法,在這個版本的 oc 上不會被拆開處理:
PS> & $OC get scc anyuid,privileged,hostmount-anyuid -o custom-columns=NAME:.metadata.name,PRIV:.allowPrivilegedContainer
Error from server (NotFound): securitycontextconstraints.security.openshift.io "anyuid,privileged,hostmount-anyuid" not found
整串被當成一個資源名稱去查,所以回報 NotFound。要查多個就用迴圈逐一查。
restricted-v2 跟 restricted-v3 在這個叢集上都對一般 ServiceAccount 開放授權,查兩者的 ClusterRoleBinding 可以確認 subject 完全一樣:
& $OC --kubeconfig $kubeconfig get clusterrolebinding system:openshift:scc:restricted-v2 -o jsonpath='{.subjects}'
[{"apiGroup":"rbac.authorization.k8s.io","kind":"Group","name":"system:authenticated"}]
& $OC --kubeconfig $kubeconfig get clusterrolebinding system:openshift:scc:restricted-v3 -o jsonpath='{.subjects}'
[{"apiGroup":"rbac.authorization.k8s.io","kind":"Group","name":"system:authenticated"}]
兩條輸出一字不差,subject 都是 Group system:authenticated。每一個通過驗證的身分(不論使用者還是 ServiceAccount)預設都落在這個 Group 裡,再由這個 Group 透過 ClusterRoleBinding 拿到使用權。
實際效果等同於「任何新建立的 Namespace 與 ServiceAccount 都自動套用 restricted-v2」,但機制上是 Group 授權,跟後面〈舊版觀念坑〉那節會看到的 add-scc-to-user 是同一套 RBAC 邏輯。
至於為什麼兩者都開放、最後套用的卻是 restricted-v2,答案在下一節的排序規則。Pod 上的 annotation 可以直接看到最終結果:
& $OC get pod <pod-name> -n <namespace> -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
這也就是為什麼 Nginx、Nexus、Elasticsearch 這類官方 Image(預設常要求 runAsUser: 0 或指定特定 UID)部署到 OpenShift 會直接被 Admission Controller 攔下來。
Pod 發起建立請求時,OpenShift Admission Controller 分兩段進行 SCC 檢核:
securityContext,取第一個能通過驗證的套用。全部比對失敗,請求直接被拒絕,Pod 狀態進入 FailedCreate。第二步的「依序」是關鍵,而且順序不是照限制程度排的。官方文件寫的排序規則有三層:先看 priority 由大到小(未設定視為最低),priority 相同時從最嚴格排到最寬鬆,兩者都相同時才按名稱排序。
先看第一層:
& $OC --kubeconfig $kubeconfig get scc -o custom-columns=NAME:.metadata.name,PRIORITY:.priority
NAME PRIORITY
anyuid 10
hostaccess <nil>
hostmount-anyuid <nil>
hostmount-anyuid-v2 <nil>
hostnetwork <nil>
hostnetwork-v2 <nil>
hostpath-provisioner <nil>
machine-api-termination-handler <nil>
nested-container <nil>
nonroot <nil>
nonroot-v2 <nil>
pipelines-scc 10
privileged <nil>
restricted <nil>
restricted-v2 <nil>
restricted-v3 <nil>
這張完整清單順便回答了兩個問題。
第一,帶 priority 的不只 anyuid,pipelines-scc 也是 10。這兩個同分,所以它們之間的先後就得走第二層規則比限制程度——排序規則不是抽象的教科書條文,這個叢集上就有現成案例。
第二,restricted-v2 跟 restricted-v3 都是 <nil>,所以一旦某個身分同時具備 anyuid 與 restricted-v2 的使用權,最終套用的一定是 anyuid,跟兩者誰比較嚴格無關。這件事等一下會在一個很不直覺的地方咬人。
以 developer 身份在測試用 Namespace 建立一個要求以 root(UID 0)啟動的 Deployment,觸發這個檢核流程:
# scc-test-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: scc-test
namespace: scc-demo
spec:
replicas: 1
selector:
matchLabels: { app: scc-test }
template:
metadata:
labels: { app: scc-test }
spec:
containers:
- name: scc-test
image: nginx:latest
securityContext:
runAsUser: 0
& $OC login -u developer -p developer https://api.crc.testing:6443 --insecure-skip-tls-verify=true
& $OC new-project scc-demo
& $OC apply -f scc-test-deploy.yaml
& $OC get events -n scc-demo
oc get events 印出 ReplicaSet 建立 Pod 失敗的完整訊息,這是 SCC 真正在擋什麼的第一手畫面:
LAST SEEN TYPE REASON OBJECT MESSAGE
2s Warning FailedCreate replicaset/scc-test-84f6f95b4c Error creating: pods "scc-test-84f6f95b4c-" is forbidden: unable to validate against any security context constraint: [provider "anyuid": Forbidden: not usable by user or serviceaccount, provider "pipelines-scc": Forbidden: not usable by user or serviceaccount, provider restricted-v2: .containers[0].runAsUser: Invalid value: 0: must be in the ranges: [1000740000, 1000749999], provider restricted-v3: .spec.securityContext.hostUsers: Invalid value: null: Host Users must be set to false, provider restricted-v3: .containers[0].runAsUser: Invalid value: 0: must be in the ranges: [1000, 65534], provider "restricted": Forbidden: not usable by user or serviceaccount, provider "nested-container": Forbidden: not usable by user or serviceaccount, provider "nonroot-v2": Forbidden: not usable by user or serviceaccount, provider "nonroot": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid": Forbidden: not usable by user or serviceaccount, provider "hostmount-anyuid-v2": Forbidden: not usable by user or serviceaccount, provider "machine-api-termination-handler": Forbidden: not usable by user or serviceaccount, provider "hostnetwork-v2": Forbidden: not usable by user or serviceaccount, provider "hostnetwork": Forbidden: not usable by user or serviceaccount, provider "hostaccess": Forbidden: not usable by user or serviceaccount, provider "hostpath-provisioner": Forbidden: not usable by user or serviceaccount, provider "privileged": Forbidden: not usable by user or serviceaccount]
這一長串是 Admission Controller 把每一個候選 SCC 都跑過一輪比對的完整紀錄。
anyuid、privileged、hostmount-anyuid 這幾個本來就允許 UID 0,卻直接被判定「not usable by user or serviceaccount」,代表這個身分根本沒有使用它們的授權,連比對 UID 範圍的機會都沒有。
真正跑到 UID 檢查這一步的只有 restricted-v2 與 restricted-v3。兩者都拒絕,但拒絕的區間不一樣:restricted-v2 是 [1000740000, 1000749999],restricted-v3 是 [1000, 65534]。這不是其中一個算錯,兩者驗證的根本不是同一個 UID。
Pod 從頭到尾沒有真正被建立過,這串訊息就是它在事件裡留下的痕跡——但不是全部。admission 除了寫 Pod 上的 openshift.io/scc,還會產生一組 securitycontextconstraints.admission.openshift.io/ 開頭的稽核 annotation:chosen(最後選中的 SCC)、denied(被判定不可用的清單)、too-restrictive-(各自的失敗原因),以及 reason——最後那個會寫成一句話,例如「X 是最嚴格的、未被拒絕,且被選在 Y 之前」。這些走 API server 的稽核日誌,oc get events 看不到。
為什麼兩個 SCC 算出不同區間
查兩個 SCC 物件本體的
runAsUser欄位:& $OC --kubeconfig $kubeconfig get scc restricted-v2 -o jsonpath='{.runAsUser}' & $OC --kubeconfig $kubeconfig get scc restricted-v3 -o jsonpath='{.runAsUser}'{"type":"MustRunAsRange"} {"type":"MustRunAsRange","uidRangeMax":65534,"uidRangeMin":1000}
restricted-v2的runAsUser沒有帶區間,這種情況下驗證用的區間會回頭讀 Namespace 的sa.scc.uid-rangeannotation,也就是下一節那個大數字區間。restricted-v3的區間則寫死在 SCC 物件裡。這不是設定疏漏。
restricted-v3額外要求 Pod 跑在 Linux 使用者命名空間(user namespace)裡,容器裡看到的 UID 只是命名空間內部的號碼,跟宿主機上的實際 UID 是兩回事,內核會做映射隔離。正因為容器裡的 UID 不會直接對應到宿主機,restricted-v3才能用一個所有 Namespace 共用的傳統區間(1000–65534),不需要像restricted-v2那樣靠每個 Namespace 專屬的大數字區間來避免互撞。
userNamespaceLevel欄位也對得上:& $OC --kubeconfig $kubeconfig get scc restricted-v2 -o jsonpath='{.userNamespaceLevel}' & $OC --kubeconfig $kubeconfig get scc restricted-v3 -o jsonpath='{.userNamespaceLevel}'AllowHostLevel RequirePodLevel
AllowHostLevel是 UID 直接對應宿主機,RequirePodLevel是強制使用者命名空間。restricted-v3物件上的kubernetes.io/description也把這件事寫得很清楚:& $OC --kubeconfig $kubeconfig get scc restricted-v3 -o jsonpath='{.metadata.annotations.kubernetes\.io/description}'restricted-v3 denies access to all host features and requires pods to be run with user namespace (and may not be root within that user namespace), and SELinux context that are allocated to the namespace. This is the most restrictive SCC. On top of the legacy 'restricted' SCC, it also requires to drop ALL capabilities and does not allow privilege escalation binaries. It will also default the seccomp profile to runtime/default if unset, otherwise this seccomp profile is required. On top of the legacy 'restricted-v2' SCC, it requires a pod runs in a user namespace. Because of this, the pod to be any non-root UID within the user namespace (between 1000-65534), and it will still be unprivileged outside of the user namespace.(最後一句 "the pod to be any non-root UID" 少了動詞,原文如此,應該是 "requires the pod to be"。)
至於使用者命名空間在 CRI-O 底層實際怎麼把 1000–65534 映射到宿主機的哪個 UID,我沒有追下去,不在這篇範圍內。
這個測試用的是 nginx:latest。本系列後續一律以 digest 釘死、經內部 Registry 代理,這裡只是借它示範 SCC 攔截。
restricted-v2 強制分配的 UID 不是隨機挑一個數字,而是從該 Namespace 的 openshift.io/sa.scc.uid-range annotation 讀出來的固定區間:
& $OC get namespace scc-demo -o jsonpath='{.metadata.annotations.openshift\.io/sa\.scc\.uid-range}'
1000740000/10000
格式是「起始 UID/區間大小」,代表這個 Namespace 底下的 Pod 會拿到 1000740000 到 1000749999 之間的某個 UID,跟上一節事件訊息裡的區間完全對得上。每個 Namespace 的起始值都不一樣,Pod 實際拿到哪個確切數字,無法預先寫死在 Dockerfile 或 YAML 裡。
要實際看到這件事,得先避開一個很容易誤踩的坑。如果直接用 kubeconfig(也就是 system:admin)建立 Pod,即使 Pod 指定的是一個完全乾淨、沒有任何額外 SCC 授權的 ServiceAccount,套用到的也不會是 restricted-v2。
原因不在 ServiceAccount,在發起請求的那個身分。system:admin 拿的是 cluster-admin 角色,而這個角色的規則是萬用字元:
PS> & $OC get clusterrole cluster-admin -o json | ... rules
[{"apiGroups":["*"],"resources":["*"],"verbs":["*"]},
{"nonResourceURLs":["*"],"verbs":["*"]}]
PS> & $OC auth can-i use scc/anyuid --as=system:admin
yes
PS> & $OC auth can-i use scc/restricted-v2 --as=system:admin
yes
PS> & $OC auth can-i use scc/privileged --as=system:admin
yes
「任何資源、任何動詞」本身就涵蓋了對每一個 SCC 的 use 權限。再加上前面查到的 anyuid priority 是 10、restricted-v2 是 <nil>,結果就是「誰發起這個建立請求」的身分蓋過了「Pod 宣告要用哪個 ServiceAccount」。前面說 priority 會在不直覺的地方咬人,就是這裡。
一個看起來很合理、但查下去不成立的解釋
anyuid這個 SCC 物件本體確實把system:cluster-admins列進了授權名單:PS> & $OC get scc anyuid -o jsonpath='{.groups}' ["system:cluster-admins"]很容易就順著推論成「
system:admin屬於這個 Group,所以拿得到 anyuid」。但查身分本身,這條推論斷了:PS> & $OC auth whoami -o yaml apiVersion: authentication.k8s.io/v1 kind: SelfSubjectReview status: userInfo: extra: authentication.kubernetes.io/credential-id: - X509SHA256=863ff641abff596ea6daf2c53e4193a0c2b06c635692b68933bf852823367e67 groups: - system:authenticated username: system:admin
system:admin的 groups 只有system:authenticated,並不包含system:cluster-admins。(kubeconfig 裡那張憑證的 Subject 是
CN=system:admin, OU=system:masters,看到system:masters很容易以為 Group 從這裡來,但 x509 認證取 Group 是看 O 欄位不是 OU,所以這個system:masters沒有真的生效,跟auth whoami的輸出一致。)真正的路徑是 ClusterRoleBinding:
PS> $crbs.items | Where-Object { $_.subjects.name -eq "system:admin" } name role subjects ---- ---- -------- cluster-admins cluster-admin Group:system:cluster-admins,User:system:admin
system:admin是以 User 身分直接列在 subjects 裡,跟system:cluster-admins這個 Group 是同一條 binding 底下並列的兩個 subject,不是從屬關係。兩者各自拿到cluster-admin,而cluster-admin的萬用字元規則本身就蓋過了所有 SCC 的使用權。也就是說,
anyuid的.groups清單在這個案例裡是一條冗餘路徑,不是因果鏈上的環節。就算把它清空,system:admin一樣能用 anyuid。
要測到 ServiceAccount 真正的邊界,得讓建立 Pod 的請求身分等於那個 ServiceAccount 本身,用 --as 模擬:
# scc-id-test2.yaml
apiVersion: v1
kind: Pod
metadata:
name: scc-id-test2
namespace: scc-demo
spec:
serviceAccountName: scc-clean
containers:
- name: scc-id-test2
image: nginx:latest
command: ["sleep", "3600"]
& $OC --kubeconfig $kubeconfig create sa scc-clean -n scc-demo
& $OC --kubeconfig $kubeconfig adm policy add-role-to-user edit -z scc-clean -n scc-demo
& $OC --kubeconfig $kubeconfig create -f scc-id-test2.yaml --as=system:serviceaccount:scc-demo:scc-clean
add-role-to-user edit 給的只是「能不能建立 Pod」的 RBAC 權限,scc-clean 除此之外沒有拿到任何 SCC 使用權。這樣量到的就是這個 SA 自己的邊界,不會被 cluster-admin 蓋過去。
叢集會把「這次是照誰的身分核准的」寫在 Pod 上
Pod 有一個
security.openshift.io/validated-scc-subject-typeannotation,四個樣本擺在一起,規律很清楚:nexus-nexus3-0 (StatefulSet controller 建的) → serviceaccount gid-origin (--as 模擬 SA 建的) → user kubeconfig-direct (純 kubeconfig 直建,不宣告 SA) → user admin-declares-sa (純 kubeconfig 直建,但宣告 scc-clean) → user它記的不是「Pod 宣告用哪個 SA」——第四個樣本明確宣告了
serviceAccountName: scc-clean,值依然是user。看起來記的是「最後是靠哪一邊的身分過關的」:發出請求的那個身分自己就夠用時記user,不夠、必須回頭看 Pod 宣告的 SA 才過得了時記serviceaccount。這也說明
--as天生產不出serviceaccount:被冒充的身分同時就是 Pod 宣告的 SA,兩邊是同一個,第一關就過了。所以--as量到的 SCC 結果是對的,但它走的路徑跟 controller 那條不完全一樣。真正有意義的是 Nexus 那顆:它記成
serviceaccount,代表 controller 自己的身分不足以過關,admission 是查到nexus-nexus3這個 SA 有anyuid授權才放行的。「controller 不會用高權限身分把 Pod 罩過去」這件事,叢集自己留了紀錄。(這個推論是四個樣本歸納出來的,不是查原始碼得到的規則,換個版本值得重測。)
先確認實際套用的 SCC,再看 id:
PS> & $OC --kubeconfig $kubeconfig get pod scc-id-test2 -n scc-demo -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
restricted-v2
PS> & $OC --kubeconfig $kubeconfig exec scc-id-test2 -n scc-demo -- id
uid=1000790000(1000790000) gid=0(root) groups=0(root),1000790000
annotation 確認是 restricted-v2。這次示範重建過 scc-demo,UID range 換了一輪起始值,跟前面查到的 1000740000 不是同一個數字,正好印證「每個 Namespace 的起始值都不一樣」。
看到上面那行 gid=0(root),很容易收成一句好記的結論:UID 隨機、GID 恆為 0。
這句話有明確的出處,而且不只一個。Red Hat Developer 的〈Adapting Docker and Kubernetes containers to run on Red Hat OpenShift Container Platform〉(Michael Greenberg,2020/10/26)第一節就寫著:雖然 OpenShift 用任意指派的 user ID 執行容器,group ID 必須永遠設為 root group (0)。同一篇在 SecurityContext 那節講得更絕——因為 OpenShift 指派任意 UID 和 GID 零,runAsUser、runAsGroup 這類指令不得出現在 deployment spec 或 Helm chart 裡,啟用會導致部署失敗。〈A Guide to OpenShift and UIDs〉(William Caban Babilonia,2020/07/28)也是同樣的敘述。GitLab 官方 Chart 那條「不要在 chart 裡輸出 securityContext.runAs{User,Group}」的規則,來源正是前面那篇。
OpenShift 官方的 Creating Images 指南用字保守得多:容器使用者「是 root group 的成員」,所以讀寫得到那些檔案——講的是群組成員資格,不是主要 GID 一定等於 0。
這兩篇部落格都寫於 2020 年,而這個叢集預設套用的 restricted-v2 是 OpenShift 4.11(2022)才引入的。在沒有人指定 runAsGroup 的預設情境下,上面的描述完全正確。所以我想測的不是它們對不對,是它們的成立條件在哪裡:在 restricted-v2 底下明確要一個非 0 的 GID,SCC 會不會攔?
# gid-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: gid-test
namespace: gid-demo
spec:
serviceAccountName: scc-clean
containers:
- name: gid-test
image: nginx:latest
command: ["sleep", "3600"]
securityContext:
runAsGroup: 1001
PS> & $OC create -f gid-test.yaml --as=system:serviceaccount:gid-demo:scc-clean
pod/gid-test created
PS> & $OC get pod gid-test -n gid-demo
NAME READY STATUS RESTARTS AGE
gid-test 1/1 Running 0 16s
PS> & $OC get pod gid-test -n gid-demo -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
restricted-v2
PS> & $OC exec gid-test -n gid-demo -- id
uid=1000700000(1000700000) gid=1001(1001) groups=1001(1001),1000700000
Pod 建起來了,annotation 一樣是 restricted-v2,而 GID 就是我要的 1001。SCC 沒有攔,也沒有把它改回 0。
比 GID 本身更值得注意的是 groups 那一欄。這次是 1001(1001),1000700000,對照前面沒指定 runAsGroup 那次是 groups=0(root),1000790000——0 不在裡面了。也就是說這個容器連 root group 的成員都不是。
再查 SCC 物件本體,可以看到它管的範圍到哪裡:
PS> & $OC get scc restricted-v2 -o json | ConvertFrom-Json | Select-Object runAsUser, fsGroup, supplementalGroups, seLinuxContext | ConvertTo-Json
{
"runAsUser": {"type": "MustRunAsRange"},
"fsGroup": {"type": "MustRunAs"},
"supplementalGroups": {"type": "RunAsAny"},
"seLinuxContext": {"type": "MustRunAs"}
}
四個策略欄位裡,runAsUser 管 UID,fsGroup 管掛載卷的群組,supplementalGroups 是 RunAsAny 等於不限制,seLinuxContext 管 SELinux 標籤。沒有一個欄位對應 runAsGroup,這跟上面「指定了就照著跑」的觀測結果一致。
所以第一件能確定的事是:restricted-v2 會強制換掉 UID,但 Pod 明確指定的主要 GID,它不擋也不改。
那沒有指定 runAsGroup 時的那個 0 是誰給的?nginx:latest 沒宣告 USER,預設就是 0:0,所以「映像帶的」跟「平台補的」在那個樣本上分不出來。換一個自己宣告了非 0 GID 的映像就能分辨——Nexus 官方映像正好是(Day 6 會量到它在別的 SCC 底下跑出 gid=200)。
這次讓它跑在 restricted-v2 底下,而且完全不設 securityContext:
PS> & $OC get pod nexus-nexus3-0 -n nexus -o jsonpath='{.spec.containers[0].image}'
docker.io/sonatype/nexus3:3.94.0-ubi
PS> & $OC get pod gid-origin -n gid-demo -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
restricted-v2
PS> & $OC exec gid-origin -n gid-demo -- id
uid=1000740000(1000740000) gid=0(root) groups=0(root),1000740000
映像自己宣告的是 200,跑出來卻是 gid=0。規格說 runAsGroup 未設定時用 runtime 預設值,這一組就是在量那個預設值是多少——答案是 0,而且它會蓋過映像自己宣告的 200。
再看 admission 實際往 Pod 物件裡寫了什麼:
PS> & $OC get pod gid-origin -n gid-demo -o jsonpath='{.spec.securityContext}'
{"fsGroup":1000740000,"seLinuxOptions":{"level":"s0:c27,c19"},"seccompProfile":{"type":"RuntimeDefault"}}
PS> & $OC get pod gid-origin -n gid-demo -o jsonpath='{.spec.containers[0].securityContext}'
{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"runAsNonRoot":true,"runAsUser":1000740000}
被寫進去的是 runAsUser、fsGroup、SELinux、seccomp,沒有 runAsGroup。SCC 從頭到尾沒有對主要 GID 下過任何指令。
把三個觀測點放在一起,圖像就完整了:
| 情境 | runAsUser | 量到的 GID |
|---|---|---|
restricted-v2,不指定 |
被 SCC 換成隨機大數字 | 0 |
restricted-v2,指定 runAsGroup: 1001 |
被 SCC 換成隨機大數字 | 1001 |
anyuid,不指定(Day 6 的 Nexus) |
不被換,維持映像宣告的 200 | 200 |
決定 GID 的不是 SCC 有沒有管它,而是 UID 有沒有被換掉。UID 一旦被換成一個映像裡不存在的數字,映像帶的那組身分資訊就跟著失效,GID 落到 0;UID 沒被換(anyuid),映像的值就留著;Pod 自己講明了要哪個 GID,那就是哪個。
上面那條規則是三個樣本歸納出來的。要問「為什麼會這樣」,Kubernetes 對 runAsUser 跟 runAsGroup 兩個欄位的 API 定義就寫著線索——兩者的預設來源根本不同:
runAsUser:執行容器進程 entrypoint 的 UID,未指定時預設為映像 metadata 裡指定的使用者。runAsGroup:執行容器進程 entrypoint 的 GID,未設定時使用 runtime 預設值。只有 UID 會回頭讀映像 metadata,GID 明文交給 runtime。所以在 Kubernetes 這一層查不到「那個 0 從哪來」不是我沒追下去——規格本身就把它交出去了,答案在 CRI-O/runc,不在 K8s 的 API 定義裡。上面那張表是觀測結果,機制層級的確切規則不在這篇範圍內,但至少可以確定它不歸 Kubernetes 管。
順帶說明 groups 那一欄為什麼會變:它是「主要 GID + supplementary groups」。沒指定 runAsGroup 時主要 GID 是 0,所以 0 出現在裡面;指定成 1001 之後主要 GID 就是 1001,0 自然消失——SCC 從來沒有把 0 加進 supplementary groups,它只是主要 GID 的副本。
檔案權限不能建立在「容器一定跑在某個特定 UID」的假設上,chmod 777 也不該用。做法照 OpenShift 官方 Creating Images 指南:
chgrp -R 0 /some/directory && chmod -R g=u /some/directory
把檔案的群組設成 root group,讓群組權限等同擁有者權限。
這個設計成立的條件是:沒有人在 Pod spec 或 Helm values 裡另外指定 runAsGroup。前面那次測試裡,設了 runAsGroup: 1001 之後 groups 連 0 都沒有,chgrp 0 布置的權限整組讀不到。而 SCC 不攔、Pod 照樣 Running,只有應用程式自己報 Permission denied——跟 Day 4 那類「不報錯卻做錯事」是同一種。
而這正是本節開頭那兩篇文章真正要防的東西。Red Hat Developer 那篇的建議是「runAsUser、runAsGroup 不得出現在 deployment spec 或 Helm chart 裡」,GitLab Chart 的相容性規則直接沿用了它。建議本身完全正確——只是它給的理由(「啟用會導致部署失敗」)跟這個叢集上量到的不一樣:部署不會失敗,失敗的是後面那套群組權限布局。
範圍提醒:把某個 SCC(例如 anyuid)授權給 default 這個 ServiceAccount 之後,同一個 Namespace 底下所有沒指定 serviceAccountName 的 Pod 都會一併拿到那個資格,不只是當初要測的那一個。這也是為什麼「機密操作集中化」要落到具體的 ServiceAccount,而不是隨手綁在 default 上——下一節重跑實測時就改綁具名的 scc-app,授權後查 RoleBinding,subject 只有 scc-app,同 Namespace 的 default 完全沒被牽動。
要賦予指定 Namespace 下的 ServiceAccount 更高權限的 SCC,用 oc adm policy add-scc-to-user:
& $OC --kubeconfig $kubeconfig adm policy add-scc-to-user <scc-name> -z <serviceaccount-name> -n <namespace>
常見的建議時序是:Helm 安裝前先完成 SCC 授權。理由通常這樣講——如果等 Helm 自己建立 ServiceAccount 才發起 Pod 建立,Pod 會在那一瞬間因為比對不到 SCC 權限被直接拒絕,預先綁定才能「消弭時間差」。
這說法乍聽合理,但前面的 FailedCreate 測試剛好留下可以拿來檢驗的素材。那次的 FailedCreate 掛在 replicaset/scc-test-84f6f95b4c 上,而 ReplicaSet controller 會持續重試。
如果重試機制本來就會自己把 Pod 救回來,那「預先授權」買到的就不是「避免部署失敗」,而是「避免一段時間的噪音與延遲」。這兩件事的性質差很多。
(以下的第一次測量是重建 scc-demo 之前那一輪的紀錄,所以 UID range 是 1000740000;第二次重跑則在重建之後。兩次的 Namespace 不是同一個世代。)
直接拿那個已經卡在 FailedCreate 的部署來測,不刪除、不重建,只補一句授權:
PS> Get-Date
2026年8月3日 下午 10:11:40
PS> & $OC --kubeconfig $kubeconfig adm policy add-scc-to-user anyuid -z default -n scc-demo
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "default"
用輪詢確認狀態:
$grantTime = Get-Date
$maxWait = 90
$elapsed = 0
do {
Start-Sleep -Seconds 5
$elapsed += 5
$pods = @((& $OC --kubeconfig $kubeconfig get pods -n scc-demo -o json | ConvertFrom-Json).items)
$pod = $pods | Select-Object -First 1
$containers = @($pod.status.containerStatuses)
$allReady = $containers.Count -gt 0 -and -not ($containers | Where-Object { -not $_.ready })
} while (-not ($pod.status.phase -eq "Running" -and $allReady) -and $elapsed -lt $maxWait)
$readyTime = Get-Date
($readyTime - $grantTime).TotalSeconds
這段輪詢也踩在 Day 4 那個坑邊上:
containerStatuses是陣列,直接寫$pod.status.containerStatuses.ready拿來做布林判斷會出事。PowerShell 判斷陣列的布林值只看「是不是空陣列」,不看裡面裝的是$true還是$false,所以就算容器全部 not ready,member enumeration 出來的仍是非空陣列,一樣被判定為真。上面改用Where-Object明確篩出「有沒有任何一個容器不是 ready」,.items也包一層@()再取第一筆。這次測試的 Namespace 剛好只有一個 Pod、一個容器,就算寫錯也不影響結果,但這種巧合不該拿來當寫法的靠山。
輪詢量到授權後 65.1 秒 Pod 自己轉成 Running,這是 $grantTime 與 $readyTime 兩個絕對時間相減得到的。
這個 65.1 秒是上界不是精確值。迴圈每 5 秒才檢查一次,偵測到 Ready 的當下,真正轉成 Ready 的時刻可能落在前面那整個 5 秒視窗裡的任何一點,所以真實時間介於 60.1 到 65.1 秒之間。誤差來自輪詢間隔,比時間戳記的秒級捨入大得多。
接著看事件時序。這裡要先提醒一件事:oc get events 的 LAST SEEN 欄位語意是「距離執行這條查詢多久」,不是「距離授權多久」。
下面這張表已經把它換算過,欄位名也跟著改掉了,不是原始輸出。換算方式是把每一列的 LAST SEEN 秒數對齊到 65.1 秒這個絕對錨點反推,所以它繼承了同樣最多 5 秒的誤差,抓的是量級不是精確時序:
距授權 TYPE REASON OBJECT MESSAGE
~5s Normal SuccessfulCreate replicaset/scc-test-84f6f95b4c Created pod: scc-test-84f6f95b4c-xmslk
~6s Normal Pulling pod/scc-test-84f6f95b4c-xmslk Pulling image "nginx:latest"
~61s Normal Pulled pod/scc-test-84f6f95b4c-xmslk Successfully pulled image "nginx:latest" in 55.831s (55.831s including waiting). Image size: 164965213 bytes.
~61s Normal Created pod/scc-test-84f6f95b4c-xmslk Created container: scc-test
~61s Normal Started pod/scc-test-84f6f95b4c-xmslk Started container scc-test
照著這篇跑一次,你的 LAST SEEN 不會跟這張表一樣,因為原始輸出的每一列是「舊了多久」,會隨查詢時間點浮動。
到這裡為止,「重試大概花 5 秒」是拿 65.1 減掉 55.831 推出來的,不是量到的。既然節點上已經有這個映像,重跑一次就能把它從推論變成直接測量。這次順便改綁具名的 scc-app,而不是 default:
PS> & $OC --kubeconfig $kubeconfig adm policy add-scc-to-user anyuid -z scc-app -n scc-demo
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "scc-app"
第二次把輪詢間隔縮到 2 秒(誤差跟著降到 ±2 秒),量到 12.97 秒。比預期的「10 秒內」高,但事件紀錄說明了差距從哪來:
9s Normal Pulling pod/scc-test-xxxxx Pulling image "nginx:latest"
3s Normal Pulled pod/scc-test-xxxxx Successfully pulled image "nginx:latest" in 6.517s (6.517s including waiting). Image size: 164965213 bytes.
nginx:latest 沒指定 imagePullPolicy,預設落在 Always,kubelet 每次都會去 registry 解析 digest,命中快取才省下傳輸——這 6.5 秒是解析往返的成本,換成固定標籤或 digest 就不會出現,不是完全省略,只是從冷拉取的 55.8 秒縮短到 6.5 秒。
扣掉這 6.5 秒,剩下約 6.5 秒是「授權生效到容器實際啟動」。要注意這跟第一次量的不是完全同一個點:第一次的 5 秒是授權到 SuccessfulCreate,也就是 Pod 物件被建出來;第二次的 6.5 秒還多包了容器建立與啟動,所以略大一點是合理的。兩次的量級一致,而且第二次是兩段時間分開測到的,不是拿單一總數反推。
結論就在這個拆解上。從授權生效到 Pod 真正動起來,兩次都是個位數秒,這是 ReplicaSet controller 下一輪調諧循環的速度,不是什麼需要「預先綁定來消弭」的長時間差。真正拖時間的是映像拉取:冷拉取 55.8 秒,就算已經快取也還要 6.5 秒,而這段完全跟 SCC 授權的時序無關,預先綁定一樣要等。
所以「預先授權」買到的不是「避免部署失敗」,ReplicaSet 本來就會自己救回來,買到的是「少幾秒 FailedCreate 噪音跟一小段不必要的等待」。這跟「不預先綁定就會被直接拒絕、必須靠預先綁定才能部署成功」的原始說法,是兩回事。
上面測的是 Deployment/ReplicaSet 這種有 controller 持續調諧的資源。我原本以為一次性的 Job 該歸在「FailedCreate 就是真的失敗」那一邊,實測發現不是。
用同樣的手法讓一個 Job 卡在 FailedCreate,oc get events 的表格模式從頭到尾只顯示一行,看起來像只發生一次。但改查 Event 物件本體的 count 欄位,短短 3 分鐘內就從個位數長到 10,而且還在繼續往上加。
會被表格誤導,是因為同一則訊息的重複事件會被聚合成同一個 Event 物件,只更新 count 跟 lastTimestamp,不會每次重試都另外開一行:
& $OC --kubeconfig $kubeconfig get events -n job-test -o json | ConvertFrom-Json |
Select-Object -ExpandProperty items |
ForEach-Object { "count: $($_.count) first: $($_.firstTimestamp) last: $($_.lastTimestamp)" }
count: 9 first: 2026-08-03T18:56:19Z last: 2026-08-03T18:59:22Z
另外提醒一下,oc describe job 印出的 Age 欄位在 CRC 上不可靠(VM 睡眠喚醒後跟宿主機時鐘會有偏移,我這次看到的是荒謬的「17h」),要看 firstTimestamp 跟 lastTimestamp 相減才準。
容易搞混的地方在 backoffLimit。這個欄位算的是「Pod 建立成功、之後執行失敗」的次數,跟「Pod 連建立都被 admission 擋下」是兩件事。後者不計入 backoffLimit,是 Job controller 自己的建立重試佇列在處理,而且不會停止。整個過程中 oc get pods 都是 No resources found,每一次重試都在 Pod 真正誕生之前就被擋掉。
(上面那筆 count: 9 只是單一時間點的快照,證得了「重試沒停」,證不了間隔有沒有隨次數拉長。要看趨勢得留著 Namespace 多取幾次,我這次沒有留。)
所以真正沒有這層保護、FailedCreate 就是終局的,只剩不透過任何 controller 直接建立的裸 Pod,也就是手動 oc run 出來、沒有 owner reference 的那種。那種情境下預先授權才是真的決定成敗,不只是減少噪音。
Helm chart 建出來的資源通常是 Deployment/StatefulSet,跟這裡測的機制一樣有 controller 重試。不過這是從 controller 的通用行為類推的,我沒有直接拿 Helm chart 實測過這個推論(明天 Day 6 會補一個 StatefulSet 的版本)。
授權時序的建議本身沒有錯,先做永遠比事後補好。這次實測推翻的是背後的原理,不是這件事值不值得做。
OpenShift 底層把 SCC 授權實作成標準的 RoleBinding/ClusterRoleBinding,名稱格式是 system:openshift:scc:<scc-name>。而 RBAC 的 API 規格只要求 roleRef 必須可解析,對 subjects 沒有存在性檢查——這一點在 upstream 被當成 issue 報過(#129268),行為維持至今。
對一個刻意不建立的 ServiceAccount 名稱直接執行 add-scc-to-user,三條指令並排看就很清楚:
PS> & $OC adm policy add-scc-to-user anyuid -z sa-does-not-exist -n gid-demo
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "sa-does-not-exist"
PS> $b = & $OC get rolebinding system:openshift:scc:anyuid -n gid-demo -o json | ConvertFrom-Json
PS> $b.subjects
{
"kind": "ServiceAccount",
"name": "sa-does-not-exist",
"namespace": "gid-demo"
}
PS> & $OC get sa sa-does-not-exist -n gid-demo
Error from server (NotFound): serviceaccounts "sa-does-not-exist" not found
綁定指令成功,RoleBinding 的 subjects 裡查得到這個名字(kind 是 ServiceAccount),但這個 SA 物件本身查無此人。這就是為什麼可以在 Helm 還沒建立 SA 之前就先送出綁定指令:
$ns = "nexus"
$sa = "nexus-nexus3"
# 1. 確保 Namespace 存在(用 $LASTEXITCODE 判斷,不是靠有沒有輸出)
# 2>$null 加 Out-Null 不會吃掉 oc 的退出碼:不存在回 1,存在回 0
& $OC --kubeconfig $kubeconfig get namespace $ns 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
& $OC --kubeconfig $kubeconfig create namespace $ns
}
# 2. 預先綁定 SCC 授權(指向即將由 Helm 安裝產生的 SA 名稱)
& $OC --kubeconfig $kubeconfig adm policy add-scc-to-user anyuid -z $sa -n $ns
# 3. 執行 Helm 安裝
helm install nexus stevehipwell/nexus3 -n $ns -f values.yaml
本系列後續導入 Nexus 時會用到同一套模式,目的同樣是把 FailedCreate 的噪音窗口降到最低。
OpenShift 3.x 或早期教學文件常教你檢視 SCC 物件本體來確認授權:
# 這條路在 OpenShift 4.x 看不到完整的綁定關係
& $OC describe scc privileged
在 4.x 中,oc adm policy add-scc-to-user 的底層實現已經轉向標準的 RoleBinding/ClusterRoleBinding。麻煩的是,直接查 SCC 本體的 .users 欄位並不是空的:
PS> & $OC --kubeconfig $kubeconfig get scc privileged -o jsonpath='{.users}'
["system:admin","system:serviceaccount:openshift-infra:build-controller"]
system:admin 確實列在裡面,如果只看這個欄位,很容易誤以為所有 SCC 授權都會記在這裡。但 add-scc-to-user 動態授權出來的記錄不會寫回這個欄位,而是記在特定 Namespace 底下的 RoleBinding 物件裡。.users 列出的是系統預設直接賦予的帳號,不是完整清單。
(前面 anyuid 的 .groups 那個案例是同一個道理的另一面:這些欄位有值,不代表它是實際生效的授權路徑。)
要驗證某個 Namespace 內的 ServiceAccount 是否取得 SCC 授權,查該 Namespace 下帶 scc: 字樣的 RoleBinding。這裡用 -o json 搭配物件屬性存取,避開 Day 4 講過的純文字比對雷區:
$bindings = & $OC --kubeconfig $kubeconfig get rolebinding -n scc-demo -o json | ConvertFrom-Json
$bindings.items | Where-Object { $_.roleRef.name -like "system:openshift:scc:*" } | Select-Object -ExpandProperty roleRef
授權前這個查詢完全沒有輸出,授權後:
PS> $bindings.items | Where-Object { $_.roleRef.name -like "system:openshift:scc:*" } | Select-Object -ExpandProperty roleRef | Select-Object name,kind
name kind
---- ----
system:openshift:scc:anyuid ClusterRole
查得到 system:openshift:scc:<scc-name> 就代表綁定完成;查不到就是授權還沒生效,不用回頭懷疑是不是打錯 SCC 名稱。
如果要列出 RoleBinding 自己的名稱,注意屬性路徑在 .metadata.name,直接寫 Select-Object name 會拿到空白欄位。這跟 Day 4 那個母題一樣,是物件結構沒摸清楚,不是資料不存在。
如果授權後舊有 Pod 仍未套用新權限,觸發一次重啟:
& $OC --kubeconfig $kubeconfig rollout restart deployment/<n> -n <namespace>
scc-demo 只是臨時測試專案,操作完整個刪掉就好,不需要逐一清理裡面的 Deployment 或 RoleBinding:
& $OC delete project scc-demo
-o json 走物件屬性,不要對純文字做關鍵字比對),不要只看 SCC 本體的 .users 或 .groups——那些欄位有值不代表它是實際生效的授權路徑。sa.scc.uid-range annotation,不要假設是某個固定數字。chgrp -R 0 搭配 chmod -R g=u(OpenShift 官方 Creating Images 指南的寫法),不要假設容器一定跑在某個固定 UID 之下。同時確認兩件事:沒有人在 Pod spec 或 Helm values 裡設定 runAsGroup(設了 SCC 不會攔,groups 裡連 0 都沒有),以及這個 workload 走的不是 anyuid(UID 沒被換掉時,GID 是映像自己的值,也不是 0)。--as 模擬該 SA,不要直接用 kubeadmin 建 Pod,否則量到的是 cluster-admin 的權限。FailedCreate 不等於部署失敗(Job 靠的是建立重試佇列,不是 backoffLimit);只有不透過 controller、直接建立的裸 Pod 沒有這層保護,預先授權在那裡才真正決定成敗。今天三節從不同角度講同一件事。
SCC 沒有把任何資訊藏起來,反而給得比你想像的多。被拒絕的理由、UID 的合法區間、可以用的 SCC 清單,全部一次印在事件裡;除此之外,admission 還會把「最後選了哪個 SCC」「為什麼選它而不是下一個候選」寫成稽核 annotation。真正擋住排查進度的,是那一長串 not usable 讀起來太累,以及有些東西被放在沒人會去看的地方。
時序那段實測推翻了一個聽起來很合理的直覺。ReplicaSet 靠 controller 調諧把 Pod 救回來,Job 走的是另一條路——建立重試佇列,不計入 backoffLimit、也不會停。兩條路徑不同,結論一樣:「不預先綁定 SCC 就會部署失敗」並不成立,預先綁定買到的是少幾秒噪音,不是成敗本身。
而 GID 那段是對一句好記的話做邊界測試。「UID 隨機、GID 恆為 0」在預設情況下描述得沒錯,但 SCC 從頭到尾沒有對主要 GID 下過任何指令——三個樣本擺在一起才看得出來,真正決定 GID 的是 UID 有沒有被換掉。UID 被換成映像裡不存在的數字,GID 落到 0;UID 沒被換,映像的值就留著;Pod 自己指定了,那就是指定的值。
所以那個 0 是連帶結果,不是保證。分不清這件事,就會寫出一份在別人環境裡失效的 Dockerfile。這是今天最該帶走的一條。
有了帳號權限跟 SCC 這兩條線,接下來要處理讓服務真正能對外運作的另一層限制。明天(Day 6)碰 Helm 在 OpenShift 上的關卡:Chart 的相容性,以及 SCC 授權在 Helm 場景下怎麼接。
參考文件
OpenShift
chosen/denied/too-restrictive-*/reason)與 validated-scc-subject-type 的產生位置,對應〈機制剖析:這串清單怎麼讀〉chgrp -R 0 + chmod -R g=u 的出處,對應〈那個 gid=0 的邊界〉Kubernetes
validated-scc-subject-type 觀察restricted-v3 要求 hostUsers: false 的來源,對應〈為什麼兩個 SCC 算出不同區間〉backoffLimit 計算的是 Failed 階段的 Pod,對應〈那 Job 呢〉