iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Kubernetes

防範軟體供應鏈攻擊:從零打造具備硬性阻擋能力的雲原生 CI/CD 流水線系列 第 12

Day 12:跨 Namespace 引用 Task(Cluster Resolver)

  • 分享至 

  • xImage
  •  

適用範圍

本文所有數值與輸出來自下列環境,量測日期 2026-08-08 至 08-09。換版本需重新驗證。

項目 版本
CRC 2.61.0
OpenShift / Kubernetes 4.21.14 / v1.34.6
OpenShift Pipelines Operator openshift-pipelines-operator-rh.v1.23.1
Tekton Pipelines controller v1.12.2
oc / kubectl 4.21.14 / v1.34.10
Gitea 1.27.0
Node / npm / Nx v24.16.0 / 11.13.0 / 23.1.1
PowerShell 5.1

Operator 版本(1.23.1)與 Tekton Pipelines 版本(v1.12.2)是兩個不同數列。TektonConfig.status.operandVersions 在本叢集為空,上游版本要讀 Deployment 的 label:

& $OC --kubeconfig $kubeconfig get deploy tekton-pipelines-controller -n openshift-pipelines -o jsonpath='{.metadata.labels}'

一、快樂路徑

1-1 前置:測試倉庫

需要一個叢集內匿名 clone 得到的 Git repo。本系列使用 Gitea 上的一個 Angular Nx monorepo(create-nx-workspace --preset=angular-monorepo--e2eTestRunner=none),後面幾天的建置、測試、掃描使用同一份程式碼。

三個與 CI 有關的設定:

  • .gitattributes* text=auto eol=lf。在 Windows 建立、Linux 容器內執行,CRLF 進版控會使容器內的 script 出現 bad interpreter
  • package-lock.json 必須進版控,npm ci 只讀它。
  • repo 必須是 public。本路徑不帶認證;私有 repo 需要 git-clone 的 basic-auth workspace 與一份 Secret。

完整建立步驟見附錄。

叢集內的可達性要單獨驗一次,主機端能連不代表叢集內能連:

$fmt = 'incluster=%{http_code} t=%{time_total}\n'
$url = 'http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/frontend-nx-mono/info/refs?service=git-upload-pack'

& $OC --kubeconfig $kubeconfig run gitea-probe -n default --restart=Never --attach --rm `
  --image=registry.access.redhat.com/ubi9/ubi-minimal:latest -- `
  curl -s -o /dev/null -w $fmt --max-time 10 $url

實測輸出 incluster=200 t=0.014955gitea-http 是 headless Service(ClusterIP: None),解析得到 Pod IP。

1-2 前置:Namespace

& $OC --kubeconfig $kubeconfig create namespace ci --dry-run=client -o yaml |
  & $OC --kubeconfig $kubeconfig apply -f -

ci 已存在時 create 會回 AlreadyExists 並以非零碼結束,改用 --dry-run | apply。若該 namespace 原先是用 create 建立的,第一次執行會出現一則補上 kubectl.kubernetes.io/last-applied-configuration 的 Warning,只出現一次。

Operator 對新 namespace 注入 pipeline ServiceAccount 與兩條 RoleBinding,注入是非同步的。namespace 建立後立即送出 PipelineRun 會得到 PodCreationFailed: serviceaccounts "pipeline" not found。等 SA 出現再繼續:

$deadline = (Get-Date).AddSeconds(60)
do {
  Start-Sleep -Seconds 2
  $sa = & $OC --kubeconfig $kubeconfig get sa pipeline -n ci --ignore-not-found -o name
} until ($sa -or (Get-Date) -gt $deadline)
$sa

1-3 查來源 Task 的識別碼

& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.params[*].name}'
& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.results[*].name}'
& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.workspaces[*].name}'

實測輸出:

params     : CRT_FILENAME HTTP_PROXY HTTPS_PROXY NO_PROXY SUBDIRECTORY USER_HOME
             DELETE_EXISTING VERBOSE SSL_VERIFY URL REVISION REFSPEC SUBMODULES
             DEPTH SPARSE_CHECKOUT_DIRECTORIES
