【CI/CD·进阶篇】安全实践:密钥管理、镜像签名与供应链安全

发布时间:2026/8/11 16:52:25
【CI/CD·进阶篇】安全实践:密钥管理、镜像签名与供应链安全 前言一条完整的 CI/CD 流水线会接触大量敏感信息代码仓库密钥、数据库密码、API Token、生产环境的 kubeconfig。如果任何一个环节泄漏后果不堪设想。本篇系统讲解 CI/CD 中的安全实践从密钥管理到镜像签名到供应链安全。一、供应链安全全景软件供应链安全: 开发阶段 CI/CD 阶段 部署阶段 ┌────────┐ ┌─────────────┐ ┌────────┐ │源码安全│ │密钥安全 │ │镜像验证│ │- 签名 │ │- 凭据管理 │ │- 签名校验│ │- 审计 │ │- 加密存储 │ │- 漏洞扫描│ │- 分支保护│ │- 最小权限 │ │- 运行时验证│ └────┬───┘ └──────┬──────┘ └───┬───┘ ↓ ↓ ↓ ┌────────┐ ┌─────────────┐ ┌────────┐ │依赖安全│ │构建安全 │ │部署安全│ │- SBOM │ │- 可重现构建 │ │- RBAC │ │- 漏洞扫描│ │- 镜像签名 │ │- 审计日志│ │- 许可证 │ │- 镜像扫描 │ │- 网络隔离│ └────────┘ └─────────────┘ └────────┘二、密钥管理密钥管理的三个层次| 层次 | 工具 | 适用场景 ||------|------|---------|| CI/CD 内置 | GitLab Variables / GitHub Secrets | 小团队密钥不多 || 加密存储 | SOPS age/PGP / Sealed Secrets | 中型团队需要加密 Git 中的密钥 || 外部密钥库 | HashiCorp Vault / External Secrets | 大型企业集中管理所有密钥 |方式一CI/CD 内置基础# GitLab CI deploy: variables: # 不敏感的配置 APP_ENV: prod script: # 敏感信息从 CI Variables 读取 - echo $DB_PASSWORD /tmp/db-pass - ./deploy.sh --db-pass-file /tmp/db-pass - rm -f /tmp/db-pass # 用完立即删除# GitHub Actions - name: Deploy env: DB_PASS: ${{ secrets.DB_PASSWORD }} API_TOKEN: ${{ secrets.API_TOKEN }} run: | ./deploy.sh --db-pass $DB_PASS --token $API_TOKEN方式二SOPS age推荐# 安装 SOPS 和 age apt install -y sops # age: https://github.com/FiloSottile/age # 生成 age 密钥对 age-keygen -o age-key.txt # Public key: age1xxx... # Private key: AGE-SECRET-KEY-xxx... # 加密配置文件 sops --encrypt --age age1xxx... \ --in-place config/secrets.yaml # 解密使用 sops --decrypt --age age1xxx... \ config/secrets.yaml | kubectl apply -f -# GitLab CI 中使用 SOPS deploy: variables: SOPS_AGE_KEY: $SOPS_AGE_PRIVATE_KEY # 从 CI Variable 读取 script: - sops --decrypt config/secrets.yaml /tmp/secrets.yaml - kubectl apply -f /tmp/secrets.yaml -n prod - rm -f /tmp/secrets.yaml方式三HashiCorp Vault# GitLab CI Vault 集成 deploy: secrets: DB_PASSWORD: vault: production/db/password # Vault 路径 token: $VAULT_TOKEN script: - echo $DB_PASSWORD # 自动注入# GitHub Actions Vault - name: Get secrets from Vault uses: hashicorp/vault-actionv3 with: url: https://vault.mycompany.com method: approle roleId: ${{ secrets.VAULT_ROLE_ID }} secretId: ${{ secrets.VAULT_SECRET_ID }} secrets: | production/db/password DB_PASSWORD | production/api/token API_TOKEN - name: Deploy env: DB_PASS: ${{ env.DB_PASSWORD }} run: ./deploy.sh**踩坑提示**无论用哪种方式核心原则是——密钥在 CI/CD 环境中只以环境变量形式短暂存在用完即删。不要把密钥写入文件、日志或制品中。三、镜像签名Cosign为什么需要镜像签名没有签名 镜像仓库 → 部署 → 运行 问题怎么知道这个镜像是你的 CI 构建的不是被篡改的 有签名 CI 构建 → 签名 → 镜像仓库 → 部署前验签 → 运行 保障只有通过签名验证的镜像才能部署生成签名密钥# 安装 Cosign curl -O -L https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 mv cosign-linux-amd64 /usr/local/bin/cosign chmod x /usr/local/bin/cosign # 生成密钥对 cosign generate-key-pair # 会生成 # cosign.pub - 公钥可以公开用于验证 # cosign.key - 私钥用于签名必须保密 # 设置环境变量 # COSIGN_PRIVATE_KEY - 私钥内容 # COSIGN_PASSWORD - 私钥密码CI 中签名镜像# GitLab CI sign-image: stage: scan image: alpine:latest needs: [package] variables: COSIGN_PRIVATE_KEY: $COSIGN_PRIVATE_KEY # 从 CI Variable COSIGN_PASSWORD: $COSIGN_PASSWORD script: - apk add --no-cache cosign # 签名镜像 - cosign sign --key env://COSIGN_PRIVATE_KEY \ $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA rules: - if: $CI_COMMIT_BRANCH main# GitHub Actions - name: Sign image uses: sigstore/cosign-installerv3 - name: Sign env: COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} run: | cosign sign --key env://COSIGN_PRIVATE_KEY \ ghcr.io/myorg/myapp:sha-${{ github.sha }}部署前验签verify-image: stage: deploy script: # 验证镜像签名 - cosign verify --key cosign.pub \ $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA # 如果验签失败cosign 返回非零退出码Job 失败 # 只有验签通过才继续部署 - kubectl set image deployment/myapp app$REGISTRY/myapp:$CI_COMMIT_SHORT_SHA -n prod四、SBOM软件物料清单什么是 SBOMSBOMSoftware Bill of Materials是软件组件的清单文件记录了所有依赖及其版本、许可证、来源。类似于食品的成分表。生成 SBOMgenerate-sbom: stage: scan image: name: anchore/syft:latest entrypoint: [] needs: [package] script: # 为 Docker 镜像生成 SBOM - syft $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA -o json sbom.json # 或为源码生成 - syft dir:. -o spdx-json sbom-source.json artifacts: paths: - sbom.json - sbom-source.json expire_in: 90 days使用 SBOM 做漏洞扫描sbom-scan: stage: scan image: name: anchore/grype:latest entrypoint: [] needs: [generate-sbom] script: # 基于 SBOM 扫描已知漏洞 - grype sbom:./sbom.json --fail-on high -o json vuln-report.json artifacts: paths: - vuln-report.json五、可重现构建为什么重要不可重现构建 同一份代码 → 在不同时间构建 → 镜像内容不同 问题无法证明部署的镜像就是CI 构建的镜像 可重现构建 同一份代码 → 任何时候构建 → 镜像内容完全相同 保障可以验证部署的镜像与 CI 构建的镜像一致实现# Dockerfile — 可重现构建 # syntaxdocker/dockerfile:1.6 FROM maven:3.9-eclipse-temurin-17 AS builder # 固定源码版本通过 Git commit hash ARG GIT_COMMIT LABEL org.opencontainers.image.revision$GIT_COMMIT WORKDIR /build # 先复制 lock 文件利用缓存 COPY pom.xml ./ RUN --mounttypecache,target/root/.m2 \ mvn dependency:go-offline # 复制源码并编译 COPY src ./src RUN --mounttypecache,target/root/.m2 \ mvn clean package -DskipTests -Drevision$GIT_COMMIT FROM eclipse-temurin:17-jre-alpine COPY --frombuilder /build/target/*.jar /app/app.jar USER 1000:1000 ENTRYPOINT [java, -jar, /app/app.jar]# CI 中传入 Git commit build: script: - docker build --build-arg GIT_COMMIT$CI_COMMIT_SHA \ -t $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA .验证可重现性verify-reproducible: stage: scan script: # 用相同参数重新构建 - docker build --build-arg GIT_COMMIT$CI_COMMIT_SHA \ -t myapp:verify . # 比较镜像 digest - ORIG_DIGEST$(docker inspect --format{{index .RepoDigests 0}} $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA) - VERIFY_DIGEST$(docker inspect --format{{index .RepoDigests 0}} myapp:verify) - | if [ $ORIG_DIGEST ! $VERIFY_DIGEST ]; then echo WARNING: Build is not reproducible! echo Original: $ORIG_DIGEST echo Verify: $VERIFY_DIGEST else echo Build is reproducible fi六、安全审计与合规审计日志# 在 CI 中记录所有部署操作 deploy: script: - | # 记录部署审计信息 cat deploy-audit.log EOF timestamp: $(date -Iseconds) user: $GITLAB_USER_LOGIN commit: $CI_COMMIT_SHA branch: $CI_COMMIT_BRANCH image: $REGISTRY/myapp:$CI_COMMIT_SHORT_SHA environment: $DEPLOY_ENV status: started EOF # 执行部署 kubectl set image deployment/myapp app$REGISTRY/myapp:$CI_COMMIT_SHORT_SHA -n prod # 记录结果 echo status: $([ $? -eq 0 ] echo success || echo failed) deploy-audit.log artifacts: paths: [deploy-audit.log] expire_in: 365 days # 审计日志保留一年签名策略Kyverno# Kubernetes 中强制要求镜像签名 apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: verify-image-signatures spec: validationFailureAction: enforce # 不验签则拒绝部署 rules: - name: check-signature match: resources: kinds: [Pod] verifyImages: - imageReferences: - registry.mycompany.com/* attestors: entries: - keys: publicKeys: | -----BEGIN PUBLIC KEY----- ...cosign.pub内容... -----END PUBLIC KEY-----七、本篇要点回顾1. 密钥管理三层CI 内置简单→ SOPS加密 Git→ Vault集中管理2. Cosign 签名CI 用私钥签名部署前用公钥验签防止镜像篡改3. SBOM 记录所有依赖成分配合 Grype 做漏洞扫描4. 可重现构建固定基础镜像版本 缓存挂载 确定性构建5. Kyverno 策略强制 K8s 只部署有签名的镜像下一篇预告《度量与优化构建提速、MTTR、变更失败率》——安全做好了接下来看如何度量 CI/CD 的效果并持续优化。