iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Kubernetes

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

Day 9 中繼驗證:Gitea webhook 送達叢集內的私有 ClusterIP

  • 分享至 

  • xImage
  •  

今日目的

Tekton 尚未部署,終點無法是「產生 PipelineRun」。本文的目的是在 Tekton 到位之前,先把 Gitea 側的投遞能力單獨驗過:

一次真實的 git push 觸發 webhook,請求抵達叢集內另一個 namespace 的私有 ClusterIP,payload 中的 SHA 與本機 HEAD 一致。

接收端是一次性的,驗完即刪。之後換成 Tekton EventListener 時,本文的 payload 可作為對照基準:屆時若失敗,比對本次結果即可分辨是投遞問題或接收問題。

不追求收斂。跑通一次、把待確認清單清掉即可,不需要像快樂路徑那樣多輪直跑無偏離。

先備知識

  • Gitea 已依快樂路徑部署完成,gitea_admin/hello 倉庫存在,本機有對應的工作目錄
  • $gtUrl$gtPass$caPath$gtCredInvoke-GiteaApi 沿用 Gitea 快樂路徑第 12 步(token 效期 8 小時,重開 PowerShell 要重新登入)
  • $OC$kubeconfig 沿用 CRC 那幾篇
  • Helm 可用,gitea-charts repo 已加入

開場對照表

常見預期 實測結果
API 回 204 就是送到了 204 只表示 Gitea 接受了投遞請求,送達與否需由接收端確認
送不到可以去 Gitea 的 log 找原因 container log 不記錄投遞成敗,hooks/{id}/deliverieshooks/{id}/history 兩個端點都是 404
clone_url 是建庫時寫進資料庫的 ROOT_URL 即時組合,修改並重啟後既有倉庫的回應隨之更新,不需重建倉庫
webhook payload 是單行 JSON 是多行美化 JSON,Where-Object { $_ -match '^\{' } 只會取到單獨一行的 {
rollout status 逾時就是失敗 可能仍在 Pulling image,需先看 Events 再判斷
ALLOWED_HOST_LIST 寫在 [webhook] 底下 已棄用,官方訊息要求改到 [security],目前靠 fallback 生效
ssh_url 的使用者是 git 是 OpenShift 指派的 UID:1000660000@gitea-...

實測環境版本表

項目 版本/值
執行日期 2026-08-07
主機 Windows 11 Pro
Shell PowerShell 5.1(非 7,本文的 JSON 處理與此有關)
叢集 CRC / OpenShift 4.21.14、Kubernetes 1.34.6
Gitea Helm chart gitea-charts/gitea 12.7.0
Gitea 應用版本 未記錄(chart 12.7.0 預設)
接收端映像 registry.access.redhat.com/ubi9/python-311(未固定 digest)
網路 行動熱點(影響第 3 步的映像拉取時間,見 §T7)

換版本就要重驗。以下所有結論只對這張表成立。


上半:步驟

0. 前提

目的:確認承接狀態,以及本文能否成立的那一個前提。

& $OC --kubeconfig $kubeconfig whoami
& $OC --kubeconfig $kubeconfig get pod -n gitea
& $OC --kubeconfig $kubeconfig get secret gitea-inline-config -n gitea -o jsonpath='{.data.webhook}' |
    ForEach-Object { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) }

實測輸出:

system:admin
三個 Pod 皆 1/1 Running
ALLOWED_HOST_LIST=loopback,private,external

第三行是本文能成立的前提。若讀到 external 或查無此 key,後續 webhook 會被 Gitea 攔下,而呼叫端仍收到 204,見 §T3。

變數

$gtNs    = "gitea"
$gtUser  = "gitea_admin"
$gtRepo  = "hello"
$sinkNs  = "webhook-sink"
$sinkUrl = "http://webhook-sink.$sinkNs.svc.cluster.local:8080/"

$d10WorkDir = Join-Path "$env:USERPROFILE\gitea-run" ("d10-" + (Get-Date -Format "yyyyMMdd-HHmmss"))
New-Item -ItemType Directory -Path $d10WorkDir -Force | Out-Null
"workDir = $d10WorkDir"

1. 設定 ROOT_URL