results    : COMMIT URL COMMITTER_DATE
workspaces : ssh-directory basic-auth ssl-ca-directory output

同時確認兩個前提:

& $OC --kubeconfig $kubeconfig get cm cluster-resolver-config -n openshift-pipelines -o jsonpath='{.data}'
& $OC --kubeconfig $kubeconfig get sc

cluster-resolver-configdefault-namespace 為空,因此 1-4 的 namespace 參數不能省。StorageClass 有預設(crc-csi-hostpath-provisioner),因此 PipelineRun 的 volumeClaimTemplate 不需要指定 storageClassName

1-4 三份 YAML

PowerShell 5.1 寫檔用 [System.IO.File]::WriteAllText 搭配不含 BOM 的編碼物件;> 產出 UTF-16LE,Set-Content -Encoding UTF8 會加 BOM。here-string 必須用單引號版 @'...'@,雙引號版會把 $(params.x) 當成子運算式求值。

自訂 Task(放在 ci):

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: d12-show-commit
  namespace: ci
spec:
  description: local task in ci
  params:
    - name: commit
      type: string
  steps:
    - name: show
      image: registry.access.redhat.com/ubi9/ubi-minimal:latest
      script: |
        #!/usr/bin/env bash
        echo "resolved commit: $(params.commit)"

description 這行是刻意加的,用途見 2-5。自訂 Task 的參數用小寫是合法的。

Pipeline:

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: d12-happy
  namespace: ci
spec:
  params:
    - name: repo-url
      type: string
    - name: repo-revision
      type: string
      default: main
  workspaces:
    - name: source
  tasks:
    - name: fetch
      taskRef:
        resolver: cluster
        params:
          - name: kind
            value: task
          - name: name
            value: git-clone
          - name: namespace
            value: openshift-pipelines
      params:
        - name: URL
          value: $(params.repo-url)
        - name: REVISION
          value: $(params.repo-revision)
      workspaces:
        - name: output
          workspace: source
    - name: show-commit
      runAfter: [fetch]
      taskRef:
        name: d12-show-commit
      params:
        - name: commit
          value: $(tasks.fetch.results.COMMIT)

$(tasks.fetch.results.COMMIT)fetch 是這份 Pipeline 內的實例名(tasks[].name),不是 Task 名稱。

PipelineRun:

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: d12-happy-
  namespace: ci
spec:
  pipelineRef:
    name: d12-happy
  taskRunTemplate:
    serviceAccountName: pipeline
  params:
    - name: repo-url
      value: http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/frontend-nx-mono.git
  workspaces:
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes: [ReadWriteOnce]
          resources:
            requests:
              storage: 1Gi

1-5 套用與執行

& $OC --kubeconfig $kubeconfig apply --validate=strict -f .\d12-show-commit.yaml
& $OC --kubeconfig $kubeconfig apply --validate=strict -f .\d12-happy-pipeline.yaml

$run  = & $OC --kubeconfig $kubeconfig create -f .\d12-happy-run.yaml -o name
$name = ($run -split '/')[-1]

generateName 不能用 apply--validate=strict 的作用範圍見 2-2。

1-6 驗證

& $OC --kubeconfig $kubeconfig get pipelinerun $name -n ci -o jsonpath='{.status.conditions[0].reason}'

實測 Succeeded,message 為 Tasks Completed: 2 (Failed: 0, Cancelled 0), Skipped: 0,從送出到完成約 45 秒。

確認跨 Namespace 解析:

$trs = & $OC --kubeconfig $kubeconfig get taskrun -n ci -l tekton.dev/pipelineRun=$name -o name
foreach ($tr in $trs) {
  $n   = ($tr -split '/')[-1]
  $src = & $OC --kubeconfig $kubeconfig get taskrun $n -n ci -o jsonpath='{.status.provenance.refSource.uri}'
  "{0}`t{1}" -f $n, $src
}

實測輸出:

d12-happy-xxxxx-fetch         /apis/tekton.dev/v1/namespaces/openshift-pipelines/task/git-clone@<uid>
d12-happy-xxxxx-show-commit   (空)

確認 result 傳遞:

& $OC --kubeconfig $kubeconfig logs -n ci -l tekton.dev/pipelineTask=show-commit -c step-show --tail=5

輸出 resolved commit: <40 位 SHA>,與本機 git rev-parse HEAD 相同。-c step-show 不能省,否則 oc 會往 stderr 輸出 Defaulted container ...,在 PowerShell 5.1 會被包成 NativeCommandError


二、技術記錄

2-1 taskRef 的 schema 不含 namespace

pipelines.tekton.dev CRD 目前安裝的兩個版本(v1beta1v1),taskRef 底下都沒有 namespace 屬性。

& $OC --kubeconfig $kubeconfig explain pipeline.spec.tasks.taskRef
FIELDS:
  apiVersion  <string>
  kind        <string>
  name        <string>

套用一份帶 taskRef.namespace 的 Pipeline,再把物件讀回來:

& $OC --kubeconfig $kubeconfig get pipeline probe -n ci -o jsonpath='{.spec.tasks[0].taskRef}'
{"kind":"Task","name":"git-clone"}

namespace 鍵不在物件裡。API Server 依 structural schema 在寫入 etcd 前剪除未定義欄位,該設定沒有到達 controller。

不是「v1 移除、v1beta1 保留」。本叢集這兩個 API 版本的 schema 都沒有這個欄位。至於 Tekton 上游是否曾經支援過 taskRef.namespace,搜尋未找到支持或否定的證據,本文不做判斷。

2-2 ockubectl 的驗證預設相反

同一份帶 taskRef.namespace 的 Pipeline:

呼叫 結果 exit
oc apply(預設) pipeline.tekton.dev/probe created,無錯誤無警告 0
oc apply --validate=false 同上 0
oc apply --validate=strict strict decoding error: unknown field "spec.tasks[0].taskRef.namespace" 1
oc apply --server-side field not declared in schema 1
kubectl apply(預設) Error from server (BadRequest): … strict decoding error 1

兩個 binary 的 help 印出的預設值不同,說明文字相同:

oc apply      --validate='ignore'
kubectl apply --validate='strict'

ignore 的說明文字為:不執行任何 schema 驗證,靜默丟棄未知或重複的欄位。

--validate=strict 的作用範圍另有一個限制:

呼叫 結果
oc apply --dry-run=server created (server dry run),exit 0,無警告
oc apply --dry-run=server --validate=strict BadRequest … strict decoding error,exit 1
oc apply --dry-run=client --validate=strict created (dry run),exit 0,未檢查未知欄位

--validate=strict 只在請求送到 API Server 的路徑上生效。配 --dry-run=client 不檢查未知欄位。

GitOps 工具多數使用 server-side apply,該路徑會被擋下。

2-3 Cluster Resolver 的組態

& $OC --kubeconfig $kubeconfig get cm resolvers-feature-flags -n openshift-pipelines -o jsonpath='{.data}'
{"enable-bundles-resolver":"true","enable-cluster-resolver":"true",
 "enable-git-resolver":"true","enable-http-resolver":"true","enable-hub-resolver":"true"}
& $OC --kubeconfig $kubeconfig get cm cluster-resolver-config -n openshift-pipelines -o jsonpath='{.data}'
{"allowed-namespaces":"","blocked-namespaces":"","default-kind":"task","default-namespace":""}
後果
allowed-namespaces 不限制來源 namespace
blocked-namespaces 無黑名單
default-kind task 未指定 kind 時以 Task 解析
default-namespace namespace 參數不能省,無預設值可回退

taskRef 底下 resolver 的 params 是自由的 name/value 陣列,不是固定 schema。參數名打錯不會產生任何錯誤,只會被忽略;--validate=strictkubectl 的預設驗證都擋不住(實測 - name: cache 這個非文件記載的參數名可以通過 kubectl apply 的 strict 驗證並正常執行)。

