各位同仁,下午好!
今天,我们将深入探讨一个在现代软件开发中日益关键的议题:软件供应链的透明度与安全性。特别是,我们将聚焦于如何利用 SLSA(Supply Chain Levels for Software Artifacts)框架,在 Go 项目的构建流程中实现端到端的工件签名与来源校验。这不仅仅是一项技术实践,更是我们作为开发者对构建可信赖软件的承诺。
1. 软件供应链安全:迫在眉睫的挑战
近年来,软件供应链攻击事件频发,从 SolarWinds 到 Log4Shell,每一次都给行业敲响了警钟。攻击者不再满足于直接攻击最终应用,而是转向利用软件开发和分发过程中的薄弱环节。
什么是软件供应链?
它不仅仅是你的代码仓库。一个典型的软件供应链包含:
- 开发者: 编写、测试代码。
- 依赖项: 第三方库、框架、操作系统组件。
- 构建系统: 编译器、构建工具、CI/CD 平台。
- 工件仓库: 存储二进制文件、容器镜像、包。
- 分发渠道: 包管理器、容器注册表。
- 部署环境: 运行软件的服务器、容器编排平台。
任何一个环节的漏洞,都可能导致恶意代码被注入、篡改,最终影响到数百万用户。
为什么需要透明度?
在传统的软件分发模式中,我们往往只能信任最终的二进制文件,却无法深入了解其“身世”。缺乏透明度意味着:
- 无法验证来源: 你无法确定一个二进制文件是否真的由声称的组织构建,也无法知道它是否经过了未经授权的修改。
- 无法审计构建过程: 当出现安全漏洞时,你难以追溯是哪个依赖项、哪个构建步骤引入了问题。
- 信任链断裂: 整个软件交付过程缺乏一个可验证的信任链。
为了解决这些问题,我们需要引入一套机制,确保从代码提交到最终部署的每一个环节都可追溯、可验证。这就是 SLSA 的核心目标。
2. SLSA 概述:构建可信赖的软件供应链
SLSA,全称 Software Supply Chain Levels for Software Artifacts,是由 Google 发起并与社区共同维护的一套安全框架和标准。它定义了软件工件供应链安全的四个层级,每个层级都提出了更严格的要求,旨在逐步提升软件的完整性、可审计性和可靠性。SLSA 的核心思想是,通过一系列的自动化措施,让软件消费者能够验证他们所使用的软件是否按照预期的、安全的方式构建。
SLSA 的四个安全级别
SLSA 框架将供应链安全能力划分为四个递增的级别,从 SLSA 1 到 SLSA 4。每个级别都建立在前一个级别的基础上,并增加了一组更严格的控制措施。
| SLSA 级别 | 核心目标 | 主要要求 | 适用场景 |
|---|---|---|---|
| SLSA 1 | 自动化构建 | 确保构建过程是自动化的,而非手动执行。 | 能够生成基本的构建来源信息,但可能不完全可信。 |
| SLSA 2 | 签名来源 | 要求构建系统能够生成并签名构建来源证明(Provenance),证明构建过程在受控环境中进行。 | 消费者可以通过验证签名,信任来源信息来自一个特定的构建系统。 |
| SLSA 3 | 加固的构建 | 引入严格的构建环境隔离(如临时的、密封的构建器),构建脚本不可篡改。确保构建输出与构建脚本一致。 | 极大地降低了构建系统被篡改的风险,提供了高度可信的来源信息。 |
| SLSA 4 | 双人评审 & 可信依赖 | 要求对所有代码变更进行双人评审。所有依赖项都必须满足 SLSA 级别要求。构建系统本身也需达到 SLSA 3。 | 最高级别的安全性,适用于对安全性要求极高的关键基础设施。 |
在 Go 项目中实现 SLSA,通常意味着我们将努力达到 SLSA 3 甚至 SLSA 4。这意味着我们不仅要自动化构建,还要确保构建过程本身是可信的、可审计的,并且最终工件及其来源证明都经过了数字签名。
SLSA 如何帮助 Go 项目?
Go 语言以其简洁、高效和强大的标准库而闻名,广泛应用于各种系统级和网络服务。然而,Go 项目同样面临供应链安全的挑战。通过实施 SLSA,Go 项目可以:
- 增强二进制文件的完整性: 确保发布的 Go 二进制文件或容器镜像未被篡改。
- 提供可验证的构建历史: 消费者可以检查 Go 程序的构建环境、依赖项和编译参数。
- 提升对依赖项的信任: 更好地管理和验证 Go 模块依赖的来源。
- 自动化合规性检查: 将安全和合规性要求融入 CI/CD 流程。
3. 核心概念:工件签名与来源证明
要实现 SLSA 的目标,我们需要理解并实践两个核心概念:工件签名和来源证明。
3.1. 工件签名 (Artifact Signing)
工件签名是确保软件完整性和来源真实性的基石。它通过数字签名技术,为二进制文件、容器镜像、包等工件附上一个“防伪标签”。
为什么需要签名?
- 防止篡改: 任何对签名的工件的修改都会使签名失效,从而立即暴露篡改行为。
- 验证来源: 签名可以证明工件确实是由某个特定的实体(开发者、组织、CI/CD 系统)发布。
- 建立信任: 消费者可以通过验证签名,建立对工件的信任。
常见的签名技术
历史上,我们使用过 GPG (GNU Privacy Guard) 或基于 PKI (Public Key Infrastructure) 的证书进行签名。然而,这些方法在密钥管理、证书分发和吊销方面存在一定的复杂性,尤其是在大规模的自动化 CI/CD 环境中。
Sigstore:现代签名解决方案
Sigstore 是一个由 Linux Foundation 支持的开源项目,旨在简化软件签名过程,使其对开发者和消费者都更易用。它提供了一个免费、开放的软件签名服务,核心组件包括:
- Cosign: 用于对容器镜像、二进制文件、SBOMs (Software Bill of Materials) 等工件进行签名和验证的命令行工具。它支持 OCI (Open Container Initiative) 注册表存储签名。
- Fulcio: 一个免费的短期证书颁发机构 (Certificate Authority)。它利用 OIDC (OpenID Connect) 身份验证,为开发者和 CI/CD 系统颁发有效期极短(例如,10分钟)的 X.509 证书。这消除了长期密钥管理的负担,并降低了密钥泄露的风险。
- Rekor: 一个透明度日志(Transparency Log)。所有通过 Fulcio 颁发的证书和由 Cosign 生成的签名都会被记录到 Rekor 中。Rekor 提供了一个公开、不可篡改的审计日志,任何人都可以查询和验证某个签名是否存在。
Sigstore 的优势在于:
- 无密钥管理负担: 利用 OIDC 和短期证书,无需手动管理长期私钥。
- 透明度: Rekor 提供了公开审计,防止签名被静默篡改或伪造。
- 易用性: Cosign 提供了简洁的 CLI 接口。
- OCI 兼容性: 利用 OCI 注册表存储签名,与现有工具链无缝集成。
Go 项目中的实践:使用 Cosign 签名 Go 二进制文件和容器镜像
假设我们有一个简单的 Go 应用:
// main.go
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Printf("Hello from Go application!n")
fmt.Printf("Built with Go version: %sn", runtime.Version())
fmt.Printf("Current time: %sn", time.Now().Format(time.RFC3339))
}
步骤 1: 构建 Go 应用
首先,我们需要编译 Go 应用程序。
# 交叉编译一个 Linux AMD64 架构的二进制文件
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o myapp main.go
# 查看构建的二进制文件
ls -lh myapp
# -rwxr-xr-x 1 user user 7.3M Jan 1 10:00 myapp
步骤 2: 使用 Cosign 签名二进制文件
在实际的 CI/CD 环境中,我们会配置 Cosign 使用 OIDC 身份验证(例如,GitHub Actions 的 OIDC 提供者)。但在这里,我们可以先演示使用本地生成的密钥对签名。
# 生成 Cosign 密钥对 (首次使用时)
# 这会生成 cosign.key 和 cosign.pub
# 并且会提示你输入一个密码来保护私钥
cosign generate-key
现在,使用生成的私钥签名 myapp 二进制文件。
# 签名二进制文件
# 会提示输入私钥密码
cosign sign-blob --key cosign.key myapp
# 签名成功后,会在当前目录生成一个 myapp.sig 文件
ls -lh myapp myapp.sig
# -rwxr-xr-x 1 user user 7.3M Jan 1 10:00 myapp
# -rw-r--r-- 1 user user 766 Jan 1 10:05 myapp.sig
步骤 3: 验证签名
任何人都可以使用公钥和 Rekor 透明度日志来验证这个签名。
# 验证二进制文件
# Cosign 会下载 myapp.sig,然后使用 cosign.pub 和 Rekor 验证它。
cosign verify-blob --key cosign.pub myapp
如果验证成功,你将看到类似 Verified OK 的输出。如果二进制文件或签名文件被篡改,验证将失败。
步骤 4: 签名 Go 容器镜像
对于 Go 项目,通常最终工件是容器镜像。使用 Cosign 签名容器镜像是其最常见的用法之一。
首先,构建 Docker 镜像:
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /myapp main.go
FROM alpine:latest
WORKDIR /
COPY --from=builder /myapp /myapp
CMD ["/myapp"]
# 构建 Docker 镜像
docker build -t myregistry/mygoapp:v1.0.0 .
# 推送镜像到容器注册表
docker push myregistry/mygoapp:v1.0.0
现在,使用 Cosign 签名这个容器镜像。同样,在 CI/CD 中通常通过 OIDC 实现无密钥签名。这里我们使用 cosign sign 命令,它会通过 Fulcio 颁发短期证书,并记录到 Rekor。
# 登录容器注册表 (如果需要)
# docker login myregistry
# 使用 Cosign 签名镜像
# 此时会通过 OIDC 流程,弹出浏览器窗口进行身份验证
# 成功后,Cosign 会使用 Fulcio 颁发短期证书,并用该证书签名镜像。
# 签名和证书都会被记录到 Rekor。
cosign sign myregistry/mygoapp:v1.0.0
签名成功后,Cosign 会在 OCI 注册表中存储一个额外的签名工件(通常是一个 OCI Image Manifest List),其标签为 myregistry/mygoapp:v1.0.0.sig 或类似。
步骤 5: 验证容器镜像签名
消费者可以通过一行命令验证镜像的签名,无需知道公钥(因为 Rekor 和 Fulcio 提供了透明度)。
# 验证镜像签名
cosign verify myregistry/mygoapp:v1.0.0
Cosign 会自动从 Rekor 获取信任根,并验证签名是否由一个可信的身份(OIDC 身份)在 Rekor 中记录。你将看到签名的身份信息(例如,GitHub 用户的电子邮件或 CI/CD 工作流的身份)。
3.2. 来源证明 (Provenance)
工件签名解决了“谁发布了这个工件”和“它是否被篡改”的问题。但它没有回答“这个工件是如何被构建的?”。这就是来源证明的作用。
什么是来源证明?
来源证明是对一个软件工件的构建过程的结构化、可验证的记录。它回答了以下问题:
- 谁构建了它? (身份)
- 何时构建的? (时间戳)
- 在哪里构建的? (构建环境)
- 使用了哪些源代码? (代码仓库、提交哈希)
- 使用了哪些输入依赖? (Go 模块、其他包)
- 使用了哪些构建命令? (编译参数、工具链版本)
- 产生了哪些输出工件? (文件名、哈希)
为什么需要来源证明?
- 可追溯性: 当发现漏洞时,可以追溯到特定的源文件、依赖项或构建步骤。
- 可审计性: 监管机构或安全团队可以审计构建过程是否符合安全策略。
- 供应链完整性: 确保构建过程本身是安全的,没有注入恶意代码。
- 可重复构建 (Reproducible Builds): 理想的来源证明应该包含足够的信息,使得理论上可以在相同的环境中重新构建出完全相同的工件。
SLSA Provenance Specification (in-toto attestations)
SLSA 来源证明基于 in-toto 框架,使用 JSON 格式的 attestation(证明)来描述构建过程。in-toto attestation 是一个灵活的框架,用于表达关于软件供应链中各种事件的声明。
一个标准的 SLSA 来源证明(以 in-toto Predicate 格式)通常包含以下关键字段:
_type: 固定为https://in-toto.io/Statement/v0.1。subject: 描述被证明的工件,包含其名称和加密哈希。predicateType: 固定为https://slsa.dev/provenance/v0.2(或更高版本)。predicate: 实际的来源证明数据,包含:builder: 描述执行构建的实体(例如,CI/CD 系统)。buildType: 构建的类型(例如,https://github.com/slsa-framework/slsa-github-generator/builders/go@v1)。invocation: 描述构建的触发方式,包括参数、环境等。materials: 输入材料的列表(源仓库、依赖项),包含 URI 和哈希。metadata: 构建的时间戳、是否成功等。
Go 项目中的实践:生成和验证来源证明
手动生成一个完整的 SLSA 来源证明非常复杂,因为它需要准确记录构建环境的每一个细节。因此,通常我们会依赖自动化工具来完成。
自动化生成 Provenance (GitHub Actions 示例)
GitHub Actions 是一个流行的 CI/CD 平台,它与 SLSA 框架和 Sigstore 生态系统有很好的集成。slsa-github-generator 是一个官方推荐的 GitHub Actions 工作流,可以自动生成 SLSA 兼容的来源证明,并使用 Sigstore 进行签名。
假设我们有一个 Go 项目,并希望在 GitHub Actions 中构建并发布。
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*.*.*' # 当有 vX.Y.Z 标签推送时触发
permissions:
contents: write # 允许写入 releases
id-token: write # 用于 OIDC 身份验证
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 确保获取完整的历史记录以便生成正确的 provenance
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Build Go application
run: |
CGO_ENABLED=0 go build -o myapp main.go
# 关键步骤:使用 slsa-github-generator 生成签名和 provenance
# 这个 action 会自动调用 Cosign,使用 GitHub OIDC 身份验证
# 然后通过 Fulcio 获取短期证书,签名 myapp 和 provenance,
# 并将签名和 provenance 记录到 Rekor。
# 最终,它会把签名和 provenance 作为 release assets 上传。
- uses: slsa-framework/slsa-github-generator/.github/actions/[email protected]
with:
# 要签名的工件列表
generate-release-assets: |
myapp
# 构建器的类型,有助于生成正确的 provenance
builder-id: 'https://github.com/slsa-framework/slsa-github-generator/builders/go@v1'
# 构建命令,用于 provenance 中记录
build-command: |
go build -o myapp main.go
当这个工作流运行时:
slsa-github-generator会检测到构建命令和输出工件myapp。- 它会捕获构建环境的元数据(Go 版本、操作系统、源代码提交信息等)。
- 它会构造一个符合
https://slsa.dev/provenance/v0.2规范的 JSON 对象(即 provenance attestation)。 - 它会使用 GitHub Actions 的 OIDC 身份验证,通过 Fulcio 获取一个短期证书。
- 它会使用这个证书同时签名
myapp二进制文件和生成的 provenance attestation。 - 签名和 provenance attestation 都会被记录到 Rekor 透明度日志中。
- 最终,
myapp二进制文件、其签名文件(.sig)以及 provenance attestation 文件(.att)会被作为 GitHub Release 的资产发布。
验证 Provenance
消费者可以通过 Cosign 命令行工具来验证工件及其关联的 provenance。
# 首先,下载二进制文件和 provenance attestation 文件 (例如,从 GitHub Release)
# wget https://github.com/your-org/your-repo/releases/download/v1.0.0/myapp
# wget https://github.com/your-org/your-repo/releases/download/v1.0.0/myapp.att
# 验证二进制文件及其来源证明
# Cosign 会验证签名,并解析 provenance attestation。
# 它会检查 provenance 中的 subject 哈希是否与 myapp 的实际哈希匹配,
# 并且验证 provenance 的签名是否来自预期的构建器身份。
cosign verify-attestation --type slsaprovenance myapp
--policy-controller 'gh.uri == "https://github.com/your-org/your-repo"'
--policy-controller 'gh.event_name == "push"'
--policy-controller 参数允许你定义策略,例如,要求 provenance 必须来自你的 GitHub 仓库 (gh.uri) 并且是由 push 事件触发的(确保不是手动构建)。Cosign 会根据这些策略来判断 provenance 是否可信。
如果验证成功,你将看到 provenance 的详细信息,包括 builder 的身份、源代码仓库、提交哈希、构建命令等。你可以人工检查这些信息,确保它们符合你的预期。
4. 深入实践:Go 项目中的端到端 SLSA 实施
实现 SLSA L3 或 L4 要求我们在整个 Go 项目的生命周期中整合安全措施。
4.1. 构建环境的强化 (SLSA L2/L3)
SLSA 对构建环境有严格的要求,以确保构建过程本身是可信的。
-
隔离的构建环境 (Ephemeral Builders):
- 构建应该在一个短暂的、一次性的、隔离的环境中进行,例如容器或虚拟机。
- 每次构建都应该从一个干净的状态开始,避免构建之间的状态泄漏和干扰。
- GitHub Actions、GitLab CI/CD、Tekton 等现代 CI/CD 平台通常默认提供这种隔离。
- Go 特性: Go 语言的静态链接特性使得生成的二进制文件通常不依赖于运行时环境的动态库,这进一步简化了构建环境的隔离,因为你只需要确保 Go 编译器和相关工具链是干净的。
-
可重复构建 (Reproducible Builds):
- 理论上,给定相同的源代码、构建工具链和环境参数,每次构建都应该产生位精确相同的输出工件。
- Go 特性: Go 语言在可重复构建方面表现良好。
go build命令通常会产生确定的输出。然而,一些因素仍可能影响可重复性,例如:- 时间戳: 默认情况下,Go 编译器会嵌入构建时间戳。可以使用
go build -ldflags="-X main.buildTime=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"或-ldflags="-X main.buildTime=unknown"来控制。 - 路径: 构建机器上的绝对路径可能会被嵌入。使用相对路径或在容器中构建可以缓解。
- 编译器版本: 确保每次构建都使用精确指定的 Go 编译器版本。
- 时间戳: 默认情况下,Go 编译器会嵌入构建时间戳。可以使用
- 来源证明应该包含足够的信息来验证或尝试重现构建。
-
构建脚本的签名与验证:
- 构建过程本身是由 CI/CD 配置文件(例如
.github/workflows/*.yml)定义的。 - 这些配置文件应该受到版本控制,并通过代码审查进行保护。
- 对于 SLSA L4,甚至要求对这些构建配置文件的修改也进行双人评审。
- SLSA 来源证明捕获了构建脚本的提交哈希,这使得消费者能够验证构建是基于特定的、已审查的脚本。
- 构建过程本身是由 CI/CD 配置文件(例如
-
工具链的信任:
- 确保 Go 编译器、Docker、Cosign 等构建工具本身是可信的,并且其版本是已知的和锁定的。
- 在 Dockerfile 中,明确指定
golang:镜像的版本,例如golang:1.21.6-alpine,而不是golang:latest。 - 在 GitHub Actions 中,使用
actions/setup-go@v5并指定go-version。
4.2. Go 模块依赖管理与 SLSA
Go 模块是 Go 项目管理依赖项的核心机制。SLSA 要求我们对依赖项有清晰的了解和信任。
-
go.mod和go.sum的重要性:go.mod定义了项目的直接和间接依赖项及其版本。go.sum包含了所有模块的加密哈希,用于验证下载的模块是否与预期的一致,防止模块被篡改。- 在 SLSA 来源证明中,
materials字段会包含go.mod和go.sum的哈希,确保这些关键文件未被篡改。
-
依赖项的签名与验证 (未来趋势):
- 目前,Go 模块系统主要依赖
go.sum进行完整性校验。 - 未来,Sigstore 社区正在探索如何将签名机制扩展到 Go 模块本身,使得开发者可以验证下载的模块是否已由其作者签名。这将进一步提升 Go 模块供应链的安全性。
- 当前最佳实践:
- 定期审计
go.mod和go.sum,移除不必要的依赖。 - 使用 Go 官方代理 (
proxy.golang.org),它会对模块进行缓存和校验。 - 考虑使用私有 Go 代理,以更好地控制和审计内部依赖。
- 定期审计
- 目前,Go 模块系统主要依赖
-
SBOMs (Software Bill of Materials):
- SBOM 是一种列出软件中所有组件及其依赖关系的清单。
- 虽然不是 SLSA 的强制要求,但生成 Go 项目的 SBOM 可以极大地增强透明度。
- 工具如
syft或go-licenses可以帮助生成 Go 项目的 SBOM。 - 可以将 SBOM 作为工件的一部分进行签名和分发,并将其在 provenance 中引用。
4.3. CI/CD 管道集成 (SLSA L3/L4)
CI/CD 管道是实现 SLSA 的关键场所。我们需要确保构建、签名和来源证明生成都自动化并集成到管道中。
GitHub Actions 示例 (进阶)
在前文的基础上,我们可以进一步细化 GitHub Actions 工作流以满足 SLSA L3/L4 要求。
# .github/workflows/release.yml
name: Release with SLSA Provenance and Signed Artifacts
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write # 允许写入 releases
id-token: write # 用于 OIDC 身份验证,Cosign/Fulcio 需要
jobs:
build-and-release:
runs-on: ubuntu-latest
env:
GO_VERSION: '1.21.6' # 锁定 Go 版本
APP_NAME: 'mygoapp'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for correct provenance generation
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true # 缓存 Go 模块
- name: Go Mod Tidy and Verify
run: |
go mod tidy
go mod verify
# 确保 go.sum 是最新的且与 go.mod 匹配
git diff --exit-code go.mod go.sum || (echo "go.mod or go.sum changed, please commit them." && exit 1)
- name: Build Go application
run: |
# 编译 Go 应用,使用 ldflags 注入版本信息和构建时间,提升可追溯性
VERSION=${{ github.ref_name }}
BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
GO_BUILD_COMMAND="CGO_ENABLED=0 go build -ldflags="-X 'main.version=$VERSION' -X 'main.buildTime=$BUILD_TIME'" -o ${{ env.APP_NAME }}"
echo "GO_BUILD_COMMAND=${GO_BUILD_COMMAND}" >> $GITHUB_ENV
eval "${GO_BUILD_COMMAND}"
# 确保构建命令在 provenance 中有记录
- name: Generate SBOM (Optional but Recommended for SLSA L4)
# 使用 Syft 生成 SBOM,并将其作为工件的一部分进行签名
uses: anchore/[email protected]
with:
output-file: ${{ env.APP_NAME }}.sbom.json
format: spdx-json
artifact-name: ${{ env.APP_NAME }}
# SBOM for the Go binary itself, not the container image yet
path: ${{ env.APP_NAME }}
- name: Sign and Generate SLSA Provenance
# 这个 Action 会处理:
# 1. 使用 GitHub OIDC 身份验证
# 2. 通过 Fulcio 获取短期证书
# 3. 使用 Cosign 签名所有指定的工件(myapp, myapp.sbom.json)
# 4. 生成 SLSA Provenance attestation(记录构建详情)
# 5. 将签名和 provenance 记录到 Rekor 透明度日志
# 6. 将所有工件(二进制、SBOM、签名文件、attestation 文件)作为 Release Assets 上传
uses: slsa-framework/slsa-github-generator/.github/actions/[email protected]
with:
generate-release-assets: |
${{ env.APP_NAME }}
${{ env.APP_NAME }}.sbom.json
builder-id: 'https://github.com/slsa-framework/slsa-github-generator/builders/go@v1'
# 将实际的构建命令传递给 provenance
build-command: ${{ env.GO_BUILD_COMMAND }}
# 对于 SLSA L4,可能需要配置更多的 builder 策略,例如要求构建器是 Ephemeral 的。
- name: Build and Push Docker Image (Optional, if you publish containers)
id: docker_build_push
if: startsWith(github.ref, 'refs/tags/') # Only build/push on tags
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/${{ env.APP_NAME }}:${{ github.ref_name }}
ghcr.io/${{ github.repository_owner }}/${{ env.APP_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Sign Docker Image (if built)
if: steps.docker_build_push.outputs.image != ''
# 使用 Cosign 签名容器镜像,同样利用 OIDC 和 Fulcio
uses: sigstore/cosign-action/[email protected]
with:
command: sign
tag: ghcr.io/${{ github.repository_owner }}/${{ env.APP_NAME }}:${{ github.ref_name }}
# Cosign action 会自动处理 OIDC 身份验证和 Rekor 记录。
# 这里的签名也会生成一个针对容器镜像的 SLSA Provenance,并记录到 Rekor。
# 注意:此处的 provenance 专用于镜像构建,与上一步的二进制构建 provenance 略有不同。
其他 CI/CD 系统考虑:
- GitLab CI/CD: GitLab 提供了其自身的 OIDC 提供者。你可以配置 Cosign 来利用 GitLab 的 OIDC 身份进行签名。GitLab CI/CD 的 Runner 提供了容器化的构建环境,这有助于满足 SLSA 的隔离要求。
- Tekton: Tekton 是 Kubernetes 原生的 CI/CD 框架,非常适合实现高度隔离和可重复的构建。Tekton Chains 可以自动为 Tekton 构建生成 in-toto 来源证明,并与 Sigstore 集成进行签名和记录。
- Jenkins: 对于 Jenkins,你需要手动配置与 Sigstore 的集成。通常涉及在 Jenkins Agent 上安装 Cosign,并配置 OIDC 凭证提供者(如果 Jenkins 部署在支持 OIDC 的环境中,例如 Kubernetes)。
4.4. 分发与部署阶段的验证
SLSA 的价值最终体现在消费者能够验证所使用的软件的真实性和完整性。
-
消费者如何验证 Go 二进制文件、容器镜像和 Provenance?
- Go 二进制文件:
# 假设你下载了 myapp, myapp.sig, myapp.att # 验证签名 (确保二进制文件未被篡改) cosign verify-blob --key <public_key_or_identity> myapp # 验证 Provenance (确保构建过程可信) cosign verify-attestation --type slsaprovenance myapp --policy-controller 'gh.uri == "https://github.com/your-org/your-repo"' --policy-controller 'gh.event_name == "push"' - 容器镜像:
# 验证镜像签名 (确保镜像是官方发布且未被篡改) # Cosign 会从 Rekor 获取信息并验证签名者的身份 cosign verify ghcr.io/your-org/mygoapp:v1.0.0 # 验证镜像的 Provenance # 这会列出与该镜像相关的所有 attestations,包括 SLSA Provenance cosign verify --type slsaprovenance ghcr.io/your-org/mygoapp:v1.0.0验证输出会显示 provenance 的详细信息,你可以编写脚本来解析这些 JSON,并根据自己的安全策略进行判断。
- Go 二进制文件:
-
策略引擎在 Kubernetes 中的应用:
在 Kubernetes 环境中,你可以使用策略引擎(如 Kyverno 或 OPA Gatekeeper)来强制执行镜像签名和 Provenance 验证策略。-
Kyverno 示例: 你可以编写 Kyverno Policy,要求部署到集群中的所有容器镜像都必须带有有效的 Cosign 签名,并且其 Provenance 必须满足特定的 SLSA 级别或来源要求。
# policy.yaml (Kyverno Policy to enforce Cosign signature) apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-cosign-signature annotations: policies.kyverno.io/category: Supply Chain Security policies.kyverno.io/description: >- Requires all images to be signed by Cosign and verified against a trusted key/identity. spec: validationFailureAction: Enforce rules: - name: check-image-signature match: resources: kinds: - Pod verifyImages: - image: "*" # Apply to all images key: "kubernetes://<your-service-account-key-ref>" # Or use keyless verification via Rekor/Fulcio # 或者使用 keyless 模式,通过 OIDC 身份验证和 Rekor # key: "https://<your-oidc-issuer>" # annotations: # io.kubernetes.container.image/signed-by: "[email protected]" # dev.sigstore.cosign/identity: "https://github.com/your-org/your-repo/.github/workflows/release.yml@refs/heads/main" # dev.sigstore.cosign/issuer: "https://token.actions.githubusercontent.com" # Attestations verification attestations: - predicateType: "https://slsa.dev/provenance/v0.2" # 强制要求 provenance 包含特定的 builder ID policy: - image: "*" subject: - subject: name: "ghcr.io/your-org/mygoapp" #digest: "sha256:..." # 可以进一步限制特定 digest predicate: builder: id: "https://github.com/slsa-framework/slsa-github-generator/builders/go@v1" # 进一步检查 materials, invocation 等 materials: - uri: "git+https://github.com/your-org/your-repo.git" digest: sha1: "{{ request.object.spec.template.spec.containers[0].imagePullSecrets[0].name }}" # 示例,实际应从 Provenance 中匹配这个 Kyverno 策略会拦截所有 Pod 创建请求,并检查其中使用的容器镜像是否符合签名和 Provenance 要求。如果不符合,Pod 将无法部署。
-
-
程序化验证 Provenance:
在某些高级场景中,你可能需要在自己的应用程序或审计工具中程序化地解析和验证 Provenance。Cosign 提供了 Go 库,可以让你在 Go 代码中实现这些验证逻辑。package main import ( "context" "fmt" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/policy" ) func main() { ctx := context.Background() imageRef := "ghcr.io/your-org/mygoapp:v1.0.0" // 获取镜像的 OCI 引用 ref, err := remote.ParseReference(imageRef) if err != nil { fmt.Printf("Error parsing image reference: %vn", err) return } // 验证镜像的签名和 provenance // 这是一个简化的示例,实际生产环境需要更复杂的策略 opts := &cosign.CheckOpts{ VerifyOptions: policy.VerifyOptions{ // 信任根配置,通常从 Sigstore 的公共根获取 // 这里为了简化,假设已经配置了默认的 Sigstore 信任根 // 生产环境应明确指定 Fulcio root 和 Rekor public key }, // 对于 keyless 签名,通常不需要提供公钥,Cosign 会使用 Rekor // 假设我们信任 GitHub Actions 作为 Builder ClaimVerifier: cosign.IntotoSubjectClaimVerifier, // 验证 in-toto subject // 可以添加进一步的策略检查,例如 builder ID, source repo 等 // CustomAttestationPredicateVerifier: CustomProvenanceVerifier, } // 执行验证 // 返回的 `verified` 包含所有通过验证的签名和 attestations verified, _, err := cosign.VerifyImageSignatures(ctx, ref, opts) if err != nil { fmt.Printf("Image verification failed: %vn", err) return } fmt.Printf("Successfully verified image %sn", imageRef) // 遍历并打印 Provenance 信息 for _, v := range verified { if v.Attestation != nil && v.Attestation.PredicateType == "https://slsa.dev/provenance/v0.2" { fmt.Printf("Found SLSA Provenance:n") // 这里可以解析 v.Attestation.Predicate 字段的 JSON 内容 // 并根据业务需求进行进一步的验证 fmt.Printf(" Builder ID: %sn", v.Attestation.Predicate["builder"].(map[string]interface{})["id"]) fmt.Printf(" Source URI: %sn", v.Attestation.Predicate["materials"].([]interface{})[0].(map[string]interface{})["uri"]) // ... 更多 Provenance 字段 } } } /* // CustomProvenanceVerifier 可以用于定义更细粒度的 Provenance 策略 func CustomProvenanceVerifier(ctx context.Context, att *protobundles.Bundle, verifiedManifest *oci.SignedImage) error { // 解析 attestation 的 predicate var predicate map[string]interface{} if err := json.Unmarshal(att.GetAttestation().GetPayload(), &predicate); err != nil { return fmt.Errorf("failed to unmarshal predicate: %w", err) } // 示例:检查 builder ID builderID, ok := predicate["builder"].(map[string]interface{})["id"].(string) if !ok || builderID != "https://github.com/slsa-framework/slsa-github-generator/builders/go@v1" { return fmt.Errorf("untrusted builder ID: %s", builderID) } // 示例:检查 source repository URI materials, ok := predicate["materials"].([]interface{}) if !ok || len(materials) == 0 { return fmt.Errorf("no materials found in provenance") } sourceURI, ok := materials[0].(map[string]interface{})["uri"].(string) if !ok || !strings.HasPrefix(sourceURI, "git+https://github.com/your-org/your-repo.git") { return fmt.Errorf("untrusted source URI: %s", sourceURI) } // 可以在这里添加更多检查,例如版本、提交哈希、构建命令等 return nil } */这段代码展示了如何使用 Cosign 的 Go 库来验证容器镜像的签名和 Provenance。通过
cosign.VerifyImageSignatures函数,你可以获取到所有相关的签名和 attestations。然后,你可以遍历这些 attestations,并解析 SLSA Provenance 的内容,进行自定义的策略验证。
5. 挑战与未来方向
实施 SLSA 和软件供应链透明度并非一蹴而就,它伴随着一些挑战,同时也指明了未来的发展方向。
挑战:
- 实施复杂性: 尤其对于大型、复杂的项目,将 SLSA 要求集成到现有的 CI/CD 管道和开发流程中可能需要大量的投入和技术改造。
- 现有工具的成熟度: 尽管 Sigstore 和 SLSA 工具链正在迅速发展,但它们仍相对较新,可能需要一些时间和社区的努力才能完全成熟和稳定。
- 遗留系统的集成: 对于历史悠久的项目,其构建系统可能不是基于现代 CI/CD 平台,难以直接应用 SLSA 最佳实践。
- 开发者心智负担: 要求开发者理解并遵循新的安全流程,可能会增加他们的工作量和学习曲线。推广这些实践需要良好的文档、培训和工具支持。
- 策略定义与管理: 定义一套既能有效提升安全又能兼顾业务效率的验证策略是一项挑战。策略过多或过严可能阻碍开发,过少则失去意义。
未来方向:
- 自动化和工具化进一步完善: 期待更智能的工具,能够自动检测并修复 SLSA 违规,进一步降低开发者的心智负担。例如,CI/CD 平台将 SLSA 兼容性作为内置功能。
- 更广泛的生态系统采纳: 随着 SLSA 和 Sigstore 的普及,更多的第三方库、框架和工具将原生支持签名和 Provenance,使得整个软件生态系统更加安全。
- 链上透明度 (Blockchain for Provenance): 虽然 SLSA 主要依赖于 Rekor 这样的透明度日志,但将 Provenance 记录到区块链上也是一个值得探索的方向。区块链的不可篡改性和去中心化特性,可以为 Provenance 提供更强的信任保证。然而,这需要解决存储成本、性能和隐私等问题。
- 零信任架构的整合: SLSA 与零信任原则高度契合。在零信任环境中,每一个组件、每一个操作都需要被验证。SLSA 提供的签名和 Provenance 信息,正是实现这种验证的关键。
- 标准化和互操作性: 随着更多组织采用 SLSA,将会有更多的标准化工作,确保不同工具和平台之间能够无缝地交换和验证 Provenance 信息。
结论
软件供应链的透明度与安全性不再是“可选项”,而是现代软件工程的“必选项”。SLSA 框架为我们提供了一个清晰的路线图,指引我们如何逐步提升 Go 项目乃至整个软件生态系统的信任水平。通过深入理解并实践工件签名和来源证明,利用 Sigstore 生态系统和 CI/CD 自动化,我们可以构建一个更加安全、可信赖的软件交付管道。这是一个持续演进的过程,需要整个社区的共同努力和投入,以共同守护软件世界的安全与稳定。