目的:讓 Gitea 產生的 URL 指向實際 Route。Chart 預設是 git.example.com,webhook payload 裡的 clone_url 由它產生。

$d10Values = @"
persistence:
  enabled: true
  storageClass: crc-csi-hostpath-provisioner
  size: 10Gi

gitea:
  admin:
    existingSecret: gitea-admin
  config:
    webhook:
      ALLOWED_HOST_LIST: loopback,private,external
    server:
      DOMAIN: gitea-gitea.apps-crc.testing
      ROOT_URL: https://gitea-gitea.apps-crc.testing/
      SSH_DOMAIN: gitea-gitea.apps-crc.testing

postgresql-ha:
  enabled: false
postgresql:
  enabled: true
valkey-cluster:
  enabled: false
valkey:
  enabled: true
"@
$d10ValuesPath = Join-Path $d10WorkDir "values.yaml"
[System.IO.File]::WriteAllText($d10ValuesPath, $d10Values, (New-Object System.Text.UTF8Encoding $false))
"bytes = $([System.IO.File]::ReadAllBytes($d10ValuesPath).Length)"

helm upgrade gitea gitea-charts/gitea -n $gtNs -f $d10ValuesPath --version 12.7.0
& $OC --kubeconfig $kubeconfig rollout status deployment/gitea -n $gtNs --timeout=180s

實測輸出:

bytes = 483
REVISION 2
deployment "gitea" successfully rolled out

PROTOCOL 維持 chart 預設的 http:Route 為 edge 終止,Pod 內部收 HTTP,只有 ROOT_URL 對外寫 https。此判斷未實測,若網頁出現 redirect 迴圈,先檢查這一項。

上述 webhook: 區段是本次實際執行的寫法,可運作,但 Gitea 已在 log 中標記棄用。建議的落點見 §T2。

2. 驗證 ROOT_URL 已落地

目的:分兩層看。Secret 是 Helm 寫入的結果,API 回應是 Gitea 實際使用的值,兩者不必然一致。

& $OC --kubeconfig $kubeconfig get secret gitea-inline-config -n $gtNs -o json | ConvertFrom-Json |
    ForEach-Object { $_.data.server } |
    ForEach-Object { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) }

(Invoke-GiteaApi -Url "$gtUrl/api/v1/repos/$gtUser/$gtRepo" | ConvertFrom-Json) |
    Select-Object clone_url, html_url, ssh_url

實測輸出:

DOMAIN=gitea-gitea.apps-crc.testing
ROOT_URL=https://gitea-gitea.apps-crc.testing/
SSH_DOMAIN=gitea-gitea.apps-crc.testing
PROTOCOL=http

clone_url : https://gitea-gitea.apps-crc.testing/gitea_admin/hello.git

hello 倉庫由快樂路徑第六輪建立,當時 ROOT_URL 仍是 git.example.com,現在的 API 回應已是新網域。說明見 §T5。

3. 建立接收端

目的:提供一個位於另一個 namespace、只印出請求內容的 HTTP 接收端。跨 namespace 為刻意設計,理由見 §T1。

& $OC --kubeconfig $kubeconfig create namespace $sinkNs

$sinkPy = @"
from http.server import BaseHTTPRequestHandler, HTTPServer

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(n).decode('utf-8', 'replace')
        print('=== %s %s' % (self.command, self.path), flush=True)
        for k in ('X-Gitea-Event', 'X-Gitea-Delivery', 'Content-Type'):
            print('%s: %s' % (k, self.headers.get(k)), flush=True)
        print(body, flush=True)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'ok')

    def log_message(self, *a):
        pass

HTTPServer(('0.0.0.0', 8080), H).serve_forever()
"@
$sinkPyPath = Join-Path $d10WorkDir "sink.py"
[System.IO.File]::WriteAllText($sinkPyPath, $sinkPy, (New-Object System.Text.UTF8Encoding $false))