feature-flagsenable-concise-resolver-syntaxfalse,簡寫語法在本叢集未啟用。

2-4 官方預建 Task 的參數命名分佈

openshift-pipelines namespace 內有 45 個 Task,參數總數 374。其中 40 個 Task 的參數全大寫,5 個含小寫字母(含小寫的參數共 34 個)。

Task 風格
helm-upgrade-from-repohelm-upgrade-from-source snake_casehelm_repochart_name
pull-requestargocd-task-sync-and-wait kebab-casesecret-key-refapplication-name
kubeconfig-creator 小寫單字與 camelCase 混用(nameurlclientKeyDataclientCertificateData

全大寫是這個 bundle 的慣例,不是 Tekton 的規則。kubeconfig-creator 在同一個 Task 內混用兩種風格。

同一份 bundle 內另有 6 個 StepAction,其中 git-clonecache-fetchcache-upload 三個名稱與 Task 重複。kind 的預設值是 task,未指定時取得的是 Task。45 個 Task 均未使用 step.ref 引用 StepAction。

工作區名稱為小寫(ssh-directorybasic-authssl-ca-directoryoutput)。

2-5 判定解析成功應使用 provenance.refSource

同一次 PipelineRun 中兩筆 TaskRun 的比較:

status.taskSpec.description status.provenance.refSource
cluster resolver(跨 namespace) 有值 有值
taskRef.name(同 namespace) 有值

taskSpec.description 在兩種引用方式下都有值,不能用來判定是否跨 namespace 解析。1-4 的自訂 Task 刻意宣告了 description,若不宣告,同 namespace 那筆的 description 會是空的,看起來像是本地引用不產生 description。

resolver 那筆的 refSource 完整內容:

{
  "digest": {"sha256": "77ac8fe80ef2d5ebf78f7e65032f650d6436aa209957e0e794c7ef8dc33e9a5a"},
  "uri": "/apis/tekton.dev/v1/namespaces/d12-tasklib/task/hello-remote@3682d222-aea7-41b9-84d3-7d3dd2ef22cb"
}

uri 含來源 namespace 與 Task 名稱,另附內容 digest。

此欄位受 feature-flagsenable-provenance-in-status 控制,本叢集為 true

2-6 三種寫錯的攔截時機與 reason

三個案例的 oc apply 全部成功(exit 0),validating webhook 未攔截任何一個。錯誤在 PipelineRun 建立時出現。

寫錯 apply reason
taskRef.namespace created(欄位被剪除) CouldntGetTask
params 傳小寫 url created PipelineValidationFailed
result 引用小寫 commit created InvalidTaskResultReference

message 全文:

CouldntGetTask:
Pipeline ci/case1-taskref-ns can't be Run; it contains Tasks that don't exist:
Couldn't retrieve Task "git-clone": tasks.tekton.dev "git-clone" not found

PipelineValidationFailed:
[User error] Validation failed for pipelinerun run-case2 with error invalid input
params for task git-clone: missing values for these params which have no default
values: [URL]

InvalidTaskResultReference:
invalid result reference in pipeline task "t2": "commit" is not a named result
returned by pipeline task "t1"

[User error] 前綴只出現在第二個案例。

上游同時存在 ParameterMissing 這個 reason 常數,但缺參數的實測結果是 PipelineValidationFailed

2-7 呼叫端的 RBAC 不參與解析

以一個自建的 namespace d12-tasklibci/pipeline 對其無任何權限)為來源測試。

呼叫端 SA 的權限:

can-i get  tasks -n d12-tasklib --as=system:serviceaccount:ci:pipeline  = no
can-i list tasks -n d12-tasklib --as=system:serviceaccount:ci:pipeline  = no

直接讀取被拒絕:

Error from server (Forbidden): tasks.tekton.dev "secret-task-a1" is forbidden:
User "system:serviceaccount:ci:pipeline" cannot get resource "tasks"
in API group "tekton.dev" in the namespace "d12-tasklib"

以 cluster resolver 引用同一個 Task,TaskRun 明確指定 serviceAccountName: pipeline

