Harbor 项目级内容信任(Content Trust)实战指南:让未签名镜像无法被拉取
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
导读:Harbor 提供了项目级内容信任能力,可在某个项目上强制“只允许拉取已签名镜像”,从而在制品分发链路中落实供应链安全基线。本文以 Harbor 仓库中的项目级内容信任测试用例 9-30-Project-level-content-trust.md 为核心骨架,逐步演示如何创建项目、开启信任策略、分别推送签名与未签名镜像并验证拉取结果;同时结合 Harbor 源码(内容信任中间件、项目元数据定义与对应测试)深入讲解该策略在拉取请求链路上的底层判定逻辑,帮助你既能在真实环境中复现验证,也能理解其实现原理。
一、什么是项目级内容信任
Harbor 的内容信任(Content Trust)机制,本质上是把“镜像是否被签名”作为一个拉取许可条件:当某个项目启用了内容信任策略后,凡是向该项目发起的镜像拉取请求,Harbor 都会先检查目标制品是否带有有效签名,未签名的镜像将被拒绝拉取。
这一能力在 Harbor 中是按项目维度配置的(区别于全局配置),因此不同项目可以采用不同的安全基线:
- 允许项目随意拉取所有镜像(默认行为,不开启内容信任);
- 要求镜像必须有Notation签名才能拉取(
enable_content_trust); - 要求镜像必须有Cosign签名才能拉取(
enable_content_trust_cosign); - 两者同时开启时,镜像需同时具备两种签名才能拉取。
从源码看,这两项策略分别对应项目元数据中的两个键,定义于 src/pkg/project/models/pro_meta.go:
ProMetaEnableContentTrust = "enable_content_trust" ProMetaEnableContentTrustCosign = "enable_content_trust_cosign"前端“项目配置 → 策略”页面中的两个开关,也正是读写这两个元数据键,参见 project-policy-config.component.ts 与对应模板 project-policy-config.component.html。
二、验证内容信任所需的环境
原文测试用例(9-30-Project-level-content-trust.md)明确给出了环境前提:
- 一个正在运行且可访问的 Harbor 实例;
- 一台装有 Docker CLI 的 Linux 主机(作为 Docker 客户端);
- 若 Harbor 使用了自签名证书,需要先把 CA 根证书拷贝到客户端对应目录:
/etc/docker/certs.d/<harbor_ip>/(Docker 拉取镜像时校验用);$HOME/.docker/tls/<harbor_ip>:4443/(Notary 签名服务校验用)。
在后续所有命令中,<harbor_ip>需要替换为 Harbor 的 IP 或 FQDN;项目名建议使用有实际含义、较长一些的名字(避免与常见短名冲突),原文中项目名用a占位,实际使用时建议换成如content-trust-demo这样的名字。
说明:原文基于 Docker Content Trust(Notary)签名方案编写,其签名服务端口为 4443。当前 Harbor 源码中的项目级内容信任中间件已经同时支持Cosign与Notation两类签名(详见本文第四节),Docker/Notary 方案仍可作为历史验证路径参考。
三、完整验证步骤(核心实操)
按照原文测试步骤,完整的端到端验证流程如下。
第 1 步:登录 UI 并创建项目
用管理员(或具备项目创建权限的用户)登录 Harbor Web 控制台,创建项目。创建完成后先不要开启内容信任,因为我们首先要向该项目推送一个“未签名”镜像作为对照组。
第 2 步:先推送一个未签名镜像
在 Docker 客户端正常(不设置任何签名环境变量)登录 Harbor 后,推送第一个镜像:
docker login <harbor_ip> docker tag nginx:latest <harbor_ip>/<project_a>/nginx:unsigned docker push <harbor_ip>/<project_a>/nginx:unsigned此时该镜像是未签名的,作为后续对照组使用。
第 3 步:在客户端开启 Docker Content Trust
在推送第二个(签名)镜像前,先设置 Docker Content Trust 相关环境变量:
export DOCKER_CONTENT_TRUST=1 export DOCKER_CONTENT_TRUST_SERVER=https://<harbor_ip>:4443DOCKER_CONTENT_TRUST=1:要求 Docker 客户端在 push/pull 时使用 Notary 签名;DOCKER_CONTENT_TRUST_SERVER:指定 Notary 签名服务地址(Harbor 的 Notary 服务默认监听 4443 端口)。
然后重新登录 Harbor(部分场景下需要重新登录以获取签名权限的凭证):
docker login <harbor_ip>第 4 步:推送一个已签名镜像
保持上述环境变量生效,推送第二个镜像。Docker 会引导你为签名设置 root key / repository key 的密码,随后将镜像连同签名一起推送到 Harbor:
docker tag nginx:latest <harbor_ip>/<project_a>/nginx:signed docker push <harbor_ip>/<project_a>/nginx:signed第 5 步:在项目配置页开启“项目级内容信任”
回到 Harbor UI,进入项目a的配置(Configuration)页面,开启项目级内容信任(Content Trust)开关并保存。
此时项目a的策略变为:任何来自该项目的镜像拉取请求,都必须携带有效签名。
第 6 步:拉取第一个(未签名)镜像 —— 应失败
docker pull <harbor_ip>/<project_a>/nginx:unsigned第 7 步:拉取第二个(已签名)镜像 —— 应成功
docker pull <harbor_ip>/<project_a>/nginx:signed预期结果
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 第 6 步 | 拉取首次推送的未签名镜像 | 拉取失败,Harbor 拒绝未签名镜像 |
| 第 7 步 | 拉取第二次推送的已签名镜像 | 拉取成功 |
两组结果对比即可证明:项目级内容信任策略只拦截“未签名”制品,而不会影响“已签名”制品的正常拉取。
四、底层原理:拉取链路上的内容信任中间件
项目级内容信任在 Harbor 中是通过注册在 Registry API 路由上的中间件实现的。在 src/server/registry/route.go 中可以看到,针对清单获取的GET与HEAD请求:
root.NewRoute(). Method(http.MethodGet). Path("/*/manifests/:reference"). Middleware(metric.InjectOpIDMiddleware(metric.ManifestOperationID)). Middleware(repoproxy.ManifestMiddleware()). Middleware(contenttrust.ContentTrust()). Middleware(vulnerable.Middleware()). HandlerFunc(getManifest)即docker pull解析镜像清单(manifest)时,会先经过contenttrust.ContentTrust()中间件做签名校验。中间件核心逻辑位于 src/server/middleware/contenttrust/contentrust.go:
// If signature policy enabled, it has to at least have one signature. if pro.ContentTrustCosignEnabled() { if err := signatureChecking(ctx, r, af, pro.ProjectID, model.TypeCosignSignature); err != nil { if errors.IsErr(err, errors.PROJECTPOLICYVIOLATION) { return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed by cosign.") } return err } } if pro.ContentTrustEnabled() { if err := signatureChecking(ctx, r, af, pro.ProjectID, model.TypeNotationSignature); err != nil { if errors.IsErr(err, errors.PROJECTPOLICYVIOLATION) { return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage("The image is not signed by notation.") } return err } }其判定流程可以概括为四步:
- 取出制品信息并定位项目:从请求上下文中取得
lib.ArtifactInfo(仓库名、引用等),再通过project.Ctl.GetByName拿到项目对象; - 读取项目信任策略:根据项目的
enable_content_trust_cosign/enable_content_trust元数据决定要检查哪种签名(Cosign 与 Notation 分别独立判定); - 获取制品及其配件(Accessories):通过
artifact.Ctl.GetByReference读取制品,并携带WithAccessory: true选项取回附属对象列表; - 在 Accessories 中查找签名:若制品没有任何 Accessories,或所有 Accessories 的类型都不匹配要求的签名类型(
model.TypeCosignSignature/model.TypeNotationSignature),则返回策略违规错误PROJECTPOLICYVIOLATION,请求被拒绝。
也就是说,签名在 Harbor 的数据模型中是作为制品的Accessory(附属物)存在的:Cosign 签名(cosign镜像,类型TypeCosignSignature)与 Notation 签名(类型TypeNotationSignature)会以子制品的形式挂接在目标镜像上,中间件通过检查这些附属物来确定“该镜像是否已被签名”。
五、中间件测试用例:验证判定行为
Harbor 为内容信任中间件编写了完备的单元测试,位于 src/server/middleware/contenttrust/contentrust_test.go。这些测试直观地印证了上文总结的判定行为:
| 测试用例 | 场景 | 预期状态码 |
|---|---|---|
TestContentTrustDisabled | 项目未开启 Cosign 内容信任 | 200 OK |
TestAuthenticatedUserPulling | 开启信任策略但制品无签名配件 | 412 Precondition Failed |
TestUnAuthenticatedUserPulling | 匿名用户拉取无签名镜像 | 412 Precondition Failed |
TestCosignPulling | 携带User-Agent: cosign的客户端拉取(签名推送前置动作) | 200 OK |
TestScannerPulling | 扫描器(scanner)拉取镜像 | 200 OK |
TestCosignSignaturePulling | 制品带 Cosign 签名配件 | 200 OK |
TestNotationSignaturePulling | 制品带 Notation 签名配件 | 200 OK |
TestBothSignaturePulling | 同时开启两类策略且两种签名都存在 | 200 OK |
其中“412 Precondition Failed(前提条件不满足)”正是 Harbor 拒绝未签名镜像时返回给客户端的语义化状态码,与你执行docker pull时看到的失败相互对应。
测试中项目元数据的构造方式也印证了配置键与行为的映射关系:
suite.project = &proModels.Project{ Name: "library", Metadata: map[string]string{ proModels.ProMetaEnableContentTrustCosign: "true", }, }哪些“拉取”会被放行
值得注意的是,中间件并不会一刀切地拦截所有请求。在 src/server/middleware/util/util.go 的SkipPolicyChecking中,以下三类场景会被显式跳过策略检查:
- 扫描器拉取:扫描器使用
v2token安全上下文且具备ActionScannerPull权限时可绕过,保证漏洞扫描不受签名策略影响; - Cosign / Notation 签名工具拉取:签名工具(User-Agent 含
cosign或notation)且具备ActionPush权限时,可以在推送签名之前先拉取目标 manifest——这是“先签名后推送”流程的必要前提; - 直接拉取签名附属物本身:当请求的制品本身就是 Cosign / Notation 签名(Accessory 类型匹配)时直接放行。
这解释了内容信任策略在“放行合法工具”与“拦截普通未签名拉取”之间如何取得平衡,也是你在排查“为什么签名流程没被拦截、普通 pull 却被拦下”时最需要关注的分支逻辑。
六、实际使用建议与故障排查
使用建议
- 先用对照组验证策略:按本文步骤先推“未签名 + 已签名”两个镜像再开启策略,能最快确认策略生效,避免把镜像损坏、认证失败等其他问题误判为内容信任拦截;
- 结合 Cosign / Notation 使用:当前 Harbor 更推荐使用 Cosign(icons/cosign.png 对应工具生态)与 Notation(icons/notation.png)方案,二者均可通过 Harbor UI 的“内容信任-Cosign / 内容信任-Notation”两个开关分别开启,并支持同时启用双签名策略;
- 注意扫描器与签名工具的绕行路径:若你观察到某些客户端(扫描器、签名工具)在策略开启后仍能拉取,这属于
SkipPolicyChecking的设计行为,而非策略失效。
常见问题排查
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 未签名镜像在开启策略后仍能拉取 | 策略未保存成功,或该请求命中绕行分支 | 到项目“配置”页确认开关状态;检查请求方是否属于扫描器/签名工具 |
| 已签名镜像无法拉取 | 签名类型与策略不匹配(例如只签了 Cosign 却开启了 Notation 策略) | 核对策略开关与镜像实际签名类型;双策略场景需同时具备两种签名 |
docker pull报 412 相关错误 | Harbor 判定镜像未签名 | 使用docker trust sign、cosign sign或notation sign完成签名后重试 |
| 自签名证书环境下签名/拉取异常 | 证书未导入客户端信任目录 | 将 CA 证书分别放入/etc/docker/certs.d/<harbor_ip>/与$HOME/.docker/tls/<harbor_ip>:4443/ |
七、小结
项目级内容信任是 Harbor 将“镜像签名”与“拉取许可”直接挂钩的安全策略:开启后,未签名镜像的拉取请求会在 Registry manifest 路由上被 contenttrust 中间件 拦截并返回失败,而已签名镜像不受影响。本文从测试用例 9-30-Project-level-content-trust.md 出发,给出了完整的可复现验证流程,并从 contentrust.go、route.go、util.go 与 contentrust_test.go 等源码中还原了其判定链路,帮助你既能在生产环境安全落地“仅允许拉取已签名镜像”的策略,也能在出现异常时快速定位到具体的拦截分支。
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考