构建基于 GitOps 与 WebAuthn 硬件密钥的 WebAssembly 插件可信供应链


在处理高合规性金融风控规则的动态加载场景时,一个核心矛盾浮现出来:业务要求规则模块(以 WebAssembly 插件形式存在)能够快速迭代并上线,而安全与合规团队则要求每一次变更都具备严格、不可否认的审计追踪,且发布流程必须能抵御最复杂的攻击,例如CI/CD流程中的凭证窃取和内部人员的恶意操作。单纯依赖分支保护和手动的GitHub审批,在审计风暴中显得苍白无力。我们需要的是一个从代码提交到生产环境运行,全程由密码学保证完整性、来源可追溯、且关键节点有人类操作员以物理方式确认的自动化系统。

定义复杂技术问题

我们的目标是为一套运行在Kubernetes上的风控引擎构建一个动态插件分发系统。这些插件以WASM模块的形式打包,因为WASM提供了优秀的沙箱隔离和跨平台能力。

核心挑战:

确保从开发者本地的Git commit,到最终在生产Pod中被加载的WASM二进制文件,是完全一致且经过授权的。

具体威胁模型分析:

  1. 源码篡改: 恶意代码在合并入主分支前被注入。这通常由Code Review和分支保护策略缓解,但不是我们这次关注的重点。
  2. 构建过程污染 (CI/CD Compromise): CI/CD环境本身被攻破,攻击者替换了正常的构建脚本或依赖,导致生成的WASM插件包含恶意代码,即使源码是干净的。
  3. 存储投毒 (Registry Compromise): 存储WASM插件的OCI(Open Container Initiative)制品库被攻破,签名的、合法的插件被替换为恶意版本。
  4. 未授权部署: 合法的操作员账号被盗用(例如通过钓鱼获取了密码和TOTP),攻击者使用被盗凭证批准了恶意插件的部署。这是最难防范的一点。

架构设计必须满足的要求:

  • 构建可追溯性: 任何一个WASM二进制文件,都必须能通过密码学手段反向追溯到其构建的精确Git commit和CI/CD运行实例。
  • 制品完整性: 确保从构建完成到部署拉取,WASM二进制文件未被任何形式地篡改。
  • 抗钓鱼的审批机制: 生产环境的部署审批必须由指定操作员执行,且该审批行为本身需要能抵抗网络钓鱼、凭证泄露等攻击。常规的Web界面点击“Approve”是不够的。
  • 自动化策略执行: 部署系统(GitOps控制器)必须能够自动化地、强制地验证上述所有安全属性,任何不满足条件的部署都应被自动拒绝。

方案A分析:基于传统分支保护与手动审批的GitOps流程

这是一种常见的“足够好”的安全实践。

  • 流程描述:

    1. 开发者在特性分支上开发WASM插件。
    2. 提交Pull Request到main分支。
    3. main分支配置了分支保护,要求至少一名审核人批准。
    4. 审核人通过GitHub UI登录,输入密码和TOTP,点击“Approve”按钮。
    5. 合并后,GitHub Actions触发,构建WASM插件,将其打包到一个OCI制品中,推送到容器镜像仓库(如GHCR)。
    6. 另一个独立的配置仓库中,运维人员手动更新部署清单,将镜像tag指向新版本。
    7. Flux CD检测到配置仓库的变更,拉取新的OCI制品并部署到Kubernetes集群。
  • 优势:

    • 实现简单,完全利用了GitHub和Flux CD的现有功能。
    • 对于大多数项目而言,其安全性和审计能力已经足够。
    • 心智负担低,团队成员容易理解。
  • 劣势与安全缺口:

    1. 审批凭证的脆弱性: 审核人的GitHub账号是整个安全链条的核心。如果账号的密码和TOTP通过钓鱼攻击被窃取,攻击者就可以冒充审核人批准恶意的Pull Request,整个安全防线瞬间瓦解。审计日志只会记录是“某某用户”批准的,但无法从密码学上证明操作的真实意图。
    2. 构建过程不透明: 虽然构建发生在受信任的GitHub Actions环境中,但最终产物(WASM OCI制品)与其构建过程之间没有强密码学绑定。审计时,你只能“相信”GitHub Actions的日志。
    3. 制品库是隐式信任的: Flux CD信任制品库中的内容。如果制品库本身被攻破,攻击者可以替换一个同名同tag的制品,Flux CD会毫无察觉地部署恶意版本。制品本身缺乏自证清白的能力。

在我们的金融场景下,这种依赖“隐式信任”和“可钓鱼凭证”的体系,无法通过严格的合规审计。

方案B分析:基于sigstore、WebAuthn和Flux CD的端到端可信供应链