reason              = Succeeded
執行的 SA           = pipeline
解析到的 description = only reachable via resolver
refSource           = /apis/tekton.dev/v1/namespaces/d12-tasklib/task/secret-task-a1@c08f6f85-…
step 輸出           = A1-RESOLVED

同一個 ServiceAccount,直接讀取為 Forbidden,經由 resolver 讀取成功並執行完成。

原因是解析由 resolver 的控制器執行,不由呼叫端執行:

tekton-pipelines-remote-resolvers 的 serviceAccountName = tekton-pipelines-resolvers

can-i get tasks   --all-namespaces --as=…:tekton-pipelines-resolvers = yes
can-i get secrets --all-namespaces --as=…:tekton-pipelines-resolvers = yes

呼叫端的 RBAC 在整個解析過程中沒有被查詢。

一個相關的觀察:ci/pipelineopenshift-pipelines 的 Task 讀取權(can-i get tasks -n openshift-pipelines = yes)。因此「引用官方預建 Task」這個用法的結果與 RBAC 允許的結果一致,用它測不出上述行為,需要一個明確未授權的 namespace 才能觀察到。

tekton-pipelines-resolvers 具備全叢集 get secrets 權限,用途未查證,本文不做推論。

2-8 namespace 名單有效,且是這條路徑上唯一的邊界控制

四種設定的實測結果,經 TektonConfig 設定後約 15 秒同步到 ConfigMap。每次使用未解析過的 Task 名稱以避開快取。

設定 結果 message
blocked-namespaces = d12-tasklib 擋下 access to specified namespace d12-tasklib is blocked
allowed-namespaces = openshift-pipelines 擋下 access to specified namespace d12-tasklib is not allowed
blocked-namespaces = * 擋下 only explicit allowed access to namespaces is allowed
blocked-namespaces = * + allowed-namespaces = d12-tasklib Succeeded

四種情況的 reason 全部是 TaskRunResolutionFailed(第四種成功,無 reason)。與 2-6 的三個錯誤不同:那三個的 reason 各自不同,解析階段的失敗共用同一個 reason,需要讀 message 才能分辨是哪一種設定造成的。

allowed-namespaces 是白名單語意,設定後不在名單上的一律擋下,不需要另外設 blocked-namespaces。第四種組合(全部封鎖後逐一放行)在本版本可行。

blocked-namespaces = * 是叢集層級設定,會影響該叢集上所有使用 cluster resolver 的 Pipeline。

2-9 設定的宣告來源是 TektonConfig,不是 ConfigMap

手動修改 cluster-resolver-configfeature-flags 之後:

觸發 結果
閒置 120 秒 未還原
patch tektonconfig(加 annotation 觸發 reconcile) 未還原
rollout restart Operator Deployment 未還原

Operator 的行為是在 TektonConfig 對應欄位發生變動時把值推到 ConfigMap,不是持續對帳:

情境 結果
TektonConfig 設成與現況等效的值 不推送
TektonConfig 設成不同的值 15 秒內推送
TektonConfig 改變既有值 30 秒內推送

因此手動改 ConfigMap 不會被自動還原,但只要有人動到 TektonConfig 對應欄位,手動修改會被覆蓋且沒有提示。

TektonConfig 的相關欄位:

& $OC --kubeconfig $kubeconfig explain tektonconfig.spec.pipeline
cluster-resolver-config   <map[string]string>
bundles-resolver-config   <map[string]string>
git-resolver-config       <map[string]string>
default-resolver-type     <string>
enable-cluster-resolver   <boolean>

Red Hat 文件在說明 feature-flags 時記載改動會在 Operator reconcile 時被覆寫,與上述實測不一致。原因未查明,本文不做判斷。

2-10 其他與本路徑有關的觀察

Resolver 快取resolver-cache-config{"max-size":"1000","ttl":"5m"}cluster-resolver-config 沒有 default-cache-mode 鍵。實測修改來源 Task 的 description 後立即重新解析,取得的是新值,不是快取的舊值。此結果僅涵蓋「來源變動後是否取得新值」,未量測「來源未變動時是否使用快取」。

