iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Kubernetes

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

Day 14:自動觸發機制 —— Tekton Triggers 的授權規範與 CEL 的邊界

  • 分享至 

  • xImage
  •  

今日目的:把 Gitea 的 Push 事件接上先前建立的 Pipeline,並且讓這條路徑具備「只有 Gitea 送得動」的能力。
先備知識:本篇銜接先前〈Workspace/PVC〉與〈Webhook DNS〉兩篇的部署成果。


開場對照表

這篇有兩處是把先前的判斷推翻,不是補充。先攤開來:

議題 原本的判斷 實測結果
EventListener 的 RBAC 必須手動綁 RoleBinding + ClusterRoleBinding,缺一就壞 在 OpenShift Pipelines 上兩個都不必綁,Operator 已經處理掉。缺漏的症狀在這台重現不出來
Webhook 簽章驗證 已知缺口,要自己想辦法 內建的 github ClusterInterceptor 直接認得 Gitea 的簽章,不必自寫 Interceptor
被擋下的請求 (原本沒討論) HTTP 一律回 202,EventListener 的 log 也不會記錄原因
傳 Commit SHA 給 git-clone 淺層拉取做不到 在 Gitea 上做得到。結論不變,但理由要換

實測環境版本表

CRC 2.61.0+6eb443
OpenShift 4.21.14 / Kubernetes v1.34.6(單節點)
Red Hat OpenShift Pipelines Operator 1.23.1(controller v1.12.2)
Tekton Triggers v0.36.0(enable-api-fields: stable)
Pipelines as Code v0.48.1
Gitea 1.27.0
用戶端:Windows 11 Pro + PowerShell 5.1

下面所有結論只對這組版本成立。 Triggers、Operator、Gitea 任何一個換版本都要重驗——本篇至少有兩處結論就是因為版本不同而與先前的敘述相反。


一、三個資源與一條鏈路

角色 資源類型 職責
事件監聽器 EventListener 在叢集內開一個 HTTP 端點(獨立 Pod + Service),接收 Webhook
資料綁定器 TriggerBinding 從 JSON Payload 擷取欄位,轉成具名參數
資源模板 TriggerTemplate 接參數,實體化出 PipelineRun/TaskRun

流程是:EventListener 收請求 ➔ Interceptor 驗證與過濾 ➔ TriggerBinding 擷取 ➔ TriggerTemplate 派生資源

Interceptor 這一段在原本的敘述裡被略過了,但它是本篇後半的重點——驗證與過濾是兩件事,而且它們跑在同一個地方、寫同一份 log。


二、最小快樂路徑

四個物件、一個 Webhook,跑通為止。ServiceAccount 沿用 ci 既有的 pipeline,不新建(理由見第三節)。

2.1 共享密鑰

$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Select-Object -First 1).FullName
$kubeconfig = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"

$secretValue = -join ((48..57) + (97..122) | Get-Random -Count 36 | ForEach-Object {[char]$_})

