Zot 2.1.22 客户端操作与签名校验指南
基于实测的 Zot v2.1.22 客户端全工作流指南:zli 配置与仓库/镜像/CVE 查询、skopeo 与 oras 推拉镜像和 OCI 制品、curl 直连原生 OCI Distribution API 全链路、GraphQL 扩展 API,以及 cosign 密钥签名/验签与服务端信任校验,全部回显为真实服务器实测。
Zot 2.1.22 客户端操作与签名校验指南
目标:讲透用 Zot 的完整客户端工作流——官方命令行 zli 的配置与查询、skopeo/oras 的镜像与 OCI 制品推拉、原生 OCI Distribution API 的 curl 全链路、GraphQL 扩展 API,以及 cosign 密钥签名/验签与服务端信任校验。本文全部回显均为真实服务器实测(zot v2.1.22,
http://localhost:8080,示例账号admin/q1w2e3r4、reader/q1w2e3r4),实测日期 2026-11-01;个别仅存输出记录的实验,命令行按官方语法标注。⚠️ 名称甄别:本文对象是 OCI 镜像仓库 Zot(project-zot/zot),与文献管理软件 Zotero 无关。
配套篇:选型见 《Zot 介绍与选型指南》,部署见 《Zot 2.1.22 Docker Compose 部署指南》,配置参数见 《Zot 2.1.22 配置参数详解》。
一、客户端工具链总览
zot 是纯服务端:所有日常操作都发生在客户端。本实验在服务器上装配了四件套——官方 CLI zli、通用镜像工具 skopeo、OCI 制品工具 oras、签名工具 cosign,加上系统自带的 curl。版本自检实测:
$ ./cosign --version
______ ______ _______. __ _______ .__ __.
/ | / __ \ / || | / _____|| \ | |
| ,----'| | | | | (----`| | | | __ | \| |
(实测节选,cosign 版本横幅前 4 行。)
$ ./oras version
Version: 1.3.6
Go version: go1.26.9
OS/Arch: linux/amd64
Git commit: 9b1fc815f6b7fba743f08b9b5c5846639cc11d3c
Git tree state: clean
$ ./skopeo --version
skopeo version 1.13.3
GitVersion: v3.1.4
GitCommit: 0e48502cf5ef34e2e60bbd3617aa19945712f705
GoVersion: go1.27.3
zli 与 zot 同源同版本发布(GitHub v2.1.22 Assets 里的 zli-linux-amd64,约 13.6 MB),实测版本回显与 zot 二进制同 commit:
$ ./zli --version
{"time":"2026-11-01T16:55:22.616015587+08:00","level":"info","message":"version","distribution-spec":"1.1.1","commit":"v2.1.22-0-g6bbc7e5","binary-type":"-events-imagetrust-lint-metrics-mgmt-profile-scrub-search-sync-ui-userprefs","go version":"go1.27.1","caller":"zotregistry.dev/zot/v2/pkg/cli/client/root.go:24","func":"zotregistry.dev/zot/v2/pkg/cli/client.NewCliRootCmd.func1","goroutine":1}
第一个坑实测在下载产物上:发布页下载的 zli-linux-amd64 无执行位,直接运行报权限错误:
$ /opt/data/quzuo/tmp/zot/bin/zli-linux-amd64 config add local http://localhost:8080
/bin/bash: line 1: /opt/data/quzuo/tmp/zot/bin/zli-linux-amd64: Permission denied
exit=126
chmod +x 并按官方惯例重命名为 zli 后即可(官方 zli 指南同样建议改名)。
💡 技术拓展:五个客户端各司其职,别用错工具——zli 只服务 zot,查仓库/镜像/CVE/签名状态(读侧);skopeo 做镜像「搬运」(copy/sync/delete,不依赖任何镜像引擎);oras 推任意 OCI 制品(非镜像文件、SBOM、签名包);cosign 生成并推送签名制品;curl 直连 OCI Distribution API 与 zot 扩展 API,排障与自动化兜底。zot 兼容所有 OCI 客户端,Podman/Helm/Argo 等同样可用,本文以实测过的五件为准。
二、zli:配置管理与服务器状态
zli 用「命名配置」管理多台 zot 服务器,一次 config add 后续命令只带别名。子命令全景实测:
$ ./zli --help
`zli`
Usage:
zli [flags]
zli [command]
Available Commands:
completion Generate the autocompletion script for specified shell
config Configure zot registry parameters for CLI
cve Lookup CVEs in images hosted on the zot registry
help Help about any command
image List images hosted on the zot registry
repo List all repositories
search Search images and their tags
status Information about the server configuration and build information
Flags:
-h, --help help for zli
-v, --version show the version and exit
Use "zli [command] --help" for more information about a command.
2.1 config:添加、列表、默认项
新增一台(实测,成功时无输出仅退出码 0):
$ ./zli config add zot8150 http://localhost:8150
exit=0
重名添加会明确报错(本机 local 在早前实验已添加,重复执行实测):
$ ./zli config add local http://localhost:8080
Error: cli config name already added
exit=1
列表与设默认(config -l 旧写法会打 deprecated 警告,新写法 config list):
$ ./zli config -l
Warning: deprecated invocation `zli config --list`; use `zli config list` instead.
local http://localhost:8080
exit=0
$ ./zli config list
local (default) http://localhost:8080
zot8150 http://localhost:8150
exit=0
$ ./zli config set-default local
exit=0
设了默认后,命令可省略 --config;显式传 --config/--url 时以显式为准(官方口径)。每个配置还有 verify-tls(默认 true)、showspinner 等变量可调(zli config <name> <variable> <value>)。
2.2 status:服务器体检一行看全
$ ./zli status
Server Status: online
Server Version: v2.1.22
Dist Spec Version: 1.1.1
Built with: events, imagetrust, lint, metrics, mgmt, profile, scrub, search, sync, ui, userprefs
exit=0
Built with 直接列出该实例编译进的扩展——与配置篇 zot --version 的 binary-type 字段互为印证:扩展没编译进去,配置里写了也不生效。
三、zli:仓库与镜像盘点
3.1 repo list 与 image list
$ ./zli repo list --config local -u admin:q1w2e3r4
REPOSITORY NAME
alpine
busybox
hello/artifact
hello-world
library/redis
exit=0
$ ./zli image list --config local -u admin:q1w2e3r4
REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE
alpine 3.19 linux/amd64 b58899f0 false 3.4MB
alpine 3.20 linux/amd64 c64c687c false 3.6MB
busybox 1.37 linux/amd64 66a6306d true 2.2MB
hello-world latest linux/amd64 d1a8d0a4 false 4.0kB
hello/artifact v1 35687f49 false 582B
library/redis 7-alpine * 8da19c73 false 16MB
linux/amd64 1cf73a2f false 16MB
exit=0
读表要点:SIGNED 列即服务端签名校验结果(第九章展开);library/redis 7-alpine 带 * 的是多架构清单,缩进行列出各平台 digest;hello/artifact 是 oras 推的非镜像制品(582B,第六章)。
3.2 search 与 image name
search query 模糊匹配仓库/镜像,image name 精确到 tag:
$ ./zli search query busybox --config local -u admin:q1w2e3r4
NAME SIZE LAST UPDATED DOWNLOADS STARS
busybox 2.2MB 2026-11-01 07:51:54.11879985 +0000 UTC 4 0
exit=0
$ ./zli image name busybox:1.37 --config local -u admin:q1w2e3r4
REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE
busybox 1.37 linux/amd64 66a6306d true 2.2MB
exit=0
DOWNLOADS 计数随真实拉取增长(本实验拉过 4 次),STARS 对应 UI 收藏(第八章 userprefs)。列表类命令支持 --sort-by(如 [update-time, alpha-asc, alpha-dsc],以各命令 --help 为准)。
3.3 search subject:referrers 查询的实测边界
search subject 按 tag 或 digest 查「谁引用了这个镜像」(签名、SBOM 等 referrers)。实测两种写法都只回表头:
$ ./zli search subject "busybox:1.37" --config local -u admin:q1w2e3r4
ARTIFACT TYPE SIZE DIGEST
exit=0
$ ./zli search subject "busybox@sha256:66a6306db78bf2dbf3487f293aa8d6990d8e506fdffab9cc43fe422becf886e4" --config local -u admin:q1w2e3r4
ARTIFACT TYPE SIZE DIGEST
exit=0
而此时 busybox:1.37 的 cosign 签名确实存在(image list 的 SIGNED 为 true,第九章)。结论口径:本实验环境下 subject 查询未返回签名条目,验证 referrers 数据建议直接走 OCI 原生端点 /v2/<name>/referrers/<digest>(v2.1.22 路径口径),签名本身用 cosign verify 判定更可靠。
四、zli cve:漏洞查询的三种状态
CVE 查询依赖服务端 search+cve 扩展(配置见 《Zot 2.1.22 配置参数详解》 第七章)。同一组命令在数据库生命周期的不同阶段有三种实测形态。
形态一:cve-db 未就绪——先 20 次重试再失败(输出节选,实测首尾):
[warning] CVE DB is not ready [1] - retry in 3 seconds
[warning] CVE DB is not ready [20] - retry in 3 seconds
Error: cve-db is not present
exit=1
形态二:扫描完成——zli cve list busybox:1.37 --config local -u admin:q1w2e3r4 输出汇总加明细表:
CRITICAL 0, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 4
ID SEVERITY TITLE VULNERABLE PACKAGE PATH INSTALL-VER FIXED-VER
CVE-2026-40200 HIGH musl: musl libc: Arbitrary code execution and...
CVE-2024-58251 MEDIUM In netstat in BusyBox through 1.37.0, local u...
CVE-2026-6042 MEDIUM musl libc: GB18030 4-byte Decoder: musl libc:...
CVE-2025-46394 LOW In tar in BusyBox through 1.37.0, a TAR archi...
exit=0
(表格节选,实测每个 CVE 行下方还有受影响包名、安装版本与修复版本等续行。)
形态三:数据库刚重下、镜像未重扫——同一命令返回:
No CVEs found for image
exit=0
反向查询用 cve affected,实测 CVE-2024-58251 命中 alpine:
$ ./zli cve affected CVE-2024-58251 --config local -u admin:q1w2e3r4
REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE
alpine 3.19 linux/amd64 b58899f0 false 3.4MB
exit=0
官方还提供 cve fixed <repo> <CVE-ID>(列已修复的 tag)、cve diff <imgA> <imgB>(两镜像 CVE 差集,多架构可带平台参数)、--cve-id 过滤与 -f json 结构化输出,均走同一 GraphQL 后端。
五、skopeo:镜像入库与批量同步
5.1 copy:单镜像入库
skopeo 不需要任何镜像引擎,docker:// 传输层直连 registry。实测把 busybox 从上游镜像源入库并做仓库间复制:
$ skopeo copy --src-creds admin:q1w2e3r4 --src-tls-verify=false --dest-creds admin:q1w2e3r4 --dest-tls-verify=false docker://localhost:8080/busybox:1.37 docker://localhost:8080/busybox-copy:1.37
Getting image source signatures
Copying blob sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f
Copying config sha256:30ecbe1509090b38ef3260f3609d64d8261230464eaa4d235901db496e9647fb
Writing manifest to image destination
exit=0
目标换成 docker://localhost:8080/<repo>:<tag> 就是「推入库」,源换成仓库地址就是「拉出库」;同一实例内复制还会触发存储层硬链接去重(实测验证见配置篇 2.3)。
5.2 sync:批量镜像(幂等重放)
skopeo sync --src docker --dest docker <上游仓库> <目标registry> 按 tag 批量复制。实测对四个仓库重复执行(镜像已存在时幂等跳过),以 alpine 与 hello-world 两次为例(实测输出):
time="2026-11-01T15:55:01+08:00" level=info msg="Tag presence check" imagename="docker.m.daocloud.io/library/alpine:3.20" tagged=true
time="2026-11-01T15:55:01+08:00" level=info msg="Copying image ref 1/1" from="docker://docker.m.daocloud.io/library/alpine:3.20" to="docker://localhost:8080/alpine:3.20"
Getting image source signatures
Skipping: image already present at destination
time="2026-11-01T15:55:02+08:00" level=info msg="Synced 1 images from 1 sources"
exit=0
time="2026-11-01T15:57:02+08:00" level=info msg="Tag presence check" imagename="docker.m.daocloud.io/library/hello-world:latest" tagged=true
time="2026-11-01T15:57:02+08:00" level=info msg="Copying image ref 1/1" from="docker://docker.m.daocloud.io/library/hello-world:latest" to="docker://localhost:8080/hello-world:latest"
Getting image source signatures
Skipping: image already present at destination
time="2026-11-01T15:57:14+08:00" level=info msg="Synced 1 images from 1 sources"
exit=0
(实测输出;命令按 skopeo 官方语法执行。)Skipping: image already present at destination 说明 sync 可安全重放,适合做例行镜像搬运脚本。注意它与 zot 服务端 sync 扩展的分工:skopeo sync 是客户端主动搬运,zot sync 是服务端周期/按需拉取(配置篇 7.3)。
5.3 inspect:拉取侧验证入库结果
$ skopeo inspect --tls-verify=false --creds admin:q1w2e3r4 docker://localhost:8080/busybox:1.37
{
"Name": "localhost:8080/busybox",
"Digest": "sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec",
"RepoTags": [
"1.37"
],
"Created": "2024-09-26T21:31:42Z",
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f"
],
"LayersData": [
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
"Digest": "sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f",
"Size": 2211514,
"Annotations": null
}
],
(JSON 节选,实测输出继续到 Env 字段。)顶层 Digest 是 manifest 清单的传输摘要,Layers 里的层 digest 与磁盘 blob 文件名一一对应(配置篇 2.3 的 stat 实测即同一串)。
六、oras:非镜像 OCI 制品推拉
oras 把任意文件打包成 OCI artifact 入库。先登录(实测 Login Succeeded),再推送笔记文件为自定义制品类型(命令按 oras 官方语法,输出实测):
$ oras push localhost:8080/hello/artifact:v1 --artifact-type application/vnd.example.note payload/notes.txt
Login Succeeded
Preparing payload/notes.txt
Uploading 4239b23cd5ca payload/notes.txt
Uploading 44136fa355b3 application/vnd.oci.empty.v1+json
Uploaded 4239b23cd5ca payload/notes.txt
Uploaded 44136fa355b3 application/vnd.oci.empty.v1+json
Uploading 35687f49c6b5 application/vnd.oci.image.manifest.v1+json
Uploaded 35687f49c6b5 application/vnd.oci.image.manifest.v1+json
Pushed [registry] localhost:8080/hello/artifact:v1
ArtifactType: application/vnd.example.note
Digest: sha256:35687f49c6b5088b77be645495522ba6e62194b5ff5aa9a2f5234b016d341ba8
(首行 Login Succeeded 为前置 oras login localhost:8080 的实测回显。)取回并核对:
$ oras pull localhost:8080/hello/artifact:v1
Downloading 35687f49c6b5 application/vnd.oci.image.manifest.v1+json
Processing 35687f49c6b5 application/vnd.oci.image.manifest.v1+json
Downloading 4239b23cd5ca payload/notes.txt
Downloaded 4239b23cd5ca payload/notes.txt
Downloaded 35687f49c6b5 application/vnd.oci.image.manifest.v1+json
Pulled [registry] localhost:8080/hello/artifact:v1
Digest: sha256:35687f49c6b5088b77be645495522ba6e62194b5ff5aa9a2f5234b016d341ba8
exit=0
推送与拉取的 Digest 一致,文件原样落回磁盘。这个制品随即出现在盘点视图里(第三章 image list 的 hello/artifact v1 582B 行)——zot 对「镜像」与「任意 OCI 制品」一视同仁地存储、搜索、扫描。
七、原生 OCI Distribution API:curl 全链路
不装任何工具时,zot 就是一个标准 OCI 注册表,curl 可完成全部生命周期操作(以下命令与回显均实测)。
7.1 认证与仓库清单
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/ -w "%{http_code}"
HTTP 200
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/_catalog -w " <- catalog\n"
{"repositories":["alpine","busybox","busybox-clone","busybox-copy","hello/artifact","hello-world","library/redis"]} <- catalog
匿名 ping 则回 401 质询(配置篇 3.1 实测),带错误口令会多等 failDelay 秒(配置篇 3.4)。
7.2 tag 列表与 manifest 获取
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/busybox/tags/list
{"name":"busybox","tags":["1.36","1.37"]}
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/busybox/manifests/1.37 -H "Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json"
HTTP/1.1 200 OK
Content-Type: application/vnd.oci.image.manifest.v1+json
Docker-Content-Digest: sha256:66a6306db78bf2dbf3487f293aa8d6990d8e506fdffab9cc43fe422becf886e4
--- manifest body ---
{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":"sha256:30ecbe1509090b38ef3260f3609d64d8261230464eaa4d235901db496e9647fb","layers":["sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f"]}
(实测节选:命令原文带 -w 头提取管道,此处保留头与清单体两段。)Docker-Content-Digest 是删除操作需要的清单 digest。
7.3 blob 下载与按 digest 删除
$ curl -s -I -u admin:q1w2e3r4 http://localhost:8080/v2/busybox/blobs/sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f
HTTP/1.1 200 OK
Content-Length: 2211514
Docker-Content-Digest: sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f
删除 tag 的正确姿势是按 digest 删 manifest(删 tag 名不合法),实测完整链:
$ DIGEST=$(curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/busybox/manifests/1.36 -I | grep -i docker-content-digest | awk "{print \$2}" | tr -d "\r")
1.36 digest=sha256:b7f3d86d6e84fc17718c48bcde1450807faa2d56704205c697b4bd5df7b9e29f
$ curl -s -u admin:q1w2e3r4 -X DELETE http://localhost:8080/v2/busybox/manifests/$DIGEST -w "%{http_code}"
HTTP 202
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/busybox/tags/list
{"name":"busybox","tags":["1.37"]}
DELETE 返回 202 表示已受理,tag 列表立即缩短;blob 空间由服务端 GC 按 gcDelay/gcInterval 延迟回收(配置篇 2.4),不会瞬时释放磁盘。
7.4 referrers 端点
签名等引用制品通过 OCI 原生端点 /v2/<name>/referrers/<digest> 枚举,v2.1.22 的路径口径即如此(cosign 签名以 artifactType: application/vnd.dev.sigstore.bundle.v0.3+json 出现在返回的 index 清单里)。zli 的 search subject 是它的封装(3.3 节实测了边界)。
八、扩展 API:GraphQL 搜索与用户偏好
8.1 /v2/_zot/ext/search:GraphQL 后端
zli 与 UI 的查询都打这个端点。写错字段名会得到带修复建议的校验错误(实测回显):
{"errors":[{"message":"Cannot query field \"ImageListForUser\" on type \"Query\". Did you mean \"ImageListForCVE\", \"ImageListForDigest\", or \"ImageList\"?","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}],"data":null}
正确查询 ImageList 时返回精简 JSON(实测,签名入库前查询 busybox 各 tag 状态):
{"data":{"ImageList":{"Results":[{"Tag":"1.37","IsSigned":false}]}}}
(实测响应;请求体按官方 GraphQL 示例构造,审计日志可见每次 POST /v2/_zot/ext/search 均记 200。)
8.2 userprefs:星标与收藏
userprefs 随 search+ui 自动启用(配置篇 7.7),PUT 即可、无响应体:
$ curl -s -X PUT -u admin:q1w2e3r4 "http://localhost:8080/v2/_zot/ext/userprefs?action=toggleStar&repo=busybox" -w "HTTP %{http_code}\n"
HTTP 200
服务端审计日志同步留下追责记录(实测原文):
{"time":"2026-11-01T09:01:17.083395157Z","level":"info","message":"HTTP API Audit","component":"session","clientIP":"[::1]:55564","subject":"admin","action":"PUT","object":"/v2/_zot/ext/userprefs?action=toggleStar&repo=busybox","status":200,"caller":"zotregistry.dev/zot/v2/pkg/api/session.go:165","func":"zotregistry.dev/zot/v2/pkg/api.SessionAuditLogger.func1.1"}
8.3 healthcheck:脚本化就绪判定
$ zot healthcheck --url http://localhost:8080/readyz
exit=0
成功无输出、仅退出码(配置篇 1.4),CI 里 zot healthcheck || exit 1 即可卡点。指标端点 /metrics 的读取权限与内容见配置篇 4.3/7.6。
九、cosign:密钥签名与验签全链
9.1 生成密钥对
cosign 密钥对一条命令生成(命令按 cosign 官方语法,输出实测):
Private key written to ./zot-demo.key
Public key written to ./zot-demo.pub
私钥留在签名方,公钥要交给 zot 服务端做校验——这就是「已签名」与「被信任」的分界。
9.2 上传公钥:admin-only 路由实测
公钥通过扩展 API 上传,路径 /v2/_zot/ext/cosign(官方口径,落盘到 rootDirectory 的 _cosign 目录)。非管理员身份上传实测被拒:
{"errors":[{"code":"DENIED","message":"requested access to the resource is denied","detail":{"description":"The access controller denied access for the operation on a resource."}}]}
http_code=403
换成 admin 重试(实测命令与回显):
$ curl -s -X POST -u admin:q1w2e3r4 --data-binary @cosign-keys/zot-demo.pub http://localhost:8080/v2/_zot/ext/cosign -w "\nHTTP %{http_code}\n"
HTTP 200
$ ls server/data/_cosign/
sha256:31887d940c803503f176f3a20cfbd889b32aa54bbca2fc81ed084e193ac49b2b
(ls 路径为实验数据目录当时形态;公钥以内容 sha256 命名存入 _cosign。)403 与配置篇 4.4 的 admin-only 路由规则完全吻合:信任材料上传要求命中 adminPolicy。
9.3 签名
对已入库的 busybox:1.37 签名(命令按 cosign 官方语法 cosign sign --key <私钥> <镜像引用>,输出实测)。cosign 先警告用 tag 而非 digest 签名有风险(原文):
WARNING: Image reference localhost:8080/busybox:1.37 uses a tag, not a digest, to identify the image to sign.
This can lead you to sign a different image than the intended one. Please use a
digest (example.com/ubuntu@sha256:abc123...) rather than tag
(example.com/ubuntu:latest) for the input to cosign. The ability to refer to
images by tag will be removed in a future release.
随后是透明度日志的存证告知与推送动作(输出节选,实测尾部):
By typing 'y', you attest that (1) you are not submitting the personal data of any other person; and (2) you understand and agree to the statement and the Agreement terms at the URLs listed above.
Signing artifact...
Pushing signature to: localhost:8080/busybox
exit=0
签名本身是一个 OCI 制品(sigstore bundle),作为 referrer 挂在目标镜像 manifest 之下,所以「签名不需要单独的存储后端」——zot 开 trust.cosign 即可托管与校验。
9.4 验签
客户端侧 cosign verify --key <公钥> <镜像引用> 实测:
Verification for localhost:8080/busybox:1.37 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"localhost:8080/busybox:1.37"},"image":{"docker-manifest-digest":"sha256:66a6306db78bf2dbf3487f293aa8d6990d8e506fdffab9cc43fe422becf886e4"},"type":"https://sigstore.dev/cosign/sign/v1"},"optional":{}}]
exit=0
claims 里的 docker-manifest-digest 正是 7.2 节实测的 Docker-Content-Digest,签名与清单严丝合缝。
9.5 服务端视角:SIGNED 列翻转
签名入库后,zot 服务端校验并更新元数据,盘点视图直接可见(同一命令 zli image list --config local -u admin:q1w2e3r4,签名前后实测):
REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE
busybox 1.37 linux/amd64 66a6306d false 2.2MB
hello/artifact v1 35687f49 false 582B
exit=0
签名并上传公钥后复测,busybox 行变为 true(同表形态见 3.1 节)。UI 与 GraphQL 的 IsSigned 同源。
💡 技术拓展:「已签名(signed)」≠「被信任(trusted)」。zot 的 GraphQL
SignatureInfo对每个签名给出Tool(cosign/notation)、IsTrusted与Author——签名存在但服务端没有可验证它的公钥/证书时,IsSigned=true而IsTrusted=false(官方示例响应中同一镜像三个 cosign 签名只有上传过公钥的那个 trusted)。cosign 生态本身又分密钥模式(本文实测:自带密钥对,验签靠上传公钥)与 keyless 模式(sigstore 公共信任根,身份走 OIDC,无需管密钥);notation 则走 x509 证书 + trustpolicy。选型时先定信任模型,再决定 zot 里传什么信任材料。
十、notation 与信任策略(官方口径,未实测)
notation 的证书经 /v2/_zot/ext/notation?truststoreType=ca 上传(同样受 adminPolicy 保护),落盘结构:
_notation
├── trustpolicy.json
└── truststore
└── x509
└── $truststoreType
└── default
└── $certificate
trustpolicy.json 由 zot 在新增 truststore 时自动维护、用户不可改,默认 signatureVerification.level 为 strict(证书过期即不信任,即便签名本身可验通),并预置 ca:default、signingAuthority:default、tsa:default 三个 truststore。配置侧只需 trust.notation: true(配置篇 7.5)。本实验未走 notation 流程,细节以官方 Verifying image signatures 文档为准。
十一、FAQ
11.1 下载的 zli 报 Permission denied(exit=126)?
发布页二进制无执行位,chmod +x zli-linux-amd64 后重命名为 zli 即可(一章实测)。同理适用于 zli 之外的 cosign/oras 发布物。
11.2 zli config add 报 cli config name already added?
同名配置已存在(实测 exit=1)。先 zli config list 核对,需要改 URL 就 zli config remove <name> 后重加(官方口径);各子命令也支持 --url 标志直连服务器、绕过配置文件(实测 help:--url string Specify zot server URL if config-name is not mentioned)。
11.3 zli cve list 返回 No CVEs found for image,镜像真干净?
不一定,这是三种状态之一(第四章实测):cve-db 未就绪会先重试 20 次并报 cve-db is not present;库就绪但镜像未重扫时返回 No CVEs found;扫完才给 TOTAL 表。等一轮服务端 finished scanning available images during scheduled cve scan 日志再复测。
11.4 cosign 上传公钥返回 403 DENIED?
信任材料上传是 admin-only 路由,非 adminPolicy 用户一律 403(9.2 实测;规则见配置篇 4.4)。用 admin 身份重试即 200。
11.5 cosign sign 的 tag 警告要理吗?
要。tag 可被重新指向,签名应绑定 digest(cosign sign --key <私钥> <repo>@sha256:<digest>,cosign 原文警告未来将移除按 tag 引用能力,9.3 实测)。
11.6 skopeo sync 重跑会不会重复拉取?
不会。实测重复执行输出 Skipping: image already present at destination 并正常收尾 Synced 1 images from 1 sources(5.2),搬运脚本可放心定时重放。
11.7 zli search subject 查不到刚签好的签名?
本实验实测 tag 与 digest 两种写法均返回空表头(3.3),但签名在 image list 的 SIGNED 列与 cosign verify 都可见。需要程序化枚举引用制品时,直接请求 /v2/<name>/referrers/<digest> 端点(7.4)。
十二、参考链接
- zli 命令行用户指南:https://zotregistry.dev/v2.1.22/user-guides/zli
- 镜像签名校验(cosign/notation 信任材料):https://zotregistry.dev/v2.1.22/articles/verifying-signatures
- Zot 认证与授权:https://zotregistry.dev/v2.1.22/articles/authn-authz
- OCI 仓库镜像与同步:https://zotregistry.dev/v2.1.22/articles/mirroring
- 项目主页与发布(zli/cosign 等二进制在 Assets):https://github.com/project-zot/zot
- 配套篇:《Zot 介绍与选型指南》、《Zot 2.1.22 Docker Compose 部署指南》、《Zot 2.1.22 配置参数详解》