iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0

https://ithelp.ithome.com.tw/upload/images/20260830/20183337NCjlK8FVvn.png
映像簽章能證明「這個產物通過核准」,但回答不了「它是誰、用什麼、從哪個來源建出來的」。

補上這一塊的東西叫 Provenance Attestation——一份跟著映像走、
記錄建構血統的證明文件。Tekton Chains 會在每個 TaskRun 跑完之後自動產生它。

讀完你能做到:讓 Chains 為每次建置產出簽章過的 SLSA Provenance、推進自架 registry,
在管道裡加一道門檻確保它真的存在,並讓准入控制器拒絕沒有 provenance 的映像。

前提

① 一條 Tekton 管道,其中有 Task 會產出名為 IMAGE_URL / IMAGE_DIGEST 的 result
② OpenShift Pipelines Operator(Chains 是它的內建元件)
③ 一個私有 registry,而且叢集內有一個可以連的位址
④ 叢集裡有 Kyverno(第 4 步要用)
⑤ 簽章流程沒有上傳 transparency log

第 ⑤ 點會決定政策怎麼寫,§6 有說明。

環境:OpenShift 4.21.14、Kubernetes v1.34.6、OpenShift Pipelines Operator v1.23.1、
Tekton Chains v0.27.4、Kyverno v1.19.0。

Operator 的 operator.tekton.dev/release label 跟 Chains 的版本是兩件事,
而這個版本差別直接決定了 §11 那一節的結果:

oc get deploy tekton-chains-controller -n openshift-pipelines \
  -o jsonpath='{.metadata.labels.version}'
# v0.27.4

1. 三分鐘版

① generateSigningSecret: true                → 10 秒產出金鑰,不用寫 Job
② storage.oci.repository 指到內部位址          → ⚠️ 要配 insecure,而它會跳過 TLS 驗證
③ 驗收:去 registry 數 .att 的數量             → annotation 說 signed=true 不算數
④ 加一個 wait-for-attestation Task            → Chains 不在 DAG 裡,要自己補一條邊
⑤ 准入政策要求 SLSA Provenance                → ⚠️ 只有舊 API 做得到,見 §11

四個會咬人的地方,理由都在 Part 2:

insecure: true 就是關掉憑證驗證 沒有「只要 http scheme 不關驗證」這個選項(§3.1)
signed=true 不代表簽好了 完全沒有 signer 時它也是 true(§8)
attestation 證明「宣稱」不是「做過」 任何 TaskRun 都能為任意 digest 鑄造 provenance(§10)
新的政策 API 在這個組合下驗不了 對有沒有 attestation 都回 fail,而成因未定(§11)

Part 1 — 快樂路徑

2. 第 1 步:開 signer

Chains 可能已經在跑了。先確認它現在在做什麼:

oc get tektonconfig config -o jsonpath='{.spec.chain}' | tr ',' '\n'
oc logs -n openshift-pipelines deploy/tekton-chains-controller --tail=200 \
  | grep -i 'signer\|private key'

沒有金鑰的時候,日誌長這樣:

error configuring x509 signer: no valid private key found, looked for: [x509.pem, cosign.key]
No signer x509 configured for tekton

Operator 有一個布林值可以直接產金鑰:

oc patch tektonconfig config --type=merge \
  -p '{"spec":{"chain":{"generateSigningSecret":true}}}'

10 秒後:

oc get secret signing-secrets -n openshift-pipelines \
  -o go-template='{{range $k,$v := .data}}{{$k}} {{end}}'
# cosign.key cosign.password cosign.pub

cosign.key 正是日誌裡說它要找的名字。

2.1 金鑰必須跟映像簽章那把分開

建構來源證明和映像簽章分開用兩把私鑰,
因為共用一把的話,那把外洩就能同時偽造「通過核准」和「建構血統」兩層——
兩層驗證就退化成一層。驗收方式是逐位元組比對:

oc get secret signing-secrets -n openshift-pipelines \
  -o jsonpath='{.data.cosign\.pub}' | base64 -d | sha256sum
oc get secret <你的映像簽章金鑰> -n <管道的 namespace> \
  -o jsonpath='{.data.cosign\.pub}' | base64 -d | sha256sum
# 兩個雜湊必須不同

再確認跑 build 的 ServiceAccount 讀不到 attestation 那把:

oc auth can-i get secret/signing-secrets -n openshift-pipelines \
  --as=system:serviceaccount:<管道 namespace>:<SA 名稱>
# no

3. 第 2 步:讓 Chains 推得到你的 registry

Chains 預設把 attestation 推到跟映像同一個位置。如果那個位址用的是
內部 CA 或自簽憑證,就會撞到:

error getting signed image: Get "https://registry.example.com/v2/":
    tls: failed to verify certificate: x509: certificate signed by unknown authority

Chains controller 讀 SSL_CERT_DIR,而 OpenShift 已經幫它掛好了一個
會自動同步的信任區(config-trusted-cabundle,帶
config.openshift.io/inject-trusted-cabundle=true 標籤)。把 CA 加進叢集 Proxy 的
trustedCA 就會流進來,而且會跟著輪替。

⚠️ 但先量一下那個改動有多大:

oc get cm -A -l config.openshift.io/inject-trusted-cabundle=true --no-headers | wc -l

實測是 23 個,涵蓋 openshift-kube-apiserveropenshift-authentication
openshift-image-registryopenshift-console。為了讓一個 controller
連得到 registry 而去動這些,不成比例。先數一次再決定。

3.1 先把 insecure 的語意釘死

上游文件的警告框寫的是:

allows connecting to OCI registries without TLS certificate verification

實測 2×2 證實了這個說法,而且比文件更精確:

位址 insecure 結果
對外 route(HTTPS,自簽憑證) false x509: certificate signed by unknown authority
對外 route(HTTPS,自簽憑證) true ✅ 成功 ← 這就是它跳過憑證驗證的證據
內部 svc(純 HTTP :8081) false ❌ 失敗(不會自己退回 http scheme)
內部 svc(純 HTTP :8081) true ✅ 成功

兩件事同時成立:

  1. insecure: true 確實跳過 TLS 憑證驗證——上游的 MITM 警告是實話
  2. 純 HTTP 的端點也需要這個旗標,它不會自己退回 http

所以「我只是要 http scheme,沒要關驗證」這種說法不成立——
同一個旗標,關不掉其中一半。

