今日目的:在 CRC 上部署 Sealed Secrets Controller,並以三項判定確認「密文可以進版控」這件事真的成立。
先備知識:已能以oc連上 CRC 叢集,並具備 cluster-admin 權限用於初始設定。
| 直覺作法 | 實測結果 |
|---|---|
chart 有寫死的 UID,授 anyuid 就好 |
授權生效,Pod 依然建不出來。anyuid 與 restricted-v2 各滿足一半 |
helm template 看渲染結果 |
看不到叢集,OpenShift 偵測不成立,判斷會錯 |
helm uninstall 之後就乾淨了 |
CRD 留著,下一輪的檢核變成恆真 |
| Pod 沒起來就查 Pod | SCC 在准入階段拒絕時,Pod 物件從未建立,查不到任何東西 |
| 解得開就代表機制正確 | 只有正證無法排除「不靠這個機制也會成立」 |
| 項目 | 版本 |
|---|---|
| CRC / OpenShift | 2.61.0 / 4.21.14 |
| Chart / app | sealed-secrets 2.19.1 / 0.38.4 |
| kubeseal | 0.38.4 |
| helm | v4.2.0(非 v3,兩者在 CRD 處理與 --dry-run=server 上有差異) |
| PowerShell | 5.1(Windows 內建) |
快樂路徑實跑五輪,以下輸出取自實測。換版本或換機器不保證重現。
Sealed Secrets 的用法是:用公鑰把 Secret 加密成 SealedSecret,密文進版控,Controller 在叢集內用私鑰解回 Secret。要讓這個用法站得住,得證三件事:
第二項存在的理由是:只有第一項的話,無法排除「綁定沒生效,只是恰好一致」。第三項的理由是:私鑰在 Controller 首次啟動時於叢集內生成,叢集重建就會產生新的金鑰對,進了版控的密文會全數作廢。
判定二與判定三各自配一個反證。反證不是形式,它排除的是具體的替代解釋——「不靠這個機制也會成立」。
$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Select-Object -First 1).FullName
$kubeconfig = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"
$apiServer = "https://api.crc.testing:6443"
$ssNs = "sealed-secrets"
$ssRelease = "sealed-secrets"
$ssDeployerSa = "ss-deployer"
$nsA = "ss-demo-a" # 加密目標
$nsB = "ss-demo-b" # 反證用,刻意不一致
$ssWorkDir = Join-Path "$env:USERPROFILE\gitea-run" ("d10-" + (Get-Date -Format "yyyyMMdd-HHmmss"))
New-Item -ItemType Directory -Path $ssWorkDir -Force | Out-Null
$pubKey = Join-Path $ssWorkDir "sealed-secrets-public.pem"
$toolDir = "$env:USERPROFILE\gitea-run\tools"
New-Item -ItemType Directory -Path $toolDir -Force | Out-Null
$kubeseal = Join-Path $toolDir "kubeseal.exe"
這台機器上有兩支 oc.exe(cache\crc_hyperv_*\ 與 .crc\bin\oc\),上面的搜尋順序固定取 cache 那支,全程用同一支。$pubKey 放在無空白的路徑下,省去每次引用都要加引號。
helm uninstall 不會帶走三樣東西:SCC 授權、CRD、以及節點上的殘留(本 chart 無 PVC,無此項)。
先查 SealedSecret,再刪任何東西。 刪 CRD 會連同該類型的所有密文一起刪掉,刪 namespace 也會帶走裡面的密文——兩者都會讓這個檢查失去意義,所以它排在最前面,而且要真的擋住,只印出來不擋等於沒查。
$ss = & $OC --kubeconfig $kubeconfig get sealedsecret -A --no-headers 2>$null
if ($ss) { $ss; throw "尚有 SealedSecret,繼續會一併刪除,請先確認" }
"既有 SealedSecret = 0"
helm uninstall sealed-secrets -n sealed-secrets --kubeconfig $kubeconfig
& $OC --kubeconfig $kubeconfig adm policy remove-scc-from-user anyuid -z sealed-secrets -n sealed-secrets
"remove-scc exit = $LASTEXITCODE"
& $OC --kubeconfig $kubeconfig delete namespace sealed-secrets ss-demo-a ss-demo-b --ignore-not-found
& $OC --kubeconfig $kubeconfig delete crd sealedsecrets.bitnami.com --ignore-not-found
release "sealed-secrets" uninstalled
未曾授予 anyuid 時回rolebindings ... not found,remove-scc exit = 1,屬正常
CRD 刪除或 not found
adm policy remove-scc-from-user 沒有 --ignore-not-found,不像 delete 系列。把這一步包成腳本時,別用 exit code 判斷這一行的成敗。
Helm 官方文件明載:不支援用 Helm 升級或刪除 CRD,這是為避免誤刪造成資料遺失的設計決定。實測 helm uninstall 之後,ClusterRole 與 ClusterRoleBinding 都刪掉了,CRD 留下,CREATED 仍是上一輪的時間。
後果是第五節的檢核「CRD 存在」會被上一輪的殘留滿足,無論本輪安裝成功與否都通過。清場時另外刪掉,並把 CREATED 時間戳納入預期輸出,那個檢核才是真實量測。
& $OC --kubeconfig $kubeconfig create namespace $ssNs
& $OC --kubeconfig $kubeconfig create sa $ssDeployerSa -n $ssNs
& $OC --kubeconfig $kubeconfig adm policy add-role-to-user edit -z $ssDeployerSa -n $ssNs
$b = & $OC --kubeconfig $kubeconfig get rolebinding -n $ssNs -o json | ConvertFrom-Json
"SCC rolebinding 數量 = $(@($b.items | Where-Object { $_.roleRef.name -like 'system:openshift:scc:*' }).Count)"
SCC rolebinding 數量 = 0
不授權任何 SCC。3.3 會把 chart 調成 restricted-v2 收得下的形狀,第五節與收工時各驗一次。
helm repo add sealed-secrets https://bitnami.github.io/sealed-secrets
helm repo update
helm search repo sealed-secrets/sealed-secrets --versions | Select-Object -First 6
$ssChartVer = "2.19.1"
$ssAppVer = "0.38.4" # 第六節的 kubeseal 取同一版
$ssToken = & $OC --kubeconfig $kubeconfig create token $ssDeployerSa -n $ssNs --duration=8h
& $OC login --token=$ssToken --server=$apiServer --insecure-skip-tls-verify=true
& $OC whoami
& $OC auth can-i create deployment -n $ssNs
& $OC auth can-i create clusterrole
system:serviceaccount:sealed-secrets:ss-deployeryes/no
Controller 需要叢集層級的 ClusterRole——它要 watch 所有 namespace 的 SealedSecret 並寫入 Secret——而部署帳號只有 edit。第二個 can-i 是 no,所以接下來的 helm 指令都要帶 --kubeconfig。不先查的話,會在下一步得到一個 0 bytes 的渲染檔而不自知。
chart 渲染出三個 securityContext 欄位:seccompProfile: RuntimeDefault、fsGroup: 65534、runAsUser: 1001。
seccompProfile: RuntimeDefault |
fsGroup: 65534 / runAsUser: 1001 |
|
|---|---|---|
restricted-v2 |
允許 | 擋下 |
anyuid |
擋下 | 允許 |
兩個 SCC 各滿足一半,沒有任何一個能同時通過。授予 anyuid 之後 Pod 依然建不出來,只是錯誤訊息換了一句:
授權前 provider "anyuid": Forbidden: not usable by user or serviceaccount
provider restricted-v2: .spec.securityContext.fsGroup: Invalid value: [65534]
provider restricted-v2: .containers[0].runAsUser: Invalid value: 1001
授權後 pod.metadata.annotations[seccomp.security.alpha.kubernetes.io/pod]: Forbidden: seccomp may not be set
provider restricted-v2: .spec.securityContext.fsGroup: Invalid value: [65534]
provider restricted-v2: .containers[0].runAsUser: Invalid value: 1001
anyuid 已經不在「not usable」清單裡,改由 seccomp may not be set 表達它的否決意見。授權生效了,是 anyuid 自己拒絕這個 Pod。
anyuid.seccompProfiles = <none> ← 欄位不存在,實測為不允許任何 profile
restricted-v2.seccompProfiles = [runtime/default]
SCC 不是一條由鬆到緊的直線。anyuid 允許任意 UID,卻在 seccomp 上比 restricted-v2 更嚴格——換到「更寬的 SCC」不保證原本能過的欄位還能過。
另外,anyuid 的 priority 是 10,是預設 SCC 中唯一有值的。OpenShift 先按 priority 再按嚴格程度挑選,所以授予它會改變選擇順序,而不只是多一個選項:即使不撞 seccomp,原本走 restricted-v2 的 Pod 也可能改走 anyuid,fsGroup 策略隨之從 MustRunAs 變成 RunAsAny。這是不授它的第二個理由。
正確作法是縮小 chart 的要求,而不是放寬 SCC。這也是 chart README 對 OpenShift 的建議。
helm install $ssRelease sealed-secrets/sealed-secrets -n $ssNs --version $ssChartVer `
--kubeconfig $kubeconfig --dry-run=server |
Out-File (Join-Path $ssWorkDir "rendered-default.yaml")
helm install $ssRelease sealed-secrets/sealed-secrets -n $ssNs --version $ssChartVer `
--kubeconfig $kubeconfig --dry-run=server `
--set podSecurityContext.fsGroup=null `
--set containerSecurityContext.runAsUser=null |
Out-File (Join-Path $ssWorkDir "rendered-fit.yaml")
foreach ($f in "rendered-default.yaml", "rendered-fit.yaml") {
$p = Join-Path $ssWorkDir $f
"$f = $([System.IO.File]::ReadAllBytes($p).Length) bytes"
Select-String -Path $p -Pattern "runAsUser|fsGroup|seccompProfile|runAsNonRoot"
}
rendered-default.yaml = 10736,命中fsGroup: 65534/seccompProfile/runAsNonRoot: true/runAsUser: 1001rendered-fit.yaml = 10683,只剩seccompProfile/runAsNonRoot: true
少的 53 bytes 就是被移除的兩行。seccompProfile 與 runAsNonRoot: true 保留。
上面這個判定必須用線上渲染。helm template 與 helm install --dry-run=client 都不查叢集,.Capabilities.APIVersions 不含 security.openshift.io/v1,Bitnami 系列 chart 的 OpenShift 偵測不成立,渲染結果會與實際部署的不同。
helm install --dry-run=server 必須帶 --kubeconfig。部署帳號只有 edit,讀不到 ClusterRoleBinding,--dry-run=server 會在取得叢集資訊時失敗——而失敗的表現是渲染檔為 0 bytes,不是明顯的錯誤訊息。
helm template --dry-run=server 未測。上游回報該旗標在 helm template 上只影響 lookup 函式,對 .Capabilities 無效,行為可能隨 helm 版本而異。
$t0 = Get-Date
helm install $ssRelease sealed-secrets/sealed-secrets -n $ssNs --version $ssChartVer `
--kubeconfig $kubeconfig `
--set podSecurityContext.fsGroup=null `
--set containerSecurityContext.runAsUser=null
STATUS: deployed,REVISION: 1
$deadline = (Get-Date).AddMinutes(10)
$last = ""
while ($true) {
$raw = & $OC get pod -n $ssNs -o json 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) {
"[{0:HH:mm:ss}] (查詢失敗,重試)" -f (Get-Date)
} else {
$pods = ($raw | ConvertFrom-Json).items
if ($pods.Count -eq 0) {
$snap = "(namespace 內尚無 Pod)"; $allReady = $false
} else {
$snap = ($pods | ForEach-Object {
$r = @($_.status.containerStatuses | Where-Object { $_.ready }).Count
$t = @($_.status.containerStatuses).Count
"$($_.metadata.name) $r/$t $($_.status.phase)"
}) -join " | "
$allReady = -not ($pods | Where-Object {
-not ($_.status.conditions | Where-Object { $_.type -eq 'Ready' -and $_.status -eq 'True' })
})
}
if ($snap -ne $last) { "[{0:HH:mm:ss}] {1}" -f (Get-Date), $snap; $last = $snap }
if ($allReady) { "ALL READY"; break }
}
if ((Get-Date) -gt $deadline) {
# Pod 從未出現時,問題在 admission,不在 Pod
& $OC get deploy,rs -n $ssNs
& $OC get rs -n $ssNs -o json | ConvertFrom-Json |
ForEach-Object { $_.items.status.conditions } | Format-List
throw "等待 Pod 就緒逾時(10 分鐘)"
}
Start-Sleep -Seconds 10
}
"全就緒耗時 = $([int]((Get-Date) - $t0).TotalSeconds) 秒"
0/1 Pending→1/1 Running。首次含拉取映像約 20 秒;映像已快取時實測介於 2–12 秒,不具重現性,勿當基準值引用。
SCC 在准入階段就拒絕,Pod 物件從未建立。所以這種查法會回報「無異常」:
oc get pod -A --field-selector=status.phase!=Running,status.phase!=Succeeded
# → No resources found
而 Deployment 當下正卡在 0/1。任何以 Pod 為對象的查詢都看不到這類故障。
迴圈一直印「(namespace 內尚無 Pod)」就是這個狀況,和拉映像慢的症狀不同——後者會看到 Pod 停在 ContainerCreating。錯誤只在 ReplicaSet 的 status.conditions 上,上面的逾時分支已經把這個查詢寫進去,不必事後才想起要查哪裡。
& $OC get pod -n $ssNs -o custom-columns=NAME:.metadata.name,SCC:.metadata.annotations."openshift\.io/scc"
# 映像無 shell 也無 id,從 Pod spec 讀。三個欄位分屬兩層:
# runAsUser → container 層
# fsGroup → pod 層
# seccompProfile → pod 層
$pod = (& $OC get pod -n $ssNs -o json | ConvertFrom-Json).items[0]
"runAsUser = $($pod.spec.containers[0].securityContext.runAsUser)"
"fsGroup = $($pod.spec.securityContext.fsGroup)"
"seccompProfile = $($pod.spec.securityContext.seccompProfile.type)"
"uid-range = $(& $OC --kubeconfig $kubeconfig get ns $ssNs -o jsonpath='{.metadata.annotations.openshift\.io/sa\.scc\.uid-range}')"
SCC 為
restricted-v2,全程未授任何 SCCrunAsUser與fsGroup皆落在同時印出的uid-range內seccompProfile = RuntimeDefault
3.4 把 runAsUser 與 fsGroup 從 manifest 移除了,但實際 Pod spec 裡它們又出現:
containers[0].securityContext.runAsUser = 1000790000
spec.securityContext.fsGroup = 1000790000
namespace uid-range = 1000790000/10000
spec.securityContext.seccompProfile = RuntimeDefault ← chart 指定值原樣保留
SCC 做的是兩件不同的事:對已指定的欄位做驗證(RuntimeDefault 在允許清單內,放行),對未指定的欄位做填值(從 namespace 區間配發)。
「把欄位拿掉」不是「沒有這個設定」,而是「交給叢集決定」。這正是授 anyuid 為何失敗、移除欄位為何成功的道理。
uid-range 每次重建 namespace 都不同(四輪實測為 1000690000 → 1000730000 → 1000760000 → 1000790000),不可寫死,與 namespace 的 annotation 比對即可。取欄位時注意兩層之分,取錯層會印出空值。
& $OC --kubeconfig $kubeconfig get crd sealedsecrets.bitnami.com `
-o custom-columns=NAME:.metadata.name,CREATED:.metadata.creationTimestamp
& $OC get secret -n $ssNs -l sealedsecrets.bitnami.com/sealed-secrets-key `
-o custom-columns=NAME:.metadata.name,TYPE:.type,CREATED:.metadata.creationTimestamp
CRD 存在,
CREATED為本輪的時間——第二節刪過才有意義
一份kubernetes.io/tls類型的金鑰 Secret
預設每 30 天產生一把新的 sealing key,附加到既有集合。最近產生的那把用於封裝新密文,也是 kubeseal --fetch-cert 會下載的那一把。舊金鑰不會被刪除,舊密文仍解得開。週期由 --key-renew-period 控制,Helm 值為 keyrenewperiod,設 0 停用。
兩個實務後果:第六節匯出的公鑰在 30 天後不再是最新的那把;第八節的備份必須在每次更新後重做,否則叢集在新金鑰產生後、備份前失效,期間封裝的密文救不回來。
$ksUrl = "https://github.com/bitnami/sealed-secrets/releases/download/v$ssAppVer/kubeseal-$ssAppVer-windows-amd64.tar.gz"
$ksTgz = Join-Path $toolDir "kubeseal-$ssAppVer-windows-amd64.tar.gz"
Invoke-WebRequest -Uri $ksUrl -OutFile $ksTgz -UseBasicParsing
tar -xzf $ksTgz -C $toolDir # Windows 內建 tar
& $kubeseal --version
kubeseal version: 0.38.4
專案已從 bitnami-labs 更名為 bitnami。舊網址中 repo 與 release 會 301 轉址,GitHub Pages 不會,全文一律使用 bitnami。解出的 kubeseal.exe 不在 PATH 上,用絕對路徑呼叫。
kubeseal 預設找的 controller 名稱是 sealed-secrets-controller、namespace 是 kube-system,與 chart 的預設安裝都不符,所以每次呼叫都要顯式帶這兩個參數。
$cert = & $kubeseal --fetch-cert `
--controller-name=$ssRelease `
--controller-namespace=$ssNs `
--kubeconfig $kubeconfig | Out-String
[System.IO.File]::WriteAllText($pubKey, $cert, (New-Object System.Text.UTF8Encoding $false))
$pk = [System.IO.File]::ReadAllBytes($pubKey)
"bytes = $($pk.Length); first5 = $(($pk[0..4] | ForEach-Object { $_.ToString('X2') }) -join ' ')"
bytes = 1752;first5 = 2D 2D 2D 2D 2D(-----BEGIN CERTIFICATE-----)
不要用 > 重導向。PowerShell 5.1 的 > 會產生帶 BOM 的檔案,污染 PEM;PowerShell 7 預設無 BOM,不會踩到。first5 若是 EF BB BF 或 FF FE,就是這裡出的問題。
公鑰無解密能力,可以進版控。
用 --dry-run=client 產生明文 YAML 後直接 pipe 給 kubeseal,明文只存在於管線中,不落盤。
& $OC --kubeconfig $kubeconfig create namespace $nsA
& $OC --kubeconfig $kubeconfig create namespace $nsB
$plainUser = "demo-user"
$plainPass = [System.Guid]::NewGuid().ToString()
$sealedPath = Join-Path $ssWorkDir "demo-cred.sealed.yaml"
$sealed = & $OC --kubeconfig $kubeconfig create secret generic demo-cred `
--dry-run=client `
--from-literal=username=$plainUser `
--from-literal=password=$plainPass `
-n $nsA -o yaml |
& $kubeseal --cert $pubKey --format yaml | Out-String
[System.IO.File]::WriteAllText($sealedPath, $sealed, (New-Object System.Text.UTF8Encoding $false))
$sealedText = [System.IO.File]::ReadAllText($sealedPath)
"含明文密碼 = $($sealedText.Contains($plainPass))"
"含明文帳號 = $($sealedText.Contains($plainUser))"
"含 base64 密碼 = $($sealedText.Contains([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainPass))))"
三者皆為
False
第三行是刻意加的:base64 不是加密,只檢查明文不足以說明密文安全。這三項只排除「明文」與「單純 base64」,不構成密碼學層面的驗證。
& $OC --kubeconfig $kubeconfig apply -f $sealedPath
& $OC --kubeconfig $kubeconfig get sealedsecret demo-cred -n $nsA
sealedsecret.bitnami.com/demo-cred created;SYNCED = True
apply 成功只代表 CRD 被接受,不代表解密發生。
get secret 抓得到只證明有東西被寫出來,內容對不對是另一件事。所以另發一次讀取,把解出的值逐 key 與加密前的原文比對。
$deadline = (Get-Date).AddMinutes(2)
while ($true) {
& $OC --kubeconfig $kubeconfig get secret demo-cred -n $nsA -o name 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) { break }
if ((Get-Date) -gt $deadline) {
& $OC --kubeconfig $kubeconfig logs deployment/$ssRelease -n $ssNs --tail=30
throw "等待 Controller 解密逾時"
}
Start-Sleep -Seconds 3
}
$gotUser = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kubeconfig get secret demo-cred -n $nsA -o jsonpath='{.data.username}')))
$gotPass = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kubeconfig get secret demo-cred -n $nsA -o jsonpath='{.data.password}')))
"username match = $($gotUser -eq $plainUser)"
"password match = $($gotPass -eq $plainPass)"
兩個
match皆為True;SealedSecret 的 conditions 為type: Synced/status: True
改掉密文中的 namespace 後 apply,看它解不開。
$crossPath = Join-Path $ssWorkDir "demo-cred.cross.yaml"
$cross = (Get-Content $sealedPath -Raw).Replace("namespace: $nsA", "namespace: $nsB")
[System.IO.File]::WriteAllText($crossPath, $cross, (New-Object System.Text.UTF8Encoding $false))
& $OC --kubeconfig $kubeconfig apply -f $crossPath
Start-Sleep -Seconds 5
& $OC --kubeconfig $kubeconfig get sealedsecret demo-cred -n $nsB
& $OC --kubeconfig $kubeconfig get secret demo-cred -n $nsB
"get secret exit = $LASTEXITCODE"
& $OC --kubeconfig $kubeconfig logs deployment/$ssRelease -n $ssNs --tail=30 | Select-String "decrypt|giving up"
apply被接受(created,exit 0)STATUS含no key could decrypt secret,SYNCED = Falseget secret為NotFound,exit = 1
Controller log 有will retry×5 →Error updating, giving up
三個訊號要一致。只有其中一個成立,表示綁定的行為與預期不同,照實記錄。
Start-Sleep 5 綽綽有餘:實測從第一次失敗到放棄僅 217 毫秒。log 中 (password, username) 與 (username, password) 兩種順序交錯出現,是 map 迭代順序不定,不是兩種不同的錯誤。
這一步排除的替代解釋是:綁定沒生效,只是恰好一致。
私鑰在 Controller 首次啟動時於叢集內生成,叢集重建就會產生新的金鑰對。備份是這個工具最常被提醒的操作要求,而它是否有效可以當場驗,不必真的毀掉叢集。
$keyBackup = Join-Path $ssWorkDir "sealing-keys-backup.yaml"
$kb = & $OC --kubeconfig $kubeconfig get secret -n $ssNs `
-l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml | Out-String
[System.IO.File]::WriteAllText($keyBackup, $kb, (New-Object System.Text.UTF8Encoding $false))
backup bytes = 7109(單把金鑰時);頂層為kind: List,內含一份 Secret,label 值為active
label 不要加 =active。備份檔中該 label 的值正是 active,只寫 label 名稱會撈到全部(含輪替後保留的舊金鑰),加了 =active 就只剩最新那把,舊密文救不回來。這是這一步最容易寫錯的地方。
kind: List 是 oc get -o yaml 撈多筆的固定結構,--recovery-private-key 明確支援該格式。
這個檔案能解開所有的 SealedSecret,不可進版控。公鑰進版控、私鑰進密碼管理器或加密磁碟,兩者不可混淆。本文為求可核對而寫在工作目錄,實務上應移到加密儲存。
正證:憑備份解得開
$recovered = & $kubeseal --recovery-unseal --recovery-private-key $keyBackup -f $sealedPath -o yaml | Out-String
"recovery exit = $LASTEXITCODE"
$rec = $recovered -split "`n"
$recUser = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(($rec | Select-String "^\s+username:").ToString() -split ":\s*")[-1].Trim()))
$recPass = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(($rec | Select-String "^\s+password:").ToString() -split ":\s*")[-1].Trim()))
"offline username match = $($recUser -eq $plainUser)"
"offline password match = $($recPass -eq $plainPass)"
Remove-Variable recovered, rec, recUser, recPass
recovery exit = 0;兩個offline ... match皆為True
--help 寫的是從 stdin 取得,實測 -f 也接受。解出的明文不落盤,比對完即清掉變數。
反證:沒有備份就解不開
& $kubeseal --recovery-unseal -f $sealedPath -o yaml
"no-key exit = $LASTEXITCODE"
error: no key could decrypt secret (username, password),no-key exit = 1
排除的替代解釋是:其實不靠那份備份也解得開。
錯誤訊息與判定二 Controller 吐的是同一句——同一份程式碼、同一個判斷,只是一個在叢集端、一個在離線端。
這一步完全不經過叢集
$saved = $env:KUBECONFIG
$env:KUBECONFIG = "C:\nonexistent\no-such-kubeconfig.yaml"
& $kubeseal --recovery-unseal --recovery-private-key $keyBackup -f $sealedPath -o yaml | Out-Null
"offline-with-no-cluster exit = $LASTEXITCODE"
$env:KUBECONFIG = $saved
offline-with-no-cluster exit = 0
叢集完全不可達時仍照常運作,這正是災難復原情境要的性質。判定一證明鏈路可用,這一步證明鏈路可還原,兩者是不同的事。
helm uninstall 不刪 CRD,不刪的話「CRD 存在」的檢核恆真。--set 移除 fsGroup 與 runAsUser,收工時確認 SCC rolebinding 數量仍為 0。--dry-run=server 且帶 --kubeconfig:渲染檔 0 bytes 就是漏了 --kubeconfig。WriteAllText 寫檔:PowerShell 5.1 的 > 會加 BOM 污染 PEM。kubeseal 每次都帶 --controller-name 與 --controller-namespace:預設值與 chart 安裝結果不符。=active:加了就只剩最新一把,舊密文救不回來。收工時保留 Controller、金鑰對與公鑰,後續會用到;ss-demo-a / ss-demo-b 兩個 demo namespace 刪除。
& $OC --kubeconfig $kubeconfig delete namespace $nsA $nsB
& $OC --kubeconfig $kubeconfig get rolebinding -n $ssNs -o json | ConvertFrom-Json |
ForEach-Object { $_.items } | Where-Object { $_.roleRef.name -like "system:openshift:scc:*" } |
Select-Object @{n='role';e={$_.roleRef.name}}
SCC rolebinding 無輸出
uid 與 resourceVersion,跨叢集還原的通用慣例是先刪掉這兩個欄位。--key-renew-period 皆為官方說明,本文只觀測到首次生成的一把。anyuid 的 seccompProfiles 語意未查到規範。 欄位不存在時的行為由實測與第三方回報佐證,非文件化語意。規格與說明:
--dry-run 的 client / server 語意--key-renew-period、kubeseal 的 controller 名稱預設值runAsUser 與 fsGroup 的建議第三方回報(本文據以佐證,非本文實測):
runAsUser / fsGroup 在 OpenShift 上的失敗runAsUser / runAsGroup / fsGroup
--dry-run=server 在 helm template 上只影響 lookup
這一天的成果是三件事:密文可以進版控、密文換了 namespace 就解不開、金鑰備份離開叢集仍然有效。
過程中比較花時間的不是部署本身,而是 SCC。「有寫死的 UID 就授 anyuid」這個規則本身沒錯,但不完整——anyuid 與 restricted-v2 在不同欄位上各自嚴格,升級到更寬的 SCC 不保證原本能過的欄位還能過。正確的方向是縮小 chart 的要求,讓 restricted-v2 收得下。
下一步是把真正的憑證封進去。目前用的是拋棄式的 demo-cred,要把 Gitea 的管理帳號加密進版控,得先決定它落在哪個 namespace——因為 namespace 是綁定的一部分,決定錯了就要重封。