& $OC --kubeconfig $kubeconfig create secret generic d14-webhook-secret `
  --from-literal=secretToken="$secretValue" -n ci

Get-Random -Count不重複取樣,取樣池只有 36 個字元,所以最多只能取 36 個。寫 40 不會報錯,會靜靜給你 36 個。

2.2 TriggerBinding

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: d14-gitea-push-binding
  namespace: ci
spec:
  params:
    - name: git-url
      value: $(body.repository.clone_url)
    - name: git-commit
      value: $(body.after)

2.3 TriggerTemplate

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: d14-gitea-push-template
  namespace: ci
spec:
  params:
    - name: git-url
    - name: git-commit
  resourcetemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: d14-run-
        labels:
          d14-trigger: "true"
      spec:
        pipelineRef:
          name: d15-happy-path
        timeouts:
          pipeline: 10m
        workspaces:
          - name: shared-workspace
            persistentVolumeClaim:
              claimName: pipeline-source-pvc

三個容易踩的點:

  • 欄位名是 resourcetemplates全小寫。上游文件在不同頁面出現過兩種寫法。
  • Triggers 的 CRD 是 x-kubernetes-preserve-unknown-fields: trueoc explain 走不進去,API Server 也不驗欄位——寫錯只會在套用時被 Triggers 自己的 webhook 擋下,或者根本不擋、執行期才炸。
  • workspace 用固定 PVC,不要用 volumeClaimTemplate。StorageClass 若是 Retain,Webhook 每觸發一次就多一顆 Released 的孤兒 PV。

2.4 EventListener

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: d14-gitea-ci
  namespace: ci
spec:
  serviceAccountName: pipeline
  triggers:
    - name: gitea-push
      interceptors:
        - name: "verify gitea signature"
          ref:
            name: github
          params:
            - name: secretRef
              value:
                secretName: d14-webhook-secret
                secretKey: secretToken
            - name: eventTypes
              value: ["push"]
        - name: "only main branch"
          ref:
            name: cel
          params:
            - name: filter
              value: "body.ref == 'refs/heads/main'"
      bindings:
        - ref: d14-gitea-push-binding
      template:
        ref: d14-gitea-push-template

套用後約 80 秒:

$ oc get el,deploy,svc -n ci
eventlistener.triggers.tekton.dev/d14-gitea-ci
    http://el-d14-gitea-ci.ci.svc.cluster.local:8080   True   MinimumReplicasAvailable   True
deployment.apps/el-d14-gitea-ci   1/1   1   1   84s
service/el-d14-gitea-ci   ClusterIP   10.217.5.28   <none>   8080/TCP,9000/TCP   84s

8080 是事件入口、9000 是 metrics。位址不必自己拼,EventListener 的 status.address.url 直接寫好了。

2.5 Gitea Webhook

{
  "type": "gitea",
  "active": true,
  "branch_filter": "*",
  "events": ["push"],
  "config": {
    "url": "http://el-d14-gitea-ci.ci.svc.cluster.local:8080",
    "content_type": "json",
    "http_method": "post",
    "secret": "<與 Secret 相同的值>"
  }
}
curl.exe -s -X POST --cacert $CA `
  -H "Authorization: token $token" -H "Content-Type: application/json" `
  -d "@hook.json" `
  "$GITEA/api/v1/repos/gitea_admin/frontend-nx-mono/hooks"

三個坑:

  • content_type 要填 json,不是 application/json 填錯不會報錯,但 Body 會變成 form-encoded,所有 Interceptor 一起失效。
  • Gitea 預設的 ALLOWED_HOST_LIST 只放行 external,打向 ClusterIP(私有網段)會被 Gitea 自己擋掉。本系列先前已經設成 loopback,private,external
  • PowerShell 5.1 不要把 JSON 用管線餵進 curl.exe 的 stdin。Windows PowerShell 的 Unicode 編碼一律帶 BOM,Gitea 會回 422 invalid character '\ufeff' at start of value。寫檔用 [System.IO.File]::WriteAllText($path, $json, (New-Object System.Text.UTF8Encoding($false))),再用 -d "@檔名"。PowerShell 7 預設 utf8NoBOM,不會踩到——這是 5.1 專屬的行為。

2.6 跑通

觸發前 Gitea HEAD = d5e9787079056cb7f34320c7f8402f904d9beea1

[21:23:26] d14-run-82c4j  Unknown/Running
[21:24:16] d14-run-82c4j  True/Succeeded

results.COMMIT = d5e9787079056cb7f34320c7f8402f904d9beea1   ← 相等

順帶一個沒寫卻出現的欄位:

spec:
  taskRunTemplate:
    serviceAccountName: pipeline        # ← TriggerTemplate 裡沒有這一行

這是 TektonConfig .spec.trigger.default-service-account 注入的。TriggerTemplate 看不到,PipelineRun 的 YAML 裡才有——排查「這條 run 到底用什麼身分跑」的時候要往這裡找。


三、RBAC:官方規範,以及 OpenShift 上的實際情況

3.1 官方的兩份 ClusterRole

Tekton Triggers 上游提供兩份給 EventListener 用的 ClusterRole,分別對應兩種範疇:

綁定方式 ClusterRole 授權內容
RoleBinding(namespace) tekton-triggers-eventlistener-roles triggers.tekton.dev 五種資源的 get/list/watch;configmaps 讀取;tekton.dev 的 pipelineruns/taskruns create;serviceaccounts impersonate;events create/patch
ClusterRoleBinding tekton-triggers-eventlistener-clusterroles clustertriggerbindings / clusterinterceptors 的 get/list/watch;secrets 的 get/list/watch

有一個細節值得先講清楚:這兩組權限的資源並不重疊。 triggers.tekton.dev 那組只有 get/list/watch,完全沒有 create;create 只給 tekton.dev 的 pipelineruns/taskruns。所以「list/watch 給了但 create 忘了」這種半殘狀態,只有在自己手寫 Role 時才會出現。

另一個細節更值得注意:-clusterroles 那份的 secrets 沒有 resourceNames 限制

ClusterRole/tekton-triggers-eventlistener-clusterroles
  (core) / secrets => get, list, watch      ← resourceNames: 無

也就是說,一旦用 ClusterRoleBinding 把它綁給某個 ServiceAccount,那個 SA 就取得全叢集所有 namespace 的 Secret 讀取權。對一個以供應鏈防護為題的環境,這個代價不小。

3.2 但在 OpenShift 上,這兩個你可能都不必綁

實測的結果是:EventListener 用 ci 既有的 pipeline SA,沒有建立任何 RoleBinding 或 ClusterRoleBinding,Pod 一次就起來,READY 1/1

原因是 Operator 已經幫你綁好了,而且不是用你以為的方式。把授權鏈整條追出來:

clusterinterceptors 的來源
  ClusterRoleBinding/openshift-pipelines-clusterinterceptors
    roleRef  : ClusterRole/openshift-pipelines-clusterinterceptors
    subjects : ServiceAccount/pipeline (ns=default)
               ServiceAccount/pipeline (ns=gitea)
               ServiceAccount/pipeline (ns=ci)        ← 逐一列舉每個使用者 namespace
               …

clustertriggerbindings 的來源
  ClusterRoleBinding/tekton-clustertriggerbindings-view-rolebinding-all-users
    roleRef  : ClusterRole/tekton-clustertriggerbindings-view-role
    subjects : Group/system:authenticated              ← 所有 SA 都在這個群組裡

3.3 這裡有一個排查陷阱

clusterinterceptorsclustertriggerbindings 都是 cluster-scoped 資源。而 cipipeline SA 另外有一個 namespace 內的 RoleBinding 綁到 ClusterRole/edit

於是很容易做出這個推論:「edit 聚合了 Triggers 的權限,所以 pipeline SA 什麼都有。」

這個推論一半是對的,一半是錯的。 namespace 的 RoleBinding 在原理上不可能授予 cluster-scoped 資源的 list/watch——edit 只解釋得了 namespace 內那一半。

oc auth can-i 這時候幫不上忙:

& $OC --kubeconfig $kubeconfig auth can-i list clusterinterceptors `
    --as=system:serviceaccount:ci:pipeline
# yes

這個 yes真的,但它來自上面那兩個 Operator 建立的 ClusterRoleBinding,不是來自 editcan-i 只告訴你能不能,不告訴你為什麼能。 拿它的答案去反推授權來源,會推錯。

要確認來源,得反過來從 ClusterRoleBinding 掃:

& $OC --kubeconfig $kubeconfig get clusterrolebinding -o json |
    ConvertFrom-Json |
    Select-Object -ExpandProperty items |
    Where-Object { $_.subjects.name -contains 'pipeline' -or $_.subjects.kind -contains 'Group' }

3.4 那什麼時候真的需要綁

三種情況:

  1. 不是 OpenShift Pipelines(上游 Tekton Triggers 自行安裝)——上面那兩個 ClusterRoleBinding 是 Operator 建的,上游沒有。
  2. 不用 pipeline SA,自己建了一個新的 ServiceAccount——它不在 openshift-pipelines-clusterinterceptors 的 subjects 名單裡。
  3. Operator 換版本之後——那份 subjects 名單是 Operator 逐一列舉的,行為可能改變。

要綁的時候,先想清楚要不要 -clusterroles 下一節會說明,只要 Secret 放對地方,它可以不綁。


四、簽章驗證:github Interceptor 認得 Gitea

4.1 Gitea 送出的標頭

Gitea 的 Webhook 每次 delivery 都會同時送出三組簽章標頭與三套事件標頭:

已設 secret:
  X-Gitea-Signature       前綴=(無)        hex 長度=64
  X-Gogs-Signature        前綴=(無)        hex 長度=64
  X-Hub-Signature         前綴=sha1=      hex 長度=40
  X-Hub-Signature-256     前綴=sha256=    hex 長度=64
  X-Gitea-Event / X-Github-Event / X-Gogs-Event  = push

X-Gitea-SignatureX-Hub-Signature-256同一個 HMAC-SHA256 值,差別只在前綴慣例。而 Tekton 的 github Interceptor 讀的正是 X-Hub-Signature-256——所以它能驗 Gitea 的簽章,不必自己寫 Interceptor。

社群早年有好幾個自寫的 gitea interceptor 專案,那是因為當時 Gitea 還沒補齊 GitHub 相容標頭。是 Gitea 端補上的,不是 Tekton 改的。(Gitea 從哪一版開始送 X-Hub-Signature-256,我沒有查證,只能說在 1.27.0 上成立。)

未設 secret 時的形態不一樣,而且很關鍵:

X-Gitea-Signature:     ""
X-Hub-Signature:       "sha1="
X-Hub-Signature-256:   "sha256="

標頭還在,前綴還在,只是後面沒有內容。 Interceptor 面對的是「空簽章」而不是「缺標頭」——排查時看到的錯誤訊息會不一樣。

4.2 Secret 放哪裡

實測結論:Secret 建在 EventListener 所在的 namespace(ci)就夠,不需要綁 -clusterroles,不需要給任何人全叢集的 Secret 讀取權。

這一點原本從 RBAC 佈局推不出來——tekton-triggers-core-interceptors(跑在 openshift-pipelines)與 EventListener 的 SA 兩邊都有不限對象的 secrets 讀取權,兩個候選者權限都夠。只能靠實測分辨,而實測的答案是「放 ci、不綁、照樣通過」。

4.3 不要開的兩個參數

github Interceptor 的 addChangedFilesgithubOwners 會去呼叫 GitHub API。Gitea 沒有對應端點,一開就壞。


五、CEL 過濾的是內容,不是來源

這一節是本篇的核心。

CEL 判斷 Payload 的內容github Interceptor 驗證請求的來源。兩者不能互相取代。要證明這件事,最直接的方式是把同一個偽造請求送兩次,中間只改 EventListener 的設定。

default namespace 起一顆臨時 Pod,直接打 ci 的 EventListener Service,Body 裡塞一個 ref: refs/heads/main不帶任何簽章

EventListener 設定 請求來源 簽章 HTTP 產生 PipelineRun
github + cel default 的臨時 Pod 202 0
只有 cel 同一顆 Pod、同一個 Body 202 1(跑完並 Succeeded)

少了簽章驗證,叢集內任何一顆 Pod 都能觸發一條真實的流水線。

而且要注意曝露範圍這件事的實際意義:

$ oc get svc el-d14-gitea-ci -n ci
el-d14-gitea-ci   ClusterIP   10.217.5.28   <none>   8080/TCP,9000/TCP

ci 的 Route 數 = 0

Service 是 ClusterIP、沒有 Route,所以攻擊面確實限於叢集內部。但上面那顆 Pod 就在叢集內部,而且在另一個 namespace。「內部」不等於「可信」——網路層對這個 Service 沒有任何阻隔。


六、擋下來之後:三個觀測性斷層

上一節的表格裡,兩列的 HTTP 都是 202。這不是巧合。

6.1 EventListener 一律回 202

四種情況——簽章正確、簽章錯誤、無簽章標頭、空值簽章——EventListener 全部回 202 Accepted,連 Response Body 都一樣:

{"eventListener":"d14-gitea-ci","namespace":"ci","eventListenerUID":"…","eventID":"…"}

攻擊者無法從回應分辨自己是被擋還是觸發成功;防守方也不能用回應碼做監控。

這是上游已知的行為。issue #1465(2022-10 提出)描述的一模一樣:EventListener 即使 Interceptor 失敗仍回 202、不產生任何資源。那張 issue 最後被 stale bot 標記 lifecycle/rotten 自動關閉,沒有關聯的修補 PR。從 v0.21.0 到本篇實測的 v0.36.0,行為沒有改變。

範圍還更大一點:issue #1183 顯示連「PipelineRun 建立失敗」也是回 202。實測也重現了——刻意讓 generateName 帶大寫,API Server 拒絕建立,HTTP 仍然是 202。

6.2 原因不在 EventListener 的 log,在另一個 namespace

被擋下的請求,EventListener 自己的 log 裡查不到原因。原因寫在 tekton-triggers-core-interceptors——那是跑在 openshift-pipelines全叢集共用的一個 Deployment:

{"level":"info","ts":1786228907.9590743,"caller":"server/server.go:157","msg":"Interceptor response is: &{Extensions:map[] Continue:false Status:{Code:FailedPrecondition Message:payload signature check failed}}"}
{"level":"info","ts":1786228908.0291278,"caller":"server/server.go:157","msg":"Interceptor response is: &{Extensions:map[] Continue:false Status:{Code:FailedPrecondition Message:no X-Hub-Signature-256 header set}}"}

只有這裡直接說出「這是簽章問題」。 但它有三個限制。

限制一:這行 log 沒有歸屬資訊。

整行只有四個欄位:level / ts / caller / msg。沒有 namespace、沒有 EventListener 名稱、沒有 eventID。而 caller 對成功與失敗都是同一個 server/server.go:157——通過與被擋走的是同一行程式碼。

在跑著多個 namespace、多個 EventListener 的叢集上,看到這一行不知道是誰。它只能告訴你「這座叢集裡有某個 EventListener 收到一個簽章不對的請求」。

對照之下,EventListener 自己的 log 帶著 namespace 與 eventID,卻沒有失敗原因。帶得動歸屬資訊的元件沒有錯誤內容,有錯誤內容的元件沒有歸屬資訊。

限制二:CEL 的正常過濾寫在同一條流裡。

{"level":"info","ts":1786229292.9724135,"caller":"server/server.go:157","msg":"Interceptor response is: &{Extensions:map[] Continue:false Status:{Code:FailedPrecondition Message:expression body.ref == 'refs/heads/main' did not return true}}"}

同一個 pod、同一個 info 等級、同一個格式、同一個 Code:FailedPrecondition唯一的差別是 Message: 後面的自由文字。

這代表你只能對非結構化文字做比對,而 CEL 運算式的原文會被完整印進同一個欄位——如果有人的 CEL 條件剛好提到 signature 之類的字,grep 就會誤中。

更麻煩的是比例:正式環境裡 CEL 的正常過濾(只收 main、擋掉 tag 與其他分支)佔絕大多數,簽章失敗是罕見事件。偽造嘗試會淹在正常過濾裡。

限制三:流水線的擁有者沒有權限讀它。

$ oc auth can-i get pods/log -n openshift-pipelines --as=system:serviceaccount:ci:pipeline
no
$ oc auth can-i get pods/log -n ci --as=system:serviceaccount:ci:pipeline
yes

一般已驗證使用者也是 no唯一能說出原因的訊號,落在流水線擁有者沒有權限的 namespace。

6.3 metrics 只能看出「有事件沒產生資源」

EventListener Service 的 9000 埠有 metrics,但它分不出原因:

四次重放(1 正確 + 3 被攔截):
  eventlistener_event_received_total{…,status="succeeded"}  4      ← 四次全是 succeeded
  eventlistener_triggered_resources_total                   1

那個 status 標籤反映的是 HTTP 層有沒有處理完,不是 trigger 的結果。真正能用的只有兩個 counter 的差值——「收到但什麼都沒產生」。

但差值也分不出原因。簽章被擋、CEL 正常過濾、資源建立失敗,三種在 metrics 上長得一模一樣。而且跟 6.2 的限制二一樣,正常的 CEL 過濾本來就會讓差值一直很大。

這個差值算術還有一個前提沒驗:triggered_resources_total 計的是資源數,一個 TriggerTemplate 若產生兩個資源,它會跳幾?我沒有測。要拿差值做告警的話,這個前提得先確認。

6.4 所以觀測性的現況是

訊號存在,但歸屬資訊與失敗原因分屬兩個元件、兩份 log、兩種 schema,而流水線擁有者只拿得到沒有原因的那一份。

這不是「沒有紀錄」,也不是「去看 core-interceptors 就好」。要在正式環境做到「偵測有人在偽造 Webhook」,這三個斷層都得自己補。


七、參數從哪來,決定爆炸半徑

第五節證明了「誰能觸發」是一個攻擊面。這一節是另一個獨立的攻擊面:觸發之後,Pipeline 用的參數是誰給的。

7.1 一個運氣

第五節那次成功的偽造觸發,Payload 裡帶的是:

"clone_url": "http://attacker/evil.git"

但實際跑出來的結果是:

URL    = http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/frontend-nx-mono.git
COMMIT = d5e9787079056cb7f34320c7f8402f904d9beea1

偽造的 URL 沒有被採用——因為 d15-happy-path 把 URL 寫死在 Task 層,Pipeline 頂層根本沒有宣告 spec.params

這是運氣,不是設計。git-url 開成 param、由 TriggerBinding 從 Payload 餵入,是幾乎所有教學的寫法。那樣寫的話,同一個偽造請求就會讓叢集去 clone 攻擊者指定的位址。

「誰能觸發」和「參數從哪來」是兩個獨立的問題,各自要各自的防護。

7.2 git-revision 傳分支名,不傳 SHA

Webhook Payload 裡有 body.after,直覺上可以直接餵給 git-clone 的 REVISION

原本的判斷是「DEPTH=1 的淺層拉取認不得孤立的 SHA」。這個判斷在 Gitea 1.27.0 上不成立。

三條 PipelineRun 串行送出(共用同一顆 RWO PVC),REVISION 各給一種值,DEPTH 全部維持 git-clone 的預設值 1

REVISION=main                    →  COMMIT=d5e9787079056cb7f34320c7f8402f904d9beea1   Succeeded
REVISION=d5e9787…(tip SHA)      →  COMMIT=d5e9787079056cb7f34320c7f8402f904d9beea1   Succeeded
REVISION=ec99892…(前一個 commit) →  COMMIT=ec99892fca81869e7ee37431b18b29b24bf7dd0b   Succeeded

三種都成功 checkout,results.COMMIT 精確等於指定的值——包括非 tip 的 SHA,而且是在 DEPTH=1 之下。這比「加大 depth 就能拿到」更強:淺層拉取本身不是障礙。

Gitea 的 app.ini 裡沒有 [git] 段、也沒有 uploadpack.allowAnySHA1InWant 之類的覆寫,所以這是預設行為,不是這台特別開過。

範圍限制:這個 repo 只有 2 個 commit,所以「舊 SHA」只測到往回一個。不要外推成「任意深度的歷史 SHA 都行」——更深的歷史、或伺服器端關掉 SHA1-in-want 的情況都沒有測。

結論不變,但理由要換。 傳分支名的好處不是「協定上非如此不可」,而是不依賴 Git 伺服器的 SHA1-in-want 政策。同一份 Pipeline 搬到 GitHub 或設定較嚴格的 GitLab 上,直接傳 SHA 可能會失敗,傳分支不會。

職責劃分也更乾淨:觸發期只負責「去哪裡拿」,「拿到了什麼」由執行 fetch 的 Task 自己回報:

value: $(tasks.git-clone.results.COMMIT)

7.3 Payload 來源的一個限制

用 Gitea 的 Test Delivery 測試時要知道:它與真實 push 的欄位結構完全相同(頂層 10 個、commits[] 子欄位 10 個、repository 子欄位 65 個,一個不多一個不少),但有兩處值的差異會影響行為:

  • before == after,所以 compare_url 退化成同一個 SHA 比自己
  • commits[].added / modified / removednull 而不是陣列

第二點最重要:要測「只有改到某些檔案才觸發」這種邏輯,Test Delivery 不夠,得真的推一個 commit。

另外,建立新分支的 push 是第三種形態:before 全零、commits 空陣列、total_commits: 0。TriggerBinding 若綁了 $(body.commits[0].id),這種事件會解析失敗。


收工前檢查清單

  • [ ] 三個資源鏈路完整:EventListener / TriggerBinding / TriggerTemplate 三者名稱與 ref 對得上,resourcetemplates 是全小寫。
  • [ ] RBAC 先確認再動手:在 OpenShift Pipelines 上用 pipeline SA 通常不必綁任何東西。要綁之前先用 ClusterRoleBinding 反查授權來源,不要用 oc auth can-i 的答案去推論原因
  • [ ] 不要無謂綁 -clusterroles:它的 secrets 沒有 resourceNames 限制,綁下去等於全叢集 Secret 讀取權。Secret 放在 EventListener 所在的 namespace 就夠。
  • [ ] 簽章驗證要有github ClusterInterceptor + Gitea 端的 Webhook Secret。只有 CEL 等於沒有驗來源。
  • [ ] 不要開 addChangedFiles / githubOwners:那兩個會呼叫 GitHub API。
  • [ ] workspace 用固定 PVCvolumeClaimTemplate + Retain 會讓每次觸發多一顆孤兒 PV。
  • [ ] Webhook 的 content_typejson:填 application/json 不報錯但會壞掉。
  • [ ] 參數來源要檢查:從 Payload 餵進 Pipeline 的每一個參數,都要當成攻擊者可控。
  • [ ] 知道 202 的意義:HTTP 回應碼不能當監控訊號,被擋與成功長得一樣。
  • [ ] 知道原因寫在哪tekton-triggers-core-interceptorsopenshift-pipelines),而且你的流水線 SA 讀不到。

外部參考文件

上游行為的依據

來源 用途
tektoncd/triggers issue #1465 Interceptor 失敗仍回 202、只在 info 等級記錄。2022-10 提出,被 stale bot 標記 lifecycle/rotten 自動關閉,無修補 PR
tektoncd/triggers issue #1183 資源建立失敗同樣靜默回 202;原報告的重現方式就是 generateName 用大寫
tektoncd/triggers config/200-clusterrole.yaml 兩份官方 ClusterRole 的權威定義(含 -clusterrolessecretsresourceNames 這件事)
Tekton EventListeners 文件 Service 預設 ClusterIP / 8080、el- 命名規則、202 回應格式、綁 ClusterRole 的官方指引
Tekton Interceptors 文件 github Interceptor 的 secretRef / eventTypes / addChangedFiles / githubOwners
Tekton Triggers Troubleshooting EventListener 自身 log 的欄位與等級調整方式(第六節的對照組)

Gitea 端

來源 用途
Gitea Webhooks 文件 三組簽章標頭的定義;未設 secret 時標頭仍在但 digest 為空這件事有官方明文
services/webhook/deliver.go 標頭的產生點原始碼:X-Gitea-Signature 無前綴、X-Hub-Signaturesha1=X-Hub-Signature-256sha256=
Gitea API 文件repoCreateHook 原始碼 POST /repos/{owner}/{repo}/hookstype / config.content_type / branch_filter 合法值

踩坑時會查到、但要小心的

來源 說明
triggers.tekton.dev/old-escape-quotes(另見 issue #1562issue #257 社群用來繞過「JSON/陣列/多行字串塞進 TriggerTemplate 造成 unmarshal 失敗」的 annotation。
PowerShell about_Character_Encoding 見「Character encoding in Windows PowerShell」一節:Windows PowerShell 的 Unicode 編碼一律帶 BOM,PowerShell 6+ 才預設 utf8NoBOM

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

尚未有邦友留言

立即登入留言