此方案旨在通过引入显式的密码学证明链条来解决方案A的所有缺陷。

  • 核心理念: 没有任何一个环节是基于隐式信任的。每一步交接,都必须有可验证的数字签名。人类的干预,特别是高权限的审批,必须通过抗钓鱼的硬件设备来完成。

  • 架构流程图:

        graph TD
          subgraph "GitHub: a-bank/wasm-plugins Repo"
              A[Developer: git push] --> B{Pull Request};
              B -- Code Review --> C{Merge to main};
          end
    
          subgraph "GitHub Actions Workflow"
              C --> D[Job 1: Build & Sign];
              D -- Build WASM & SBOM --> E[Package as OCI Artifact];
              E -- Keyless Signing with GitHub OIDC --> F[cosign sign: Attest Build Provenance];
              F --> G{Job 2: Human Approval Gate};
              G -- Generates Signed URL --> H["Operator receives notification (Slack/Email)"];
          end
    
          subgraph "Operator's Machine"
               H --> I{Opens URL in Browser};
               I -- WebAuthn Ceremony --> J[Touches YubiKey/Security Key];
               J -- Signed Challenge --> K[Approval Server];
          end
    
          subgraph "GitHub Actions Workflow (Resumed)"
              K -- Validates Signature & Notifies --> G;
              G -- Approval Received --> L[cosign attest: Attach Operator's Approval];
              L --> M[Push Signed OCI Artifact to GHCR];
          end
    
          subgraph "Kubernetes Cluster"
              N[Flux CD] -- Watches --> M;
              N --> O{Verification Step};
              O -- Verifies cosign build signature --> P[OK];
              O -- Verifies cosign approval attestation --> Q[OK];
              P & Q --> R[Deploy WASM Plugin Host Pod];
          end
    
          style H fill:#f9f,stroke:#333,stroke-width:2px
          style J fill:#f9f,stroke:#333,stroke-width:2px
  • 优势:

    1. 强化的构建证明: 使用sigstorecosign工具进行无密钥签名(Keyless Signing)。它利用GitHub Actions的OIDC身份提供者,为构建产物生成一个短暂的证书,并将其签名信息记录在名为Rekor的防篡改公共账本中。审计时,任何人都可以验证该OCI制品确实是由github.com/a-bank/wasm-plugins仓库的某个特定工作流构建的。
    2. 抗钓鱼的物理审批: 核心创新点。通过WebAuthn,审批操作不再是输入密码,而是操作员必须物理触摸其注册的硬件密钥(如YubiKey)。浏览器与硬件密钥直接通信,生成一个无法被中间人攻击或钓鱼网站窃取的签名。这个签名是对特定部署请求(例如,部署sha256:abc...)的数字承诺。
    3. 自动化运行时验证: Flux CD被配置为在部署前,必须检查OCI制品的cosign签名。它不仅会检查构建签名,还会检查我们自定义的“操作员审批”证明(attestation)。只有两个签名都存在且有效,部署才会继续。这把安全策略从文档变成了强制执行的代码。
    4. 端到端的审计链: 整个流程产生了一系列不可否认的密码学证据:Git commit哈希 -> CI构建日志 -> OCI制品摘要 -> 构建签名(由CI身份证明) -> 审批证明(由操作员硬件密钥证明)-> 生产环境的Pod。
  • 劣势:

    • 实现复杂度高: 需要引入cosign, sigstore生态系统,并需要开发或集成一个用于处理WebAuthn审批流程的微服务或自定义GitHub Action。
    • 新的运维负担: 需要管理操作员的WebAuthn设备注册和生命周期。
    • 对开发者/操作员的培训成本: 团队需要理解数字签名、证明(Attestation)等概念。

最终选择与核心实现概览

对于我们所处的专业领域,合规性与安全性是不可妥协的。方案B虽然复杂,但它提供的密码学保障是方案A完全无法比拟的。因此,我们选择方案B。

以下是关键部分的核心实现代码和配置。

1. GitHub Actions工作流 (.github/workflows/build-and-deploy.yml)

这个工作流被拆分为两个主要作业:build-signwait-for-approval

name: Build, Sign, and Deploy WASM Plugin

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  packages: write
  id-token: write # Required for sigstore keyless signing

jobs:
  build-sign:
    runs-on: ubuntu-latest
    outputs:
      image_digest: ${{ steps.push_oci.outputs.digest }}
      image_name: ghcr.io/${{ github.repository_owner }}/financial-rule-plugin
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Cosign
        uses: sigstore/cosign-[email protected]

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Build WASM plugin
        # 在真实项目中,这里会是复杂的编译步骤
        run: |
          echo "financial rule logic" > rule.wasm
          mkdir -p build/
          mv rule.wasm build/

      - name: Generate SBOM
        uses: anchore/syft-action@v0
        with:
          path: ./build
          format: spdx-json
          output-file: ./build/sbom.spdx.json

      - name: Push WASM and SBOM as OCI Artifact
        id: push_oci
        run: |
          # 使用 ORAS CLI 将包含 WASM 和 SBOM 的目录推送到 OCI 仓库
          # ORAS (OCI Registry As Storage)
          oras push ghcr.io/${{ github.repository_owner }}/financial-rule-plugin:${{ github.sha }} \
            --config /dev/null:application/vnd.oci.image.config.v1+json \
            ./build/rule.wasm:application/vnd.module.wasm.v1+gzip \
            ./build/sbom.spdx.json:application/spdx+json
          
          # 获取推送后制品的精确摘要 (digest)
          DIGEST=$(oras manifest fetch ghcr.io/${{ github.repository_owner }}/financial-rule-plugin:${{ github.sha }} --pretty | jq -r .digest)
          echo "digest=$DIGEST" >> $GITHUB_OUTPUT

      - name: Sign OCI Artifact with Cosign (Keyless)
        run: |
          # 使用GitHub Actions的OIDC Token进行无密钥签名
          # 这将创建一个可验证的链接,证明此制品由这个特定的GitHub Actions运行所构建
          cosign sign --yes ghcr.io/${{ github.repository_owner }}/financial-rule-plugin@${{ steps.push_oci.outputs.digest }}
          echo "Signed artifact with build provenance."

  wait-for-approval:
    needs: build-sign
    runs-on: ubuntu-latest
    environment: production # 使用GitHub环境进行保护和审批人设置
    
    steps:
      - name: Install Cosign
        uses: sigstore/cosign-[email protected]

      - name: Wait for Human Operator Approval via WebAuthn
        id: webauthn_gate
        run: |
          # 这是一个关键步骤的伪代码实现
          # 在真实场景中,这将是一个调用我们内部审批服务的脚本
          # 1. 向审批服务发起一个针对 `needs.build-sign.outputs.image_digest` 的审批请求
          # 2. 审批服务生成一个一次性的、带签名的URL,并通过Slack/Email发送给审批人
          # 3. 脚本进入轮询状态,等待审批服务返回确认
          echo "Approval request sent for digest: ${{ needs.build-sign.outputs.image_digest }}"
          echo "Please visit the approval URL sent to your registered channel and use your hardware key."
          
          # 模拟等待过程
          sleep 60 

          # 假设审批成功,审批服务会返回一个签名的attestation
          # 这个attestation是一个JSON文件,内容包含审批人、时间戳和被审批的制品摘要
          APPROVAL_PAYLOAD=$(printf '{"approver":"[email protected]","timestamp":"%s","digest":"%s"}' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${{ needs.build-sign.outputs.image_digest }}")
          echo $APPROVAL_PAYLOAD > approval-attestation.json
          echo "attestation_path=./approval-attestation.json" >> $GITHUB_OUTPUT
          echo "Operator approval received."

      - name: Attach Approval Attestation
        if: success() && steps.webauthn_gate.outputs.attestation_path != ''
        run: |
          # 将审批证明(attestation)附加到OCI制品上
          # --type a-bank-approval 定义了一个自定义的证明类型
          cosign attest --yes --type a-bank-approval --predicate ${{ steps.webauthn_gate.outputs.attestation_path }} ghcr.io/${{ needs.build-sign.outputs.image_name }}@${{ needs.build-sign.outputs.image_digest }}
          echo "Attached operator approval attestation."
      
      - name: Update GitOps Repository
        if: success()
        run: |
          # 此步骤将检出配置仓库,并更新ImagePolicy或Kustomization文件
          # 指向新的、已完全签名和证明的制品摘要
          echo "Updating GitOps repository to deploy digest: ${{ needs.build-sign.outputs.image_digest }}"
          # ... git clone, kustomize edit set image, git push ...

2. WebAuthn审批服务(概念)

这个服务是连接GitHub Actions和操作员硬件密钥的桥梁。它本身需要是一个安全的服务,通常使用Go或Rust编写。

package main

// main.go - 这是一个极度简化的概念,不包含完整的错误处理和安全实现

import (
	"fmt"
	"net/http"
	"github.com/go-webauthn/webauthn/webauthn"
	// ... 其他依赖
)

var webAuthn *webauthn.WebAuthn
var approvalRequests map[string]string // a map from sessionID to image digest

func main() {
    // 初始化WebAuthn配置
	wconfig := &webauthn.Config{
		RPDisplayName: "A-Bank Secure Deployment Gate",
		RPID:          "approvals.a-bank.com",
		RPOrigins:     []string{"https://approvals.a-bank.com"},
	}
	webAuthn, _ = webauthn.New(wconfig)

    // API: 由GitHub Actions调用,用于创建审批请求
	http.HandleFunc("/api/v1/request-approval", handleRequestApproval)
    // Web: 操作员访问的页面,用于执行签名
	http.HandleFunc("/approve/{sessionID}", handleApprovalPage)
    // API: WebAuthn流程的回调
	http.HandleFunc("/api/v1/verify-approval", handleVerifyApproval)

	http.ListenAndServe(":8080", nil)
}

func handleRequestApproval(w http.ResponseWriter, r *http.Request) {
    // 1. 验证来自GitHub Actions的JWT Token,确保请求合法
    // 2. 解析请求体,获取image digest
    // 3. 生成一个唯一的sessionID
    // 4. 存储 sessionID -> imageDigest 的映射
    // 5. 生成带sessionID的URL: https://approvals.a-bank.com/approve/{sessionID}
    // 6. 将URL通过安全渠道发送给操作员
    // 7. 返回202 Accepted
}

func handleApprovalPage(w http.ResponseWriter, r *http.Request) {
    // 1. 从URL中获取sessionID
    // 2. 从存储中查找对应的image digest
    // 3. 渲染一个页面,显示 "You are about to approve the deployment of plugin with digest: [digest]. Please touch your security key."
    // 4. 页面上的JavaScript会调用navigator.credentials.get()发起WebAuthn签名请求
}

func handleVerifyApproval(w http.ResponseWriter, r *http.Request) {
    // 1. 解析来自前端的WebAuthn签名响应
    // 2. 根据sessionID找到对应的用户和请求信息
    // 3. 调用 webAuthn.ValidateAssertionResponse() 验证签名
    // 4. 如果验证成功:
    //    a. 生成包含审批信息的JSON Attestation
    //    b. 将该Attestation返回给轮询的GitHub Actions
    //    c. 标记该审批请求已完成
}

3. Flux CD 配置

要在Flux CD中强制执行签名验证,我们需要在Kustomization资源中定义验证策略。

# ./clusters/production/flux-system/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: wasm-plugins
  namespace: flux-system
spec:
  interval: 10m0s
  path: ./apps/wasm-plugins
  prune: true
  sourceRef:
    kind: GitRepository
    name: gitops-repo
  # 这是最关键的部分
  verify:
    mode: cosign
    secretRef:
      name: cosign-pub # 一个包含cosign公钥的secret,但在无密钥模式下,我们验证身份
    # 对于无密钥签名,我们验证的是证书的签发者(Issuer)和主题(Subject)
    # 这确保了只有来自我们特定GitHub仓库的构建才能被部署
    identities:
    - issuer: https://token.actions.githubusercontent.com
      subjectRegexp: "^https://github.com/a-bank/wasm-plugins/.github/workflows/build-and-deploy.yml@refs/heads/main$"

上述配置告诉Flux CD:在应用./apps/wasm-plugins目录下的任何清单之前,必须检查其中引用的所有OCI制品的签名。该签名必须能通过cosign验证,并且签发证书的身份必须匹配我们GitHub Actions工作流的身份。Flux CD原生并不直接支持验证attestation,但社区正在朝这个方向发展。目前的一个变通方法是,只有在attestation步骤成功后,才更新GitOps仓库中的镜像tag,Flux CD对构建签名的验证间接保证了审批流程已完成。

架构的扩展性与局限性

这种架构模式不仅仅适用于WASM插件。任何以OCI制品形式存储的资产,包括容器镜像、Helm Charts,都可以应用这套端到端的可信供应链模型。WebAuthn审批服务也可以扩展为支持M-of-N审批(需要N个审批人中的M个进行硬件签名),以满足更严格的合规要求。

当前方案的局限性:

  1. WebAuthn审批服务的复杂性: 最大的障碍是需要自行开发和维护那个审批中间件。这增加了系统的复杂性和潜在的攻击面,该服务本身必须被高度保护。
  2. 对Flux CD功能的依赖: 虽然Flux CD支持cosign签名验证,但对复杂证明(Attestation)的策略驱动验证(例如,“必须包含一个type: a-bank-approval的证明,且approver字段必须在指定列表中”)仍在发展中。当前我们通过工作流的逻辑顺序来保证这一点,但这不如部署时的策略强制来得稳固。
  3. 密钥分发与轮换: 在非无密钥场景下,或者对于审批服务的签名密钥,管理其生命周期会带来额外的操作负担。无密钥签名缓解了构建端的这个问题,但审批服务的可信根问题依然存在。

这个架构的核心价值在于,它将安全审计从“事后查看日志”转变为“事前密码学验证”,将人类审批的薄弱环节用抗钓鱼的硬件技术进行了加固,真正实现了在拥抱GitOps敏捷性的同时,满足了金融级别场景的严苛安全要求。


  目录