& $OC --kubeconfig $kubeconfig create configmap webhook-sink-src `
    --from-file=sink.py=$sinkPyPath -n $sinkNs
$sinkManifest = @"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webhook-sink
  namespace: $sinkNs
spec:
  replicas: 1
  selector:
    matchLabels: { app: webhook-sink }
  template:
    metadata:
      labels: { app: webhook-sink }
    spec:
      containers:
        - name: sink
          image: registry.access.redhat.com/ubi9/python-311
          command: ["python", "/src/sink.py"]
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: src
              mountPath: /src
      volumes:
        - name: src
          configMap:
            name: webhook-sink-src
---
apiVersion: v1
kind: Service
metadata:
  name: webhook-sink
  namespace: $sinkNs
spec:
  selector: { app: webhook-sink }
  ports:
    - port: 8080
      targetPort: 8080
"@
$sinkPath = Join-Path $d10WorkDir "sink.yaml"
[System.IO.File]::WriteAllText($sinkPath, $sinkManifest, (New-Object System.Text.UTF8Encoding $false))
& $OC --kubeconfig $kubeconfig apply -f $sinkPath
& $OC --kubeconfig $kubeconfig rollout status deployment/webhook-sink -n $sinkNs --timeout=600s
& $OC --kubeconfig $kubeconfig get pod -n $sinkNs -o custom-columns=NAME:.metadata.name,SCC:.metadata.annotations."openshift\.io/scc",STATUS:.status.phase

實測輸出:

SCC = restricted-v2 / STATUS = Running

未指定 securityContext,由 restricted-v2 指派任意 UID,UBI python-311 仍可啟動並監聽 8080。

逾時不等於失敗。本次首輪使用 --timeout=180s,回 error: timed out waiting for the condition,Pod 停在 ContainerCreating,實際情況是映像尚未拉取完成,續等至第 276 秒轉為 Running。上述指令已將 timeout 放寬為 600s,判斷方式見 §T7。

4. 確認接收端可達且位址為私有

目的:排除「webhook 沒送達是因為接收端本身不通」。本步通過而後續 webhook 仍失敗,可將問題範圍縮小到 Gitea 的白名單。

& $OC --kubeconfig $kubeconfig get svc webhook-sink -n $sinkNs -o custom-columns=NAME:.metadata.name,CLUSTERIP:.spec.clusterIP,PORT:.spec.ports[0].port

$gtPod = & $OC --kubeconfig $kubeconfig get pod -n $gtNs -l app.kubernetes.io/name=gitea `
    -o jsonpath='{.items[0].metadata.name}'
& $OC --kubeconfig $kubeconfig exec $gtPod -n $gtNs -- `
    curl -s -o /dev/null -w "from gitea pod: %{http_code}\n" -X POST $sinkUrl

實測輸出:

CLUSTERIP = 10.217.5.179        ← RFC1918,白名單管轄的範圍
from gitea pod: 200

Gitea Pod 內有 curl,不需改用 wget

5. 建立 webhook

目的:註冊 webhook。URL 使用 CoreDNS 服務名稱,不使用 Route。

$hookBody = [ordered]@{
    type   = "gitea"
    active = $true
    events = @("push")
    config = [ordered]@{
        url          = $sinkUrl
        content_type = "json"
    }
} | ConvertTo-Json -Depth 10 -Compress
$hookBody

$hookRaw = Invoke-GiteaApi -Url "$gtUrl/api/v1/repos/$gtUser/$gtRepo/hooks" -Method POST -JsonBody $hookBody
"exit=$lastExit"
$hookRaw
$hook = $hookRaw | ConvertFrom-Json
$hookId = $hook.id
"hookId = $hookId"

實測輸出:

exit=0
{"id":1,"name":"","type":"gitea","branch_filter":"",
 "config":{"url":"http://webhook-sink.webhook-sink.svc.cluster.local:8080/","content_type":"json"},
 "events":["push"],"authorization_header":"","active":true,...}

PowerShell 5.1 的 ConvertTo-Json 預設 -Depth 2config 這層位於邊界附近,因此指定 -Depth 10,見 §T4。

6. 觸發:測試投遞

目的:先用測試投遞確認鏈路。此端點回 204 且無 body,不代表送達

Invoke-GiteaApi -Url "$gtUrl/api/v1/repos/$gtUser/$gtRepo/hooks/$hookId/tests" -Method POST
"exit=$lastExit"
Start-Sleep -Seconds 5
& $OC --kubeconfig $kubeconfig logs deployment/webhook-sink -n $sinkNs --tail=40

實測輸出:

HTTP=204,body 空

=== POST /
X-Gitea-Event: push
X-Gitea-Delivery: ec690cdf-…
Content-Type: application/json
{ …完整 payload… }

接收端有 log 才算送達。若 exit=0 但接收端無輸出,先執行第 9 步取得 Gitea 的 container log:訊息留在投遞端,不在接收端。

7. 觸發:真實 push

目的:確認由實際的 git 操作觸發,而非僅測試端點可用。

$sinkBefore = (& $OC --kubeconfig $kubeconfig logs deployment/webhook-sink -n $sinkNs | Measure-Object -Line).Lines
"before lines = $sinkBefore"

# 路徑為 Gitea 快樂路徑第 14 步建立的本機倉庫
Push-Location "<Gitea 快樂路徑的 workDir>\hello"
$credFile = Join-Path $env:TEMP ("gt-cred-{0}" -f [guid]::NewGuid())
$credArg  = "store --file='" + $credFile.Replace([char]92, [char]47) + "'"
try {
    [System.IO.File]::WriteAllText($credFile,
        "$gtUrl".Replace("https://", "https://${gtUser}:${gtPass}@") + "`n",
        (New-Object System.Text.UTF8Encoding $false))
    Add-Content -Path "README.md" -Value "webhook test $(Get-Date -Format o)"
    git -c user.name="deployer" -c user.email="deployer@local" add README.md
    git -c user.name="deployer" -c user.email="deployer@local" commit -q -m "trigger webhook"
    git -c credential.helper= -c credential.interactive=false `
        -c credential.helper="$credArg" push origin main
    "push exit = $LASTEXITCODE"
    $pushSha = git rev-parse HEAD
    "push SHA = $pushSha"
} finally {
    if (Test-Path $credFile) { Remove-Item $credFile -Force }
    Pop-Location
}

實測輸出:

190c3b4..0b7293e  main -> main
push exit = 0
push SHA  = 0b7293e3452a8a5d699da59aba83fdaefb8c2f15

8. 從接收端核對

目的:以接收端的 log 確認送達,並比對 payload 中的 SHA 與本機一致。這是本文的終點判定。

Start-Sleep -Seconds 5
$sinkLog = & $OC --kubeconfig $kubeconfig logs deployment/webhook-sink -n $sinkNs
"after lines = $(($sinkLog | Measure-Object -Line).Lines)"

# Gitea 送出的是多行美化 JSON,不是單行。
# 取最後一個 POST 區塊,自其中第一個獨立的 `{` 起合併到結尾
$starts     = @(0..($sinkLog.Count-1) | Where-Object { $sinkLog[$_] -match '^=== POST' })
$blockStart = $starts[-1]
$jsonStart  = ($blockStart..($sinkLog.Count-1) | Where-Object { $sinkLog[$_].Trim() -eq '{' } | Select-Object -First 1)
$payloadLine = ($sinkLog[$jsonStart..($sinkLog.Count-1)] -join "`n")

[System.IO.File]::WriteAllText((Join-Path $d10WorkDir "payload.json"), $payloadLine,
                               (New-Object System.Text.UTF8Encoding $false))
"payload bytes = $([System.IO.File]::ReadAllBytes((Join-Path $d10WorkDir 'payload.json')).Length)"

$payload = $payloadLine | ConvertFrom-Json
"after      = $($payload.after)"
"before     = $($payload.before)"
"local SHA  = $pushSha"
"SHA match  = $($payload.after -eq $pushSha)"
"clone_url  = $($payload.repository.clone_url)"
"html_url   = $($payload.repository.html_url)"
"ssh_url    = $($payload.repository.ssh_url)"

實測輸出:

payload bytes = 6165
after      = 0b7293e3452a8a5d699da59aba83fdaefb8c2f15
before     = 190c3b4a…
local SHA  = 0b7293e3452a8a5d699da59aba83fdaefb8c2f15
SHA match  = True                              ← 終點判定
clone_url  = https://gitea-gitea.apps-crc.testing/gitea_admin/hello.git
html_url   = https://gitea-gitea.apps-crc.testing/gitea_admin/hello
ssh_url    = 1000660000@gitea-gitea.apps-crc.testing:gitea_admin/hello.git

clone_urlhtml_url 和第 2 步的 API 回應一致,兩處同源。ssh_url 中的數字為 OpenShift 指派的 UID,見 §T6。

上述解析寫法較冗長,原因見 §T4。

9. 從 Gitea 端核對

目的:確認投遞端也認為成功。與第 8 步互為獨立來源,無論第 8 步成功與否都要執行。

& $OC --kubeconfig $kubeconfig logs $gtPod -n $gtNs --tail=80 | Select-String "webhook|hook"

實測輸出:

POST /api/v1/repos/gitea_admin/hello/hooks          201 Created
POST /api/v1/repos/gitea_admin/hello/hooks/1/tests  204 No Content
POST /api/internal/hook/pre-receive/…               200 OK
POST /api/internal/hook/post-receive/…              200 OK

modules/setting/webhook.go:38:loadWebhookFrom() [E] Deprecation: config option
`[webhook].ALLOWED_HOST_LIST` present, please use `[security].ALLOWED_HOST_LIST`
instead because this fallback will be/has been removed in v28.0.0

輸出中沒有任何一行說明投遞成功或失敗。另外測試兩個候選端點:

GET /api/v1/repos/gitea_admin/hello/hooks/1/deliveries  → 404
GET /api/v1/repos/gitea_admin/hello/hooks/1/history     → 404

本步結果的影響見 §T3,棄用警告見 §T2。

10. 收工與清除

目的:接收端是一次性的,驗完即刪。

Get-ChildItem $d10WorkDir | Select-Object Name, Length
& $OC --kubeconfig $kubeconfig get all -n $sinkNs
Get-ChildItem $env:TEMP -Filter "gt-*" | Select-Object Name
& $OC --kubeconfig $kubeconfig delete namespace $sinkNs
Invoke-GiteaApi -Url "$gtUrl/api/v1/repos/$gtUser/$gtRepo/hooks/$hookId" -Method DELETE
"exit=$lastExit"

實測輸出:

產物  values.yaml     483
      sink.py         655
      sink.yaml       780
      payload.json  6,165

刪除  namespace/webhook-sink(含 Deployment、Service、ConfigMap)
      webhook id=1(DELETE → 204,剩餘 hooks = [])
      $env:TEMP 下的 gt-* 與 d10-* 暫存皆 0 殘留

webhook-sink 無 PVC,不會產生孤兒目錄。

ROOT_URL 的設定保留不還原,Tekton 那篇會用到。


收工前檢查清單

  • [ ] 第 0 步讀到的是 ALLOWED_HOST_LIST=loopback,private,external,不是 external
  • [ ] helm upgrade 後 REVISION 有進位,rollout status 有回 successfully rolled out
  • [ ] 第 2 步的 API clone_url 已經是 Route 網域,不是 git.example.com
  • [ ] sink Pod Running,SCC 為 restricted-v2
  • [ ] Service 的 ClusterIP 落在 10.217.x.x(RFC1918),且 Gitea Pod 內 curl 得到 200
  • [ ] 建立 webhook 回 201,拿得到 hookId
  • [ ] 測試投遞後,接收端 log 出現 === POST /X-Gitea-Event: push
  • [ ] push exit = 0,且 SHA match = True
  • [ ] payload.json 已落地(本次 6,165 bytes),可作為 Tekton 那篇的對照基準
  • [ ] webhook-sink namespace 已刪、webhook 已刪、gt-* 暫存無殘留
  • [ ] ROOT_URL 設定沒有被還原

下半:技術分析

T1. 為什麼終點要選在「另一個 namespace 的私有 ClusterIP」

這個終點同時涵蓋三項條件:

  1. 跨 namespace:Service 的 ClusterIP 落在 10.217.x.x,屬 RFC1918 私有位址,也就是 Gitea ALLOWED_HOST_LIST 管轄的範圍。改用同 namespace 或 localhost 測試會繞過這項限制。
  2. svc.cluster.local 而不是 Route:叢集內部溝通不需繞經外部 Ingress,也避開自簽憑證造成的 TLS 失敗。後續 Tekton EventListener 的呼叫方式相同。
  3. 接收端只印出請求內容log_message 覆寫為空,使 stdout 只保留自行印出的內容,讓第 8 步的解析有明確邊界。

T2. ALLOWED_HOST_LIST 的位置已棄用

第 9 步的輸出中包含一行棄用警告:

Deprecation: config option `[webhook].ALLOWED_HOST_LIST` present,
please use `[security].ALLOWED_HOST_LIST` instead
because this fallback will be/has been removed in v28.0.0

也就是說,本文第 1 步的寫法:

gitea:
  config:
    webhook:
      ALLOWED_HOST_LIST: loopback,private,external

應該改成:

gitea:
  config:
    security:
      ALLOWED_HOST_LIST: loopback,private,external

兩件事要分開:

  • 目前靠 fallback 仍生效,本次投遞成功即為其生效的證據。
  • 改到 security 之後是否等效,本次未實測。官方訊息已指出替代路徑,但未實際執行。若要改,重跑第 0 步與第 6 步即可確認。

此訊息為 [E] 等級,夾在 access log 之間,需看完整輸出才會注意到。

T3. 204 不是送達,而且投遞端不留紀錄

第 6 步的測試投遞端點回 204 No Content,body 空。其語意是 Gitea 接受了投遞請求,不是接收端收到了。送達的證據只存在於接收端的 stdout。

第 9 步的結果進一步限制了排查方式:

  • Gitea 的 container log 只有 API 請求本身的存取紀錄,沒有任何一行寫投遞成功或失敗
  • hooks/{id}/deliverieshooks/{id}/history 兩個候選 API 端點都是 404
  • 網頁的 Recent Deliveries 看得到,但沒有對應的 API

結論:

接收端的 log 是「投遞是否送達」唯一可程式化取得的證據。

這影響 Tekton 那篇的排查順序。若 EventListener 沒收到事件,Gitea 側查不到線索,只能從接收端反推,或開網頁看 Recent Deliveries。因此排查應從 EventListener 側著手,而不是先翻 Gitea 的 log。

白名單擋下投遞時的失敗形狀是:呼叫端 204、接收端無輸出、投遞端無紀錄。三者都不會產生錯誤訊息,因此第 0 步要單獨確認前提。

T4. PowerShell 5.1 的兩個坑

其一:payload 是多行美化 JSON。

原本的寫法:

$payloadLine = $sinkLog | Where-Object { $_ -match '^\{' } | Select-Object -Last 1

此寫法成立的前提是接收端輸出單行 JSON。Gitea 送出的是多行美化 JSON,oc logs 回傳的又是逐行字串陣列,因此取到的是單獨一行的 {ConvertFrom-Json 隨即失敗。

實際採用的作法是先定位區塊,再整段合併:

$starts     = @(0..($sinkLog.Count-1) | Where-Object { $sinkLog[$_] -match '^=== POST' })
$blockStart = $starts[-1]
$jsonStart  = ($blockStart..($sinkLog.Count-1) | Where-Object { $sinkLog[$_].Trim() -eq '{' } | Select-Object -First 1)
$payloadLine = ($sinkLog[$jsonStart..($sinkLog.Count-1)] -join "`n")

=== POST 這個標記的作用是切分區塊。接收端若不印出,多次投遞的輸出會混在一起,無法界定最後一次的範圍。

另一個方向是讓接收端輸出單行:

print(json.dumps(json.loads(body), separators=(',',':')), flush=True)

不建議。這會讓接收端多做一次解析,不再是原樣印出。維持原樣輸出、由讀取端合併較符合本文的用途。

其二:ConvertTo-Json-Depth

PowerShell 5.1 的 ConvertTo-Json 預設 -Depth 2,超過深度的物件會被序列化為型別名稱字串,而不是巢狀 JSON。第 5 步的 config 位於此邊界附近,因此指定 -Depth 10。若未指定而序列化不完整,Gitea 回的是欄位格式錯誤,訊息本身不會指向深度設定。

T5. URL 是即時組合,不是建庫時寫入

第 2 步原本要回答的問題是:hello 這個倉庫是在 ROOT_URL 還是 git.example.com 的時候建的,那些 URL 會不會已經寫死在資料庫裡?如果是,就得重建倉庫。

實測結果為即時組合:helm upgrade 設定 ROOT_URL 並重啟後,既有倉庫的 API 回應隨即變為新網域,Gitea 啟動 log 亦可佐證:

cmd/web.go:333:listen() [I] AppURL(ROOT_URL): https://gitea-gitea.apps-crc.testing/

因此不需要重建倉庫。第 8 步 payload 中的三個 URL 與第 2 步的 API 回應一致,兩處同源。後續變更網域時,修改 ROOT_URL 並重啟即可,既有倉庫不會殘留舊值。

T6. ssh_url 中的使用者名稱是 UID

ssh_url = 1000660000@gitea-gitea.apps-crc.testing:gitea_admin/hello.git

正常應為 git@host。容器以 restricted-v2 指派的任意 UID 執行,/etc/passwd 中沒有對應項目,Gitea 取不到使用者名稱,因而填入數字 UID。

第 2 步的 API 回應與第 8 步的 payload 兩處數值一致,確認同源,非單點異常。

影響範圍

  • HTTP clone 不受影響,本次 push 即為證明
  • SSH clone 會拿到這個位址,是否可用未實測
  • 若後續要使用 SSH,需設定 chart 的 RUN_USER,或改用 HTTP

本系列的 pipeline 使用 HTTP clone,因此僅記錄不處理。這也是 SCC 影響範圍的一個例子:指派的 UID 不只決定 Pod 能否啟動,也會出現在應用程式產生的資料中。

T7. rollout status 逾時不等於失敗

第 3 步首輪用 --timeout=180s,回了:

error: timed out waiting for the condition

Pod 停在 ContainerCreatingoc describe 的 Events 顯示:

Normal  Pulling  3m16s  kubelet  Pulling image "registry.access.redhat.com/ubi9/python-311"

映像仍在下載中。續等至第 276 秒轉為 Running,總計約 8 分鐘。

執行環境註記:本次執行時主機使用行動熱點連線,映像下載速度低於一般固網。8 分鐘不具代表性,一般網路下 UBI python-311 的首次拉取應快於此。因此 --timeout=180s 在一般網路下是否足夠,本次無法判斷。步驟中放寬為 600s 屬保守處置,不是實測結論。

判斷順序:

  1. 先看 oc describe pod 的 Events,出現 Pulling image 表示仍在下載,續等即可
  2. 確定起不來時,記下 oc describe podoc logs 的輸出再往下判斷
  3. 不要先換映像:拉取失敗、權限不足、python 執行錯誤三者的處置不同,換映像會讓原本的訊息消失

本次一併確認一項原本未實測的項目:UBI python-311 可在 restricted-v2 指派的任意 UID 下執行,不指定 securityContext 亦可啟動並監聽 8080。


本文的界線

以下項目本文未涵蓋或未驗證:

  • ROOT_URL 設錯的實際後果未驗證。 本文只驗證 payload 中的值正確。pipeline 是否會因此 clone 到錯誤網域,要等 Tekton 在場才驗得到。
  • 失敗重現不在本文範圍。 未把 ALLOWED_HOST_LIST 改回 external 觀察「204 加空白接收端」。§T3 對失敗形狀的描述為推論,非實測。
  • port-forward 的三個陷阱未在此環境重驗。
  • 接收端不是 Tekton。 本文驗證的是 Gitea 送得出去,不是 Tekton 收得到。
  • gitea.config.security.ALLOWED_HOST_LIST 未實測是否等效。(§T2)
  • ssh_url 的 UID 使用者名稱是否會讓 SSH clone 失敗,未實測。(§T6)
  • --timeout=180s 在一般網路下是否足夠,未驗證。 本次為行動網路,無法判斷。(§T7)

收尾

本文沒有部署長期存在的元件,webhook-sink 與 webhook 都在驗證後刪除。保留下來的是 ROOT_URL 設定,以及 6,165 bytes 的 payload.json

這兩項供下一篇使用。Tekton EventListener 接上後若事件沒進來,可先排除「Gitea 送不出去」這一項,並依 §T3 從接收端側著手排查。

把投遞與接收分兩次驗證,目的是在失敗時縮小需要檢查的範圍。


上一篇
Day 8:Gitea部署到 git push
下一篇
Day 10:把憑證擋在版控之外 —— Sealed Secrets 的加密、綁定與離線復原
系列文
防範軟體供應鏈攻擊:從零打造具備硬性阻擋能力的雲原生 CI/CD 流水線11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言