ClusterTaskclustertasks.tekton.dev CRD 在本叢集不存在,oc get clustertask 回報無此資源型別。45 個預建 Task 均為 namespace 範圍的 Task,集中在 openshift-pipelines

版本化雙胞胎:預建 Task 有不帶後綴與帶 -1-23-0 後綴兩組(如 git-clonegit-clone-1-23-0)。不帶後綴的會隨 Operator 升版變動。

Namespace 注入的其他影響:Operator 對非 openshift-* / kube-* 前綴的每個 namespace 注入 pipeline SA 與兩條 RoleBinding。注入後該 namespace 的 security.openshift.io/MinimallySufficientPodSecurityStandardrestricted 變為 baseline。TaskRun 的 Pod 實際套用 pipelines-scc

清理:刪除 PipelineRun 後 TaskRun、Pod、PVC 的回收是非同步的,實測約 90 秒歸零。CRC 的 crc-csi-hostpath-provisionerreclaimPolicyRetainvolumeClaimTemplate 產生的 PV 在 PVC 刪除後停留在 Released 且不自動回收,get pvc -n ci 看不到(PV 是叢集層級物件)。單次約 1.7 MB。

& $OC --kubeconfig $kubeconfig get pv --no-headers | Where-Object { $_ -match 'Released' }

三、收工前檢查清單

  • [ ] 跨 Namespace 引用使用 resolver: cluster,且 kindnamenamespace 三個 params 都寫了
  • [ ] 同 Namespace 引用只用 taskRef.name
  • [ ] params/results 名稱是查出來的
  • [ ] result 引用寫的是 Pipeline 內的實例名,不是 Task 名
  • [ ] 以 provenance.refSource 而非 taskSpec.description 判定跨 Namespace 解析
  • [ ] 已確認自己叢集的 allowed-namespacesblocked-namespaces 現值

四、未驗證與範圍限制

  • Tekton 上游是否曾經支援 taskRef.namespace:搜尋未找到支持或否定的證據。可確認的只有本叢集兩個 API 版本的 schema 都沒有這個欄位。
  • 2-9 與 Red Hat 文件不一致的原因:未查明。可能是文件過時、版本差異,或文件描述的是另一種 reconcile 觸發條件。
  • Resolver 快取在來源未變動時是否被使用:未量測。需要 resolver 的 metrics 或 log,本次未做。
  • tekton-pipelines-resolvers 的全叢集 get secrets 權限用途:未查證。
  • kubectl 的驗證行為:以 v1.34.10 量測。目前這台機器上未安裝 kubectl,該結果無法立即複驗。
  • allowed-namespacesblocked-namespaces 的組合僅測試 2-8 表中四種,未窮舉。

五、參考資料

本文的數值、指令輸出與行為判定全部來自第〇節環境的實測。外部資料只用於兩件事:確認某個行為是否有官方說明,以及在實測與文件不一致時標出差異。

有明確連結的

用於 來源
oc apply--validate 預設值為 ignore、以及 ignore 會靜默丟棄未知欄位 oc-apply(1) man page:https://manpages.opensuse.org/Tumbleweed/openshift-clients/oc-apply.1.en.html
PipelineRunReason 常數清單(含 CouldntGetTaskParameterMissingPipelineValidationFailed tektoncd/pipeline 的 Go API 文件:https://pkg.go.dev/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1
Retain 之下 PV 停留在 Released、不自動回收、需管理員手動處理 Kubernetes 官方文件,Persistent Volumes 的 Reclaiming 章節:https://kubernetes.io/docs/concepts/storage/persistent-volumes/

上一篇
Day 11:裝好 Tekton Pipelines 之後,叢集多了一個能變成任何人的 ServiceAccount
下一篇
Day 13:Workspace 的四層命名——同一顆 PVC,兩個 Pod 看到不一樣的路徑
系列文
防範軟體供應鏈攻擊:從零打造具備硬性阻擋能力的雲原生 CI/CD 流水線14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言