這個旗標本身綁版本:feat(oci): support insecure OCI registry(#1374)
進 v0.27.0,之後還有 fix: pass insecure option to name.NewDigest for OCI storage(#1684)。
舊版可能根本沒有這個 key,或行為不一致。

3.2 三條路,選一個

做什麼 代價
(a) 內部位址 + insecure: true 走 ClusterIP 純 HTTP 沒有 TLS。攻擊面是叢集 SDN,流量不經過 router
(b) 對外位址 + insecure: true 建立 TLS 但不驗憑證 ⚠️ 最差:付了 TLS 的成本又沒得到 TLS 的保護
(c) 把 CA 加進叢集信任區 不用 insecure 唯一不弱化 TLS 的路,但動到上面那 23 個 ConfigMap

本文選 (a),理由是流量不離開叢集 SDN,而且不動全叢集的信任區。

⚠️ 但這仍然是弱化。一篇講供應鏈安全的文件不該假裝它不是。
不能接受的話,答案是 (c)——而且要先把那 23 個 ConfigMap 數一次、確認自己能接受。

3.3 設定本體

oc get svc <registry> -n <ns> -o jsonpath='{.spec.ports[*].name}'
# http
cat > /tmp/chain.json <<'JSON'
{"spec":{"chain":{
  "transparency.enabled": "false",
  "storage.oci.repository": "nexus-nexus3.nexus-proxy.svc.cluster.local:8081/docker-hosted/web",
  "storage.oci.repository.insecure": true
}}}
JSON
oc patch tektonconfig config --type=merge --patch-file=/tmp/chain.json

storage.oci.repository 吃完整的 host:port/repo/name 路徑。
attestation 會落進那個 repo——跟映像同一個 repo,只是用另一個名字寫進去,
所以從對外位址讀一樣找得到。

transparency.enabled 的預設值本來就是 "false"。顯式寫進去是宣告意圖,
防止未來預設值改變。

4. 驗收:.att 真的在嗎

這一步不能省,而且不能看 annotation。 理由見 §8。

# 直接數 registry 裡的 tag
curl -su "$USER:$PASS" https://registry.example.com/v2/<repo>/tags/list \
  | tr ',' '\n' | grep -c '\.att'

指定 digest 檢查:

DIG=sha256:05d42a95...
curl -su "$USER:$PASS" -o /dev/null -w '%{http_code}\n' \
  -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
  "https://registry.example.com/v2/<repo>/manifests/sha256-${DIG#sha256:}.att"
# 200

⚠️ 不要為了測這個去跑一次完整管道。Chains 認的只是兩個 result 名稱,
用一個 30 秒的探針就能單獨驗證憑證與位址這一段——見 §9。

5. 第 3 步:補一條 DAG 邊

Chains 是 controller,不是 Task。它在 TaskRun 完成之後才動作,
而 DAG 完全不知道它的存在:

DAG 這一側:  build-push → ... → update-gitops → wait-for-sync
                  │                                    │
                  └── 沒有任何一條邊連到下面 ──────┐   │
                                                   ↓   ↓
Chains 這一側:  (看到 TaskRun 完成)→ 簽 → 推 .att

只要准入層開始要求 attestation,這條沒有邊的路就是競態。
wait-for-attestation 把它變成顯性的門檻:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: wait-for-attestation
spec:
  params:
    - name: IMAGE                    # 內部位址 + @digest
    - name: TIMEOUT_SECONDS
      default: "180"
    - name: INTERVAL_SECONDS
      default: "5"
  steps:
    # cosign 映像是 distroless,沒有 shell,所以輪詢要另開一步
    - name: wait-for-upload
      image: image-registry.openshift-image-registry.svc:5000/openshift/cli:latest
      env:
        - { name: REF,              value: $(params.IMAGE) }
        - { name: TIMEOUT_SECONDS,  value: $(params.TIMEOUT_SECONDS) }
        - { name: INTERVAL_SECONDS, value: $(params.INTERVAL_SECONDS) }
      volumeMounts:
        - { name: dockercfg, mountPath: /dockercfg }
      script: |
        #!/bin/sh
        set -e
        HOST="${REF%%/*}"; REST="${REF#*/}"; REPO="${REST%@*}"
        DIG="${REF#*@}"; TAG="sha256-${DIG#sha256:}.att"

        # dockerconfigjson 的 auth 欄位本身就是 base64(user:pass)
        AUTH=$(python3 - "$HOST" <<'PY'
        import base64, json, sys
        d = json.load(open("/dockercfg/config.json"))["auths"][sys.argv[1]]
        a = d.get("auth")
        if not a:
            a = base64.b64encode(("%s:%s" % (d["username"], d["password"])).encode()).decode()
        print(a)
        PY
        )
        START=$(date +%s)
        while : ; do
          CODE=$(curl -s -o /dev/null -w '%{http_code}' \
                   -H "Authorization: Basic $AUTH" \
                   -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
                   "http://$HOST/v2/$REPO/manifests/$TAG")
          ELAPSED=$(( $(date +%s) - START ))
          [ "$CODE" = "200" ] && { echo "attestation 已就緒(等了 ${ELAPSED} 秒)"; exit 0; }
          [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ] && {
            echo "逾時,$TAG 沒有出現(最後 HTTP $CODE)"
            echo "→ 查 tekton-chains-controller 的日誌"; exit 1; }
          echo "  [${ELAPSED}s] HTTP $CODE,繼續等"
          sleep "$INTERVAL_SECONDS"
        done

    - name: verify
      image: <你的 cosign 映像>
      env:
        - { name: DOCKER_CONFIG, value: /dockercfg }
      volumeMounts:
        - { name: dockercfg,  mountPath: /dockercfg }
        - { name: chains-pub, mountPath: /keys }
      command: ["/ko-app/cosign"]
      args:
        - verify-attestation
        - --key=/keys/cosign.pub
        - --type=slsaprovenance
        - --insecure-ignore-tlog
        - --allow-insecure-registry
        - $(params.IMAGE)

  volumes:
    - name: dockercfg
      secret:
        secretName: <registry 憑證>
        items: [{ key: .dockerconfigjson, path: config.json }]
    - name: chains-pub
      configMap: { name: chains-pub }

公鑰用 ConfigMap 送進去(公鑰本來就是公開的):

oc get secret signing-secrets -n openshift-pipelines \
  -o jsonpath='{.data.cosign\.pub}' | base64 -d > chains.pub
oc create configmap chains-pub -n <管道 namespace> --from-file=cosign.pub=chains.pub

接進管道,插在簽章與部署之間:

- name: wait-for-attestation
  runAfter: [cosign-sign]
  taskRef: { name: wait-for-attestation }
  params:
    - name: IMAGE
      value: <內部位址>/<repo>@$(tasks.build-push.results.IMAGE_DIGEST)

- name: update-gitops
  runAfter: [wait-for-attestation]     # 原本是 [cosign-sign]

第二個 step 存在的理由見 §12:只檢查檔案在不在是不夠的。

6. 第 4 步:准入層要求 provenance

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-web-attestation
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: require-slsa-provenance
      match:
        any:
          - resources: { kinds: [Pod], namespaces: [demo] }
      verifyImages:
        - imageReferences: ["registry.example.com/<repo>*"]
          mutateDigest: false          # digest 由部署 manifest 決定,不要動它
          verifyDigest: false
          required: true
          useCache: false
          imageRegistryCredentials:
            secrets: [<registry 憑證>]
          attestations:
            - type: https://slsa.dev/provenance/v0.2
              attestors:
                - count: 1
                  entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          <signing-secrets 的 cosign.pub>
                          -----END PUBLIC KEY-----
                        # 兩個都要 —— 沒有 transparency log 可查
                        rekor: { ignoreTlog: true }
                        ctlog: { ignoreSCT: true }

這裡刻意用了一個已經標記淘汰的 API。
實測下來,本文這個組合(Chains v0.27.4 + Kyverno v1.19.0 + tlog 關閉)
用現行的 ImageValidatingPolicy 驗不了 attestation。證據跟未知部分都在 §11。
映像簽章那一層仍然可以(也應該)用新 API,兩條政策各管一層。

6.1 三個對照

政策上線之前先跑這三個,缺一不可:

① 有簽章 + 有 attestation   → 應該放行
② 有簽章、沒有 attestation   → 應該擋下   ← 只有這個能證明新的一層有用
③ 兩者都沒有                → 應該擋下

第 ② 個最重要:沒有它,你分不出「政策生效了」和
「政策其實只是把第一層的結果重複一次」。

方便的是,接上 Chains 之前建的舊映像天然就是第 ② 種——有簽章、沒有 attestation,
不用另外造。

7. 端對端

推一個 commit 走完整流程:

build-push            12:46:23
cosign-sign           12:46:46
wait-for-attestation  12:46:53
update-gitops         12:47:00
wait-for-sync         12:48:45
                      19/19 全綠

部署  <repo>@sha256:7c1ceec6...   pod 1/1 Running

Part 2 — 細節探討

8. ⚠️ signed=true 不代表簽好了

Chains 會在每個處理過的 TaskRun 上寫一個 annotation:

chains.tekton.dev/signed = true

完全沒有設定 signer 的時候,它一樣寫 true 它會照常組出 payload、
照常標記完成、什麼都不上傳,只在 controller 日誌留一行 warn

沒有 signer          →  signed=true    ,零產出
有 signer 但推不上去  →  signed=failed  ,retries=3

「沒設定」比「設定錯誤」危險。 設錯了它誠實報錯;完全沒設它靜默成功。

所以第 4 步的驗收條件只有一個:registry 裡出現 .att
不要看 annotation,不要看日誌裡的 Successfully uploaded——去數檔案

9. 30 秒探針:不要用 6 分鐘的管道除錯

Chains 認的只是名為 IMAGE_URL / IMAGE_DIGEST 的 TaskRun result。
只要有 TaskRun 產出這兩個值,它就會做 attestation ——不需要真的重建映像:

apiVersion: tekton.dev/v1
kind: TaskRun
metadata: { generateName: chains-probe- }
spec:
  serviceAccountName: <管道的 SA>       # 憑證是從這個 SA 拿的
  taskSpec:
    results: [{ name: IMAGE_URL }, { name: IMAGE_DIGEST }]
    steps:
      - name: emit
        image: busybox
        script: |
          #!/bin/sh
          printf '%s' '<registry>/<repo>'        > $(results.IMAGE_URL.path)
          printf '%s' 'sha256:<已存在的 digest>' > $(results.IMAGE_DIGEST.path)

把「憑證、位址、CA」這一段從六分鐘壓成三十秒。除錯迴圈的長度會決定
你願意試幾種設定。

憑證是從 TaskRun 的 ServiceAccount 拿的,不是 Chains controller 自己的。
Chains 會讀 SA 的 secrets imagePullSecrets 兩個欄位。

10. ⚠️ Attestation 證明的是「宣稱」,不是「做過」

上面那個探針只做了一件事:用 busybox printf 了一個 digest。
Chains 為它簽出來的 provenance 是這樣的:

"buildConfig": {"steps": [{
    "entryPoint": "#!/bin/sh\nprintf '%s' '<registry>/<repo>' > /tekton/results/IMAGE_URL\n...",
    "environment": {"container": "emit", "image": "oci://.../library/busybox@sha256:73aaf090..."}}]},
"materials": [{"uri": "oci://.../library/busybox"}],
"metadata": {"completeness": {"environment": false, "materials": false, "parameters": false}}

cosign verify-attestation 驗得過

密碼學上完全有效,語意上完全是假的。 任何能建立 TaskRun 的人,
都能為任意 digest 鑄造一份合法簽章的建構來源證明。

這種證明要有意義,前提是建置器本身可信,
而 Chains 只負責如實記錄「這個 TaskRun 說它產出了什麼」。
它的可信度上限,等於「誰能在那個 namespace 建立 TaskRun」的可信度。

10.1 真實建置的那份也不完整

buildConfig.steps  registry.redhat.io/rhel9/buildah          ← 真的建置器
materials          oci://registry.redhat.io/rhel9/buildah    ← 只有 1 筆
invocation.parameters
  IMAGE       <registry>/<repo>:3484ce5a...
  DOCKERFILE  docker-build-context/Dockerfile
completeness       environment=false  materials=false  parameters=false

materials 裡沒有原始碼 repo。commit hash 只是偶然出現在 IMAGE
參數的 tag 字串裡,不是被當成建構材料記錄的。SLSA 自己的 completeness
三個欄位全部 false

要把 git 來源記進去要看 PipelineRun 層級的
artifacts.pipelinerun.enable-deep-inspection,那是另一個題目。

寫進文件時要說清楚這份 provenance 回答得了什麼、回答不了什麼。
它現在回答不了「這個映像來自哪個 commit」。

11. ⚠️ 這個組合驗不了 attestation,只能用要被移除的那個 API

Kyverno 有兩套政策 API:舊的 ClusterPolicy(將於 v1.20 移除)
和現行的 ImageValidatingPolicy

在映像簽章上,新 API 是好的。在 attestation 上,本文這個組合用不了。

同一個映像、同一把公鑰、同一份 attestation:

cosign CLI    verify-attestation --key --type slsaprovenance --insecure-ignore-tlog
              → 通過

ClusterPolicy attestations + rekor.ignoreTlog:true + ctlog.ignoreSCT:true
              → pass

ImageValidatingPolicy  verifyAttestationSignatures(image, attestations.slsa, [...])
                       ctlog: {insecureIgnoreTlog: true, insecureIgnoreSCT: true}
              → fail:"cosign bundle verification failed"

11.1 兩種情況都回 fail,所以分不出來

              ClusterPolicy   ImageValidatingPolicy
有 attestation      pass  ✅         fail
沒有 attestation    fail  ✅         fail

一個永遠回 fail 的檢查分不出「有」和「沒有」。一旦轉成阻擋模式,
它擋掉的就是全部——包括 §6.1 的 ①,合法的映像也上不去。
只跑 ② 和 ③、看到 fail 就以為政策生效了,正好會漏掉這件事;
要跑 ① 才看得出來它連該放行的都擋。

⚠️ 這個對照要用 useCache: false 重跑。第一次測時舊 API 回的是
verified from cache,那個結果不能算數。

11.2 成因未定,不要瞎猜

實測:.att.sig 兩個物件都沒有任何 bundle 註記——
這跟前提 ⑤(tlog 關閉)一致,沒有 tlog entry 就沒有 Rekor bundle。
而 Kyverno 回的錯誤是 cosign bundle verification failed

⚠️ 不要接受「換新版 Chains 就好了」這個說法。
embed Rekor bundle in OCI attestation layer annotations(chains #1610,進 v0.28.0)
看起來對得上,但它嵌的是 Rekor bundle——沒有 tlog 就沒有東西可嵌。
在 tlog 關閉的組態下,升級不一定有用。

要分辨得跑這兩個,本文都沒跑

① 打開 transparency.enabled,看 IVP 會不會過   ← 要連得到 Rekor,離線做不到
② 換 Chains ≥ 0.28.0 但 tlog 維持關閉          ← Operator 釘住版本,要先換 Operator

先量自己的版本和 tlog 設定,再決定要不要花時間試新 API:

oc get deploy tekton-chains-controller -n openshift-pipelines \
  -o jsonpath='{.metadata.labels.version}'
oc get cm chains-config -n openshift-pipelines \
  -o jsonpath='{.data.transparency\.enabled}'

11.3 這個決定有到期日

Kyverno 官方的 upgrading 文件寫得很直:

ClusterPolicy and Policy (kyverno.io/v1) are officially deprecated
and will be removed in v1.20.

v1.19 is the final release with full support for these types.

也就是說:下一個 minor 版就沒了,而你現在用的就是最後一個支援它的版本。

月份跟版本是兩種來源,強度不一樣,不要混著引:

主張 來源 強度
「在 v1.20 移除、v1.19 是最後一個完整支援的版」 kyverno.io upgrading 官方
「v1.20 排在 2026 年 10 月」 elastic/cloud-on-k8s#9336Nirmata 部落格 社群排程,kyverno.io 沒寫

GitHub 的 Kyverno Release 1.20.0 milestone 也沒有 due date(實查)。

所以:規劃盯版本界線,不盯月份。
月份可以寫,但要標清楚它不是官方承諾。

還有一條路本文沒走過。上游 Chains 有 storage.oci.encoding-format
dsse(預設,DSSE payload 放在 .sig / .att tag)或
sigstore-bundle(Sigstore protobuf-bundle,經 OCI 1.1 Referrers API 儲存)。

而 Kyverno 新 API 的 stack trace 顯示它走的是 cosign v3 的 new-bundle 路徑——
換成 sigstore-bundle 之後有可能就驗得過。

本文驗不了,因為本文環境的 schema 根本沒有這個 key:

oc explain tektonconfig.spec.chain | grep -ci encoding
# 0

所以準確的說法是:ClusterPolicy 移除之後還有兩條路,但本文都沒驗過——
sigstore-bundle 編碼(本文的 schema 沒有這個 key,就算有,registry
還要支援 OCI 1.1 Referrers API),或新版 Chains 的 layer 註記(#1610,
但那要一起打開 tlog 才有東西可嵌,見 §11.2)。

管道側的 wait-for-attestation 不受任一條影響(它用 cosign CLI)。

升級之前要先確認這一層還在。 這種事不會有人通知你,
而且失敗的樣子是「政策消失了」,不是「政策報錯」。

12. 為什麼 wait-for-attestation 要真的驗簽

只檢查 .att 這個 tag 存不存在,會在「檔案在但驗不過」的情況下放行,
而那個失敗一樣會延後到部署階段才爆——你就白做了這條邊。

讓它驗跟准入控制器一樣的東西:同一把公鑰、同一種 predicate type。
這樣一來,「管道綠燈」就真的預告了「准入會過」。

實測它常常「等了 0 秒」——Chains 對 TaskRun 完成的反應大約是 1 秒,
而中間那幾個 Task 剛好蓋掉這段延遲。這個 margin 沒有被任何東西保證。
這條邊的價值不在於它平常要等多久,在於 Chains 慢的那次它會擋在正確的位置。

13. Chains 也會簽映像本身

artifacts.oci.format   simplesigning
artifacts.oci.storage  oci

這兩個預設值不是給 attestation 的,是給映像簽章的。開了 signer 之後,
Chains 會用自己那把金鑰再推一份 .sig,跟你原本的映像簽章在同一個 tag

.sig 的 layer 數:1 → 2

實測既有的簽章驗證政策不受影響(cosign 是「有任一份對得上就行」),
但要自己驗一次——這是升級之後最容易靜默壞掉的地方。

不需要這個行為的話,把 artifacts.oci.storage 設成 "" 就只留 attestation。

13.1 還有一個更容易漏的:artifacts.taskrun.storage

上游 Chains 的預設是 tekton(寫進 TaskRun 的 annotation),不是 oci

上面 Part 1 能跑通,是因為 OpenShift Pipelines Operator 在沒設定時會幫你套 oci

oc get tektonconfig config -o jsonpath='{.spec.chain}' | tr ',' '\n' | grep taskrun
# "artifacts.taskrun.format":"in-toto"
# "artifacts.taskrun.storage":"oci"     ← 我們從沒設過它

⚠️ 拿這篇去套原生 Tekton 的話,registry 裡不會有 .att——
而且你會看到 signed=true(見 §8)。要自己加:

artifacts.taskrun.storage: oci

這就是 §4 那個驗收步驟存在的理由。

14. 這篇沒涵蓋的

  • Git 來源沒有進 provenance:見 §10.1。要靠 PipelineRun 層級的 deep inspection。
  • Transparency log:關掉了,所以信任完全依賴叢集自己保管的金鑰。
    沒有外部、防竄改的公開紀錄可供第三方獨立稽核。
  • 金鑰輪替與多方核准:兩把金鑰各自單一份,沒有輪替排程,不是 N-of-M 多簽。
  • 保護範圍:政策鎖一個 namespace、一條映像路徑。換個 namespace 就繞過去了。
  • Trusted builder:§10 那個問題沒有在這篇被解決,只是被說清楚。

15. 參考文件

Tekton Chains

格式與規範

驗證端

OpenShift

  • Configuring a custom PKI —— Proxy trustedCAinject-trusted-cabundle 的傳播機制(§3 選擇不走這條的原因)

上一篇
Day 29:在 OpenShift 上用 Kyverno 擋掉沒有簽章的映像
系列文
防範軟體供應鏈攻擊:從零打造具備硬性阻擋能力的雲原生 CI/CD 流水線30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言