在处理高合规性金融风控规则的动态加载场景时,一个核心矛盾浮现出来:业务要求规则模块(以 WebAssembly 插件形式存在)能够快速迭代并上线,而安全与合规团队则要求每一次变更都具备严格、不可否认的审计追踪,且发布流程必须能抵御最复杂的攻击,例如CI/CD流程中的凭证窃取和内部人员的恶意操作。单纯依赖分支保护和手动的GitHub审批,在审计风暴中显得苍白无力。我们需要的是一个从代码提交到生产环境运行,全程由密码学保证完整性、来源可追溯、且关键节点有人类操作员以物理方式确认的自动化系统。
定义复杂技术问题
我们的目标是为一套运行在Kubernetes上的风控引擎构建一个动态插件分发系统。这些插件以WASM模块的形式打包,因为WASM提供了优秀的沙箱隔离和跨平台能力。
核心挑战:
确保从开发者本地的Git commit,到最终在生产Pod中被加载的WASM二进制文件,是完全一致且经过授权的。
具体威胁模型分析:
- 源码篡改: 恶意代码在合并入主分支前被注入。这通常由Code Review和分支保护策略缓解,但不是我们这次关注的重点。
- 构建过程污染 (CI/CD Compromise): CI/CD环境本身被攻破,攻击者替换了正常的构建脚本或依赖,导致生成的WASM插件包含恶意代码,即使源码是干净的。
- 存储投毒 (Registry Compromise): 存储WASM插件的OCI(Open Container Initiative)制品库被攻破,签名的、合法的插件被替换为恶意版本。
- 未授权部署: 合法的操作员账号被盗用(例如通过钓鱼获取了密码和TOTP),攻击者使用被盗凭证批准了恶意插件的部署。这是最难防范的一点。
架构设计必须满足的要求:
- 构建可追溯性: 任何一个WASM二进制文件,都必须能通过密码学手段反向追溯到其构建的精确Git commit和CI/CD运行实例。
- 制品完整性: 确保从构建完成到部署拉取,WASM二进制文件未被任何形式地篡改。
- 抗钓鱼的审批机制: 生产环境的部署审批必须由指定操作员执行,且该审批行为本身需要能抵抗网络钓鱼、凭证泄露等攻击。常规的Web界面点击“Approve”是不够的。
- 自动化策略执行: 部署系统(GitOps控制器)必须能够自动化地、强制地验证上述所有安全属性,任何不满足条件的部署都应被自动拒绝。
方案A分析:基于传统分支保护与手动审批的GitOps流程
这是一种常见的“足够好”的安全实践。
流程描述:
- 开发者在特性分支上开发WASM插件。
- 提交Pull Request到
main
分支。 -
main
分支配置了分支保护,要求至少一名审核人批准。 - 审核人通过GitHub UI登录,输入密码和TOTP,点击“Approve”按钮。
- 合并后,GitHub Actions触发,构建WASM插件,将其打包到一个OCI制品中,推送到容器镜像仓库(如GHCR)。
- 另一个独立的配置仓库中,运维人员手动更新部署清单,将镜像tag指向新版本。
- Flux CD检测到配置仓库的变更,拉取新的OCI制品并部署到Kubernetes集群。
优势:
- 实现简单,完全利用了GitHub和Flux CD的现有功能。
- 对于大多数项目而言,其安全性和审计能力已经足够。
- 心智负担低,团队成员容易理解。
劣势与安全缺口:
- 审批凭证的脆弱性: 审核人的GitHub账号是整个安全链条的核心。如果账号的密码和TOTP通过钓鱼攻击被窃取,攻击者就可以冒充审核人批准恶意的Pull Request,整个安全防线瞬间瓦解。审计日志只会记录是“某某用户”批准的,但无法从密码学上证明操作的真实意图。
- 构建过程不透明: 虽然构建发生在受信任的GitHub Actions环境中,但最终产物(WASM OCI制品)与其构建过程之间没有强密码学绑定。审计时,你只能“相信”GitHub Actions的日志。
- 制品库是隐式信任的: 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
优势:
- 强化的构建证明: 使用
sigstore
的cosign
工具进行无密钥签名(Keyless Signing)。它利用GitHub Actions的OIDC身份提供者,为构建产物生成一个短暂的证书,并将其签名信息记录在名为Rekor的防篡改公共账本中。审计时,任何人都可以验证该OCI制品确实是由github.com/a-bank/wasm-plugins
仓库的某个特定工作流构建的。 - 抗钓鱼的物理审批: 核心创新点。通过WebAuthn,审批操作不再是输入密码,而是操作员必须物理触摸其注册的硬件密钥(如YubiKey)。浏览器与硬件密钥直接通信,生成一个无法被中间人攻击或钓鱼网站窃取的签名。这个签名是对特定部署请求(例如,部署
sha256:abc...
)的数字承诺。 - 自动化运行时验证: Flux CD被配置为在部署前,必须检查OCI制品的
cosign
签名。它不仅会检查构建签名,还会检查我们自定义的“操作员审批”证明(attestation)。只有两个签名都存在且有效,部署才会继续。这把安全策略从文档变成了强制执行的代码。 - 端到端的审计链: 整个流程产生了一系列不可否认的密码学证据: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-sign
和 wait-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个进行硬件签名),以满足更严格的合规要求。
当前方案的局限性:
- WebAuthn审批服务的复杂性: 最大的障碍是需要自行开发和维护那个审批中间件。这增加了系统的复杂性和潜在的攻击面,该服务本身必须被高度保护。
- 对Flux CD功能的依赖: 虽然Flux CD支持
cosign
签名验证,但对复杂证明(Attestation)的策略驱动验证(例如,“必须包含一个type: a-bank-approval
的证明,且approver
字段必须在指定列表中”)仍在发展中。当前我们通过工作流的逻辑顺序来保证这一点,但这不如部署时的策略强制来得稳固。 - 密钥分发与轮换: 在非无密钥场景下,或者对于审批服务的签名密钥,管理其生命周期会带来额外的操作负担。无密钥签名缓解了构建端的这个问题,但审批服务的可信根问题依然存在。
这个架构的核心价值在于,它将安全审计从“事后查看日志”转变为“事前密码学验证”,将人类审批的薄弱环节用抗钓鱼的硬件技术进行了加固,真正实现了在拥抱GitOps敏捷性的同时,满足了金融级别场景的严苛安全要求。