Rekor 透明日志完整教程 / 07 - CI/CD 集成
第 7 章:CI/CD 集成
本章介绍如何将 Rekor 和 cosign 集成到主流 CI/CD 平台中,实现自动化签名、验证和发布流程。
7.1 集成概览
7.1.1 为什么 CI/CD 中需要 Rekor?
| 问题 | CI/CD 集成的解决方案 |
|---|---|
| 手动签名容易遗漏 | 自动化签名,每次构建都签名 |
| 签名记录分散 | 所有签名统一记录在 Rekor 中 |
| 验证流程复杂 | 部署前自动验证签名和 Rekor 记录 |
| 审计困难 | Rekor 提供完整的构建签名历史 |
7.1.2 典型集成流程
代码提交 ──► CI 触发 ──► 构建 ──► 测试 ──► 签名 ──► 上传到 Rekor ──► 发布
│ │ │ │ │ │ │
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
Git GitHub Docker pytest cosign rekor Registry
Actions Build sign entry
7.2 GitHub Actions 集成
7.2.1 基本配置
GitHub Actions 是最常用的 CI/CD 平台,原生支持 OIDC,与 Sigstore 无缝集成。
# .github/workflows/build-and-sign.yml
name: Build and Sign
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # 关键:允许 OIDC 令牌请求
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
sign:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign container image
env:
DIGEST: ${{ needs.build.outputs.digest }}
run: |
cosign sign --yes \
--oidc-issuer=https://token.actions.githubusercontent.com \
${REGISTRY}/${IMAGE_NAME}@${DIGEST}
7.2.2 完整工作流(构建 + 签名 + 验证 + SBOM)
# .github/workflows/secure-build.yml
name: Secure Build Pipeline
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write
attestations: write
security-events: write
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
sign:
needs: build
runs-on: ubuntu-latest
steps:
- uses: sigstore/cosign-installer@v3
- name: Sign image (keyless)
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
- name: Verify signature
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
sbom:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Attest SBOM
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ needs.build.outputs.digest }}
push-to-registry: true
verify:
needs: [sign, sbom]
runs-on: ubuntu-latest
steps:
- uses: sigstore/cosign-installer@v3
- name: Verify signature
run: |
cosign verify \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
- name: Verify attestation
run: |
cosign verify-attestation \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--type spdxjson \
ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}
7.2.3 GitHub Actions 权限说明
| 权限 | 用途 |
|---|---|
id-token: write | 请求 OIDC 令牌,用于无密钥签名 |
packages: write | 推送容器镜像到 GHCR |
contents: read | 读取仓库代码 |
attestations: write | 创建构建证明(attestation) |
7.3 GitLab CI 集成
7.3.1 基本配置
# .gitlab-ci.yml
stages:
- build
- sign
- verify
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
build:
stage: build
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_NAME:$IMAGE_TAG .
- docker push $IMAGE_NAME:$IMAGE_TAG
- echo "IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE_NAME:$IMAGE_TAG | cut -d@ -f2)" >> build.env
artifacts:
reports:
dotenv: build.env
sign:
stage: sign
image: alpine:3.18
needs:
- build
before_script:
- apk add --no-cache cosign
script:
- cosign sign --yes
--oidc-issuer=$CI_SERVER_URL
$IMAGE_NAME:$IMAGE_TAG@$IMAGE_DIGEST
id_tokens:
OIDC_TOKEN:
aud: sigstore
verify:
stage: verify
image: alpine:3.18
needs:
- sign
before_script:
- apk add --no-cache cosign
script:
- cosign verify
--certificate-identity=$CI_PIPELINE_URL
--certificate-oidc-issuer=$CI_SERVER_URL
$IMAGE_NAME:$IMAGE_TAG@$IMAGE_DIGEST
7.3.2 GitLab CI 变量
| 变量 | 说明 |
|---|---|
CI_SERVER_URL | GitLab 实例 URL |
CI_REGISTRY | 容器注册表地址 |
CI_PIPELINE_URL | 流水线 URL(用作证书身份) |
CI_COMMIT_SHA | 提交 SHA |
7.4 Jenkins 集成
7.4.1 Jenkinsfile 示例
// Jenkinsfile
pipeline {
agent any
environment {
REGISTRY = 'ghcr.io'
IMAGE_NAME = 'myorg/myapp'
IMAGE_TAG = "${env.BUILD_NUMBER}-${env.GIT_COMMIT[0..7]}"
}
stages {
stage('Build') {
steps {
script {
docker.withRegistry("https://${REGISTRY}", 'ghcr-credentials') {
def image = docker.build("${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}")
image.push()
env.IMAGE_DIGEST = sh(
script: "docker inspect --format='{{index .RepoDigests 0}}' ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} | cut -d@ -f2",
returnStdout: true
).trim()
}
}
}
}
stage('Sign') {
steps {
withCredentials([file(credentialsId: 'cosign-private-key', variable: 'COSIGN_KEY')]) {
sh """
cosign sign --key ${COSIGN_KEY} --yes \
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}
"""
}
}
}
stage('Verify') {
steps {
withCredentials([file(credentialsId: 'cosign-public-key', variable: 'COSIGN_PUB')]) {
sh """
cosign verify --key ${COSIGN_PUB} \
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}
"""
}
}
}
}
}
7.5 多平台 CI 集成对比
| 平台 | OIDC 支持 | 配置复杂度 | Rekor 集成 | 推荐指数 |
|---|---|---|---|---|
| GitHub Actions | ✅ 原生 | ⭐⭐ | 自动 | ⭐⭐⭐⭐⭐ |
| GitLab CI | ✅ 原生 | ⭐⭐⭐ | 自动 | ⭐⭐⭐⭐ |
| Jenkins | ⚠️ 需插件 | ⭐⭐⭐⭐ | 手动 | ⭐⭐⭐ |
| Azure DevOps | ✅ 原生 | ⭐⭐⭐ | 自动 | ⭐⭐⭐⭐ |
| CircleCI | ✅ 原生 | ⭐⭐⭐ | 自动 | ⭐⭐⭐⭐ |
7.6 自动化签名脚本
7.6.1 通用签名脚本
#!/bin/bash
# scripts/sign-artifact.sh
# 通用构件签名脚本
set -euo pipefail
# 配置
REKOR_SERVER="${REKOR_SERVER:-https://rekor.sigstore.dev}"
SIGN_MODE="${SIGN_MODE:-keyless}" # keyless 或 key
# 参数
ARTIFACT="$1"
SIGNATURE_OUTPUT="${2:-${ARTIFACT}.sig}"
CERTIFICATE_OUTPUT="${3:-${ARTIFACT}.cert}"
echo "=== 构件签名 ==="
echo "构件: $ARTIFACT"
echo "模式: $SIGN_MODE"
case "$SIGN_MODE" in
keyless)
echo "使用无密钥签名..."
cosign sign-blob --yes \
--output-signature "$SIGNATURE_OUTPUT" \
--output-certificate "$CERTIFICATE_OUTPUT" \
"$ARTIFACT"
;;
key)
echo "使用密钥签名..."
cosign sign-blob \
--key "${COSIGN_KEY:-cosign.key}" \
--yes \
--output-signature "$SIGNATURE_OUTPUT" \
--output-certificate "$CERTIFICATE_OUTPUT" \
"$ARTIFACT"
;;
*)
echo "未知签名模式: $SIGN_MODE"
exit 1
;;
esac
echo "签名文件: $SIGNATURE_OUTPUT"
echo "证书文件: $CERTIFICATE_OUTPUT"
# 输出 Rekor 条目信息
echo "=== Rekor 条目已创建 ==="
7.6.2 通用验证脚本
#!/bin/bash
# scripts/verify-artifact.sh
# 通用构件验证脚本
set -euo pipefail
# 参数
ARTIFACT="$1"
SIGNATURE_FILE="${2:-${ARTIFACT}.sig}"
CERTIFICATE_FILE="${3:-${ARTIFACT}.cert}"
# 验证配置
CERTIFICATE_IDENTITY="${CERTIFICATE_IDENTITY:-}"
CERTIFICATE_OIDC_ISSUER="${CERTIFICATE_OIDC_ISSUER:-}"
PUBLIC_KEY="${PUBLIC_KEY:-}"
echo "=== 构件验证 ==="
echo "构件: $ARTIFACT"
if [ -n "$PUBLIC_KEY" ]; then
echo "模式: 密钥验证"
cosign verify-blob \
--key "$PUBLIC_KEY" \
--signature "$SIGNATURE_FILE" \
--certificate "$CERTIFICATE_FILE" \
"$ARTIFACT"
elif [ -n "$CERTIFICATE_IDENTITY" ]; then
echo "模式: 无密钥验证"
cosign verify-blob \
--certificate-identity "$CERTIFICATE_IDENTITY" \
--certificate-oidc-issuer "$CERTIFICATE_OIDC_ISSUER" \
--signature "$SIGNATURE_FILE" \
--certificate "$CERTIFICATE_FILE" \
"$ARTIFACT"
else
echo "错误: 必须提供 PUBLIC_KEY 或 CERTIFICATE_IDENTITY"
exit 1
fi
echo "✅ 验证通过"
7.7 自动化发布流水线
7.7.1 完整发布流程
┌──────────────────────────────────────────────────────────────────────┐
│ 自动化发布流水线 │
│ │
│ 开发者 │
│ │ │
│ │ 1. git tag v1.0.0 │
│ │ 2. git push origin v1.0.0 │
│ │ │
│ ▼ │
│ CI/CD 触发 │
│ │ │
│ ├─► 3. 构建容器镜像 │
│ │ │
│ ├─► 4. 运行测试 │
│ │ │
│ ├─► 5. cosign sign --yes <image> │
│ │ └─ 自动上传签名到 Rekor │
│ │ │
│ ├─► 6. 生成 SBOM │
│ │ └─ cosign attest --type spdxjson <image> │
│ │ │
│ ├─► 7. 漏洞扫描 │
│ │ └─ grype <image> --fail-on critical │
│ │ │
│ ├─► 8. 验证签名 │
│ │ └─ cosign verify <image> │
│ │ │
│ └─► 9. 推送到生产注册表 │
│ │
│ 部署 │
│ │ │
│ ├─► 10. 验证镜像签名 │
│ │ └─ cosign verify --certificate-identity=... <image> │
│ │ │
│ └─► 11. 部署到集群 │
└──────────────────────────────────────────────────────────────────────┘
7.7.2 GitHub Actions 发布工作流
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
packages: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
- name: Verify signature
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
deploy:
needs: release
runs-on: ubuntu-latest
environment: production
steps:
- name: Verify image before deploy
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/${{ github.repository }}/" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/${{ github.repository }}@${{ needs.release.outputs.digest }}
- name: Deploy
run: |
echo "Deploying version ${{ needs.release.outputs.version }}"
# kubectl apply, helm upgrade, etc.
7.8 签名验证集成到部署
7.8.1 Kubernetes 准入控制器
使用 Sigstore Policy Controller 在 Kubernetes 中强制验证签名:
# install-policy-controller.yaml
apiVersion: v1
kind: Namespace
metadata:
name: sigstore-policy-controller
---
# 安装 Policy Controller
# helm repo add sigstore https://sigstore.github.io/helm-charts
# helm install policy-controller sigstore/policy-controller -n sigstore-policy-controller
# policy.yaml - 要求所有镜像必须签名
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signatures
spec:
images:
- glob: "ghcr.io/myorg/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: ".*myorg/myrepo.*"
ctlog:
url: https://rekor.sigstore.dev
7.8.2 Argo CD 集成
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
spec:
project: default
source:
repoURL: https://github.com/myorg/myrepo.git
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
7.9 多仓库/多服务签名管理
7.9.1 统一签名策略
# .github/workflows/reusable-sign.yml
name: Reusable Sign Workflow
on:
workflow_call:
inputs:
image:
required: true
type: string
digest:
required: true
type: string
jobs:
sign:
runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
steps:
- uses: sigstore/cosign-installer@v3
- name: Sign image
run: |
cosign sign --yes \
${{ inputs.image }}@${{ inputs.digest }}
# 在其他 workflow 中调用
jobs:
build:
# ... 构建逻辑
outputs:
digest: ${{ steps.build.outputs.digest }}
sign:
needs: build
uses: ./.github/workflows/reusable-sign.yml
with:
image: ghcr.io/myorg/myapp
digest: ${{ needs.build.outputs.digest }}
7.10 注意事项
OIDC 令牌过期:GitHub Actions 的 OIDC 令牌有效期较短,签名步骤应在令牌获取后尽快执行。
并发构建:同一镜像的多次并发签名可能导致 Rekor 写入冲突,建议使用 digest 而非 tag 作为签名目标。
速率限制:公共 Rekor 有速率限制,大规模构建应考虑私有实例。
密钥安全:如果使用传统密钥签名,确保密钥在 CI/CD 中的安全存储(GitHub Secrets、GitLab Variables 等)。
7.11 本章小结
| 平台 | 签名方式 | 配置复杂度 | 关键配置 |
|---|---|---|---|
| GitHub Actions | 无密钥 | 低 | id-token: write |
| GitLab CI | 无密钥 | 中 | id_tokens |
| Jenkins | 密钥/KMS | 高 | 凭据管理 |
| Azure DevOps | 无密钥 | 中 | OIDC 服务连接 |
扩展阅读
下一章:08 - 私有实例部署 — 部署和运维私有 Rekor 实例的完整指南。