新增一個 namespace 後,Deployment 看起來沒有問題,Pod 卻停在 ImagePullBackOff。檢查才發現 imagePullSecrets 指向 regcred,但這份 Secret 只存在另一個 namespace。手動複製一次很快;每增加一個 workload、每輪替一次 credential 都要再複製,才是後續的負擔。
我的 GitOps 設定用 Vault 保存 Registry 登入資料,再由 ClusterExternalSecret 在選中的 namespaces 建立 ExternalSecret。各個 ExternalSecret 負責產生當地的 regcred,workload 仍然使用 Kubernetes 原生的 imagePullSecrets。
本文集中在分發與輪替。Vault Kubernetes auth、CA 信任與一般 ExternalSecret 的起點,可先看 Vault 與 External Secrets。
一份來源,不是一份跨 namespace Secret
Vault KV: registry/home → ClusterSecretStore/vault-registry
↑
ClusterExternalSecret → namespace apps-a / ExternalSecret → Secret/regcred
→ namespace apps-b / ExternalSecret → Secret/regcred
這裡仍然有多份 Kubernetes Secrets,只是它們有同一個受管理的來源。Kubernetes 的 private registry 文件要求 pull Secret 位於使用它的 Pod 所在 namespace;不能直接引用 default/regcred 來避開這個邊界。
開始前要準備好:
- 已安裝支援範例
external-secrets.io/v1資源的 External Secrets Operator(ESO)。 - Vault KV v2 的 mount 為
secret,Kubernetes auth 已可用。 - ESO 的 ServiceAccount 為
external-secrets,位於同名 namespace。 - ESO 能驗證
https://vault.home.arpa的 certificate,簽發它的 CA certificate 存在external-secrets/vault-caConfigMap;這裡存的是公開憑證,不是 CA private key。 - Node 能解析並透過 TLS 存取 Registry。Secret 分發不會替 container runtime 安裝 CA。
先保存一份可獨立使用的 Docker config
Registry credential 應有 pull 所需的最小權限,不使用可以管理整個 Registry 的帳號。準備一份受保護的 Docker config JSON,確認其中的 auths 對應實際 image 使用的 hostname 與 port,再存入 Vault:
vault kv put secret/registry/home \
".dockerconfigjson=@/secure/registry-pull.json"
這個檔案不能只是指向桌面 credential helper 的 credsStore 或 credHelpers 設定。Kubernetes 不會呼叫桌面 helper 替 kubelet 找登入資訊;交付給它的 config 必須包含可用於 Registry 的 credential。若從既有 Secret 移轉,需先將 .data[".dockerconfigjson"] 解碼一次,不把外層 base64 當成原始 JSON 再存入 Vault。
此例只給 ESO role 讀取單一 KV v2 path:
path "secret/data/registry/home" {
capabilities = ["read"]
}
把 policy 存成 registry-pull-read.hcl,由有權管理 Vault 的人套用並綁定既有 Kubernetes auth:
vault policy write registry-pull-read registry-pull-read.hcl
vault write auth/kubernetes/role/external-secrets-registry \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
audience=https://kubernetes.default.svc.cluster.local \
policies=registry-pull-read \
ttl=1h
audience 必須符合 ESO 要求的 ServiceAccount JWT 與該 cluster 的設定;下方 Store 的 serviceAccountRef.audiences 明確使用相同值。這仍是範例,不應略過環境核對,設定位置可參考 ESO Kubernetes authentication。建立 role 也不會代替 Vault Kubernetes auth 所需的 API server、CA 與 TokenReview 設定。
Store 管來源,ClusterExternalSecret 管分發
以下 Store 只提供 Registry credential。範例額外用 conditions 限制哪些 namespace 能使用它;這是存取邊界,不只是分發篩選:
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-registry
spec:
conditions:
- namespaceSelector:
matchLabels:
registry.example.com/pull-credentials: enabled
provider:
vault:
server: https://vault.home.arpa
path: secret
version: v2
caProvider:
type: ConfigMap
namespace: external-secrets
name: vault-ca
key: ca.crt
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets-registry
serviceAccountRef:
name: external-secrets
namespace: external-secrets
audiences:
- https://kubernetes.default.svc.cluster.local
---
apiVersion: external-secrets.io/v1
kind: ClusterExternalSecret
metadata:
name: registry-pull-credentials
spec:
externalSecretName: regcred
namespaceSelectors:
- matchLabels:
registry.example.com/pull-credentials: enabled
refreshTime: 1h
externalSecretSpec:
refreshPolicy: Periodic
refreshInterval: 2m
secretStoreRef:
name: vault-registry
kind: ClusterSecretStore
target:
name: regcred
creationPolicy: Owner
template:
engineVersion: v2
type: kubernetes.io/dockerconfigjson
data:
- secretKey: .dockerconfigjson
remoteRef:
key: registry/home
property: .dockerconfigjson
兩個 refresh 欄位做不同的事:refreshTime 讓 ClusterExternalSecret 定期檢查 namespace 裡的 ExternalSecrets;refreshInterval 決定各個 ExternalSecret 定期讀取 Vault 的間隔。前者不是 credential 的更新週期,後者也不是保證兩分鐘內一定完成的 SLA。Controller、Vault 或 network 故障都可能延後同步。ClusterExternalSecret 文件有完整定義。
Namespace label 是授權流程的一部分
需要這份 pull credential 的 namespace 才加入 label:
apiVersion: v1
kind: Namespace
metadata:
name: apps-a
labels:
registry.example.com/pull-credentials: enabled
ClusterExternalSecret 的 selector 只決定它在哪裡建立 ExternalSecrets,不能單獨阻止其他人自行建立 ExternalSecret 引用同一個 Store。因此還需要上面的 Store conditions、限制 namespace label 的修改權,以及限制誰能建立 ExternalSecrets。Store conditions 的語意見 ClusterSecretStore 文件。
任何可以在已授權 namespace 建立 Pod 的人,都可能透過掛載 Secret 取得這份 credential,即使沒有直接 get secrets 權限。共用 credential 適合相同信任範圍的 workloads;不同租戶或不同 repository 權限應拆成不同 credential、Store 與 selector,不為了方便而共用一個權限過大的帳號。
Pod 仍要明確引用 regcred
Secret 出現不會自動修改 Deployment。可以在 Pod template 或受控的 ServiceAccount 設定 imagePullSecrets。以下是用來驗證的短生命週期 Pod,image 必須換成 Registry 中確實存在、且允許以 true 結束的測試 image:
apiVersion: v1
kind: Pod
metadata:
name: registry-pull-check
namespace: apps-a
spec:
restartPolicy: Never
imagePullSecrets:
- name: regcred
containers:
- name: check
image: registry.home.arpa:5000/examples/pull-check:tested
imagePullPolicy: Always
command: ["/bin/sh", "-c", "true"]
先確認兩層 reconcile 都完成,再建立測試 Pod:
kubectl get clustersecretstore vault-registry
kubectl get clusterexternalsecret registry-pull-credentials
kubectl -n apps-a wait --for=condition=Ready externalsecret/regcred --timeout=120s
kubectl -n apps-a get secret regcred \
-o jsonpath='{.type}{"\n"}'
kubectl apply -f registry-pull-check.yaml
kubectl -n apps-a describe pod registry-pull-check
Secret type 應為 kubernetes.io/dockerconfigjson;Pod Events 要確認 image pull 成功。Always 會要求 runtime 檢查 image,但 layer 仍可能重用 cache。若 node 另有 Registry credential,成功也不一定證明它用了 regcred;嚴格驗證時要排除其他 credential source,或比對 Registry 端登入身分紀錄。完成後刪除測試 Pod。
輪替時保留重疊的有效期間
正常輪替可以依序做:
- 在 Registry 建立新的、同樣受限的 pull credential,暫時保留舊的。
- 更新 Vault 裡同一個
.dockerconfigjsonproperty。 - 確認每個目標 ExternalSecret 的 Ready、refresh time 與錯誤狀態,而非只看 ClusterExternalSecret 是否存在。
- 使用新 Pod 驗證實際 pull,確認所有目標 namespace 都已完成更新。
- 再到 Registry 撤銷舊 credential。
已經執行的 container 不會因 pull Secret 更新就 restart;這也不是用環境變數載入 application secret 的 reload 流程。若是外洩事件,撤銷舊 credential 可能必須優先執行,並接受新 Pod 暫時無法拉取 image 的可用性代價。
移除 namespace label 或刪除 ClusterExternalSecret 可能連帶清理它擁有的 ExternalSecrets 與 Secrets。creationPolicy: Owner 會建立 ownership,不能把移除 label 當成只停止更新、保留資料的開關。既有同名 ExternalSecret 衝突時,也應先確認 ownership 再遷移,不直接覆寫。
這套流程把「到每個 namespace 手動複製」變成可審查的宣告,但沒有消除分發的成本:每個 ExternalSecret 仍各自向 Vault refresh。Namespace 數量增加後要觀察 controller 與 Vault 負載。最重要的驗收仍是三件事:只有預定 namespace 收到 credential、Secret 確實更新、新的 image pull 真的使用了有效授權。