Zot 2.1.22 配置参数详解

Zot v2.1.22 配置文件参数体系详解:verify/schema/healthcheck 子命令、storage 去重与 GC 默认值实测、htpasswd watcher 与 TLS/failDelay、accessControl 五类策略与 metrics 热重载实测、ratelimit 限速与 compat 兼容、extensions 逐项(search/sync/scrub/trust/metrics)、生产模板逐项解说与热重载能力矩阵,全部命令配真实服务器实测回显。

ZotZot2026/11/01

Zot 2.1.22 配置参数详解

目标:以官方 v2.1.22 管理配置(admin-configuration)、认证与授权(authn-authz)、镜像同步(mirroring)文档为参数权威,逐段讲透 Zot 配置文件的全部参数:顶层与 storage、http 网络与认证、accessControl 授权、ratelimit 与 compat、log、extensions 扩展、scheduler 与 cluster,最后给出生产级完整模板逐项解说与热重载能力矩阵。本文全部命令与回显均为真实服务器实测(zot v2.1.22 full 构建二进制,配置文件 /etc/zot/config.json,数据目录 /var/lib/registry,监听 8080,示例账号 admin/q1w2e3r4、reader/q1w2e3r4),实测日期 2026-11-01。

⚠️ 名称甄别:本文对象是 OCI 镜像仓库 Zot(project-zot/zot,官网 zotregistry.dev),与文献管理软件 Zotero 无任何关系,仅读音相近。

配套篇:部署见 《Zot 2.1.22 Docker Compose 部署指南》,选型见 《Zot 介绍与选型指南》,客户端操作与签名校验见 《Zot 2.1.22 客户端操作与签名校验指南》。

一、参数体系与查看方式

zot 的全部行为由一个配置文件驱动:JSON 或 YAML 均可,以位置参数交给 zot serve 启动。官方建议每次上线新配置前先做语法校验。要弄清楚「有哪些参数、当前什么值在生效」,有三条权威途径:zot --help 的子命令体系、zot schema 导出的 JSON Schema、以及服务启动/重载时打印的配置快照日志。本节全部实测。

1.1 子命令体系

$ zot --help
`zot`

Usage:
  zot [flags]
  zot [command]

Available Commands:
  completion     Generate the autocompletion script for the specified shell
  healthcheck    `healthcheck` probes a local zot health endpoint and exits 0 on success
  help           Help about any command
  schema         `schema` dumps JSON Schema for zot config
  scrub          `scrub` checks manifest/blob integrity
  serve          `serve` stores and distributes OCI images
  verify         `verify` validates a zot config file
  verify-feature `verify-feature` validates specific zot features

Flags:
  -h, --help      help for zot
  -v, --version   show the version and exit

Use "zot [command] --help" for more information about a command.

与配置直接相关的子命令:

子命令作用是否要求服务停止
serve <config>用指定配置启动仓库服务—
verify <config>校验配置文件合法性否
schema导出配置文件的完整 JSON Schema否
healthcheck --url http://localhost:8080/readyz探测健康端点,成功仅返回 0否(面向运行中的服务)
scrub <config>离线校验 manifest/blob 完整性是(见 7.4 实测)
verify-feature校验特定功能所需配置否

1.2 zot verify:上线前的配置预检

注意用法是位置参数 zot verify <config>,不是 --config 旗标(上面 zot verify --help 实测的 Usage 行即为 zot verify <config> [flags])。

对生产配置文件实测,合法时打印一行 config file is valid:

$ zot verify /etc/zot/config.json
{"time":"2026-11-01T07:58:07.986926299Z","level":"info","message":"config file is valid","config":"/etc/zot/config.json","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:224","func":"zotregistry.dev/zot/v2/pkg/cli/server.newVerifyCmd.func1","goroutine":1}

对损坏的配置(写入 /tmp/bad.json 的 JSON 不完整)实测,连续两条 error 日志加一行 Error,退出码 1:

$ zot verify /tmp/bad.json
{"time":"2026-11-01T07:58:08.075688224Z","level":"error","message":"failed to read configuration","error":"While parsing config: unexpected end of JSON input","path":"/tmp/bad.json","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:1347","func":"zotregistry.dev/zot/v2/pkg/cli/server.LoadConfiguration","goroutine":1}
{"time":"2026-11-01T07:58:08.075739504Z","level":"error","message":"invalid config file","config":"/tmp/bad.json","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:219","func":"zotregistry.dev/zot/v2/pkg/cli/server.newVerifyCmd.func1","goroutine":1}
Error: While parsing config: unexpected end of JSON input
exit=1

1.3 zot schema:参数定义的权威来源

zot schema 导出配置文件的 JSON Schema(draft-07),这是逐字段核对参数名与类型的权威依据。输出节选(实测首部):

$ zot schema | head -25
{
  "$ref": "#/definitions/zotregistry.dev~01zot~01v2~01pkg~01api~01config.Config",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "zotregistry.dev~1zot~01v2~01pkg~01api~01config.AWSSecretsManagerConfig": {
      "additionalProperties": false,
      "properties": {
        "refreshInterval": {

(超长输出节选,实测首行起。)注意 schema 对每个配置对象都声明 additionalProperties: false——未知字段不被接受,写错参数名会被 zot verify 拦下,这是「配置即代码」评审时的好帮手。

1.4 zot healthcheck:给编排系统用的就绪探针

$ zot healthcheck --url http://localhost:8080/readyz
exit=0

成功时没有任何输出,只以退出码 0 表态;可直接用作 systemd 健康检查或部署脚本的卡点。

1.5 配置快照:生效参数的 source of truth

服务启动时,zot 在日志里打印一行完整的 configuration settings 快照(超长单行 JSON,此处仅引行首节选,实测完整行见 /var/lib/registry/zot.log):

{"time":"2026-11-01T09:48:46.684315724Z","level":"info","message":"configuration settings","params":{"distSpecVersion":"1.1.1","GoVersion":"go1.27.1","Commit":"v2.1.22-6bbc7e56db2b7a64eee589324efe5813245e3a28","ReleaseTag":"v2.1.22","BinaryType":"-events-imagetrust-lint-metrics-mgmt-profile-scrub-search-sync-ui-userprefs","Storage":{"RootDirectory":"/tmp/zot-warn-test","MaxRepos":0,"Dedupe":true

热重载成功时同理打印 loaded new configuration settings(超长单行 JSON,仅引行首节选):

{"time":"2026-11-01T08:58:06.821945085Z","level":"info","message":"loaded new configuration settings","reloaded params":{

快照里时长一律以纳秒呈现(如 3600000000000 = 1h),未显式配置的参数会露出默认值——本文 2.4 节的 GC 默认值就是从这样的快照里读出来的。排障时先 grep 这行,比翻配置文件更可靠。

💡 技术拓展:「热重载」与「重启生效」的边界。zot 用 fsnotify 监视配置文件,变更经防抖(日志 reason:"debounced file change")后自动应用可重载集合内的参数并打印新快照;集合之外的参数会打一条 warn 并继续沿用旧值,必须重启服务。实测可重载:accessControl 授权策略(4.3 节);实测不可重载:http.ratelimit(5.1 节,warn 原文 config changes are outside the reloadable set and need a restart to take effect)。实操判据只有一条:改完看日志——没有 warn 且新值出现在快照里,才算生效。

二、顶层与 storage 参数

storage 段管「数据放在哪、怎么去重、怎么回收」。生产模板:

"storage": {
  "rootDirectory": "/var/lib/registry",
  "dedupe": true,
  "gc": true,
  "gcDelay": "1h",
  "gcInterval": "24h"
}

2.1 distSpecVersion:与二进制对齐的规格版本

官方初始配置示例用 "1.0.1",文档示例里也出现过 "1.1.0-dev"。写一个与二进制实际支持版本不一致的值,zot verify 仍判合法,但先打一条 warn(实测):

$ zot verify /tmp/warn-test.json
{"time":"2026-11-01T09:48:46.627213376Z","level":"warn","message":"config dist-spec version differs from version actually used","config version":"1.0.1","supported version":"1.1.1","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:1300","func":"zotregistry.dev/zot/v2/pkg/cli/server.updateDistSpecVersion","goroutine":1}
{"time":"2026-11-01T09:48:46.627324555Z","level":"info","message":"config file is valid","config":"/tmp/warn-test.json","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:224","func":"zotregistry.dev/zot/v2/pkg/cli/server.newVerifyCmd.func1","goroutine":1}
exit=0

zot serve 同一配置时同样先 warn,随后启动快照里 distSpecVersion 已被强制改写为二进制支持的实际版本 "1.1.1"(见 1.5 节快照行首节选)。结论:该字段是声明性的,不一致无害、不阻塞启动,但建议与 zot --version 回显的 distribution-spec 字段对齐,避免版本审计时口径混乱。

2.2 rootDirectory:本地与云后端

rootDirectory 指向 OCI image layout 存储根目录。zot 支持一个或多个本地目录(HTTP API 视角合并为单一数据视图);NFS、fuse 等挂载文件系统按本地目录对待。远端后端支持 Amazon S3 及 S3 兼容对象存储(如 MinIO)与 Google GCS,通过 storage.storageDriver 配置(本文实验为本地文件系统,云后端未实测,参数细节见官方 Storage Planning 文档)。

2.3 dedupe:硬链接去重实测

dedupe: true 时,同一 blob 内容在磁盘上只存一份,不同仓库目录以硬链接引用。实测:把 busybox:1.37 复制到同仓库实例的另一个仓库名下:

$ 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

到数据目录验证两个仓库路径下的同名 blob 文件是同一 inode、硬链接数 2:

$ stat -c "%h links, inode %i, %n" /var/lib/registry/busybox/blobs/sha256/68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f /var/lib/registry/busybox-copy/blobs/sha256/68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f
2 links, inode 3937396, /var/lib/registry/busybox/blobs/sha256/68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f
2 links, inode 3937396, /var/lib/registry/busybox-copy/blobs/sha256/68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f

$ du -sh /var/lib/registry/busybox /var/lib/registry/busybox-clone /var/lib/registry/busybox-copy
2.2M	/var/lib/registry/busybox
2.3M	/var/lib/registry/busybox-clone
24K	/var/lib/registry/busybox-copy

busybox-copy 目录仅 24K——blob 全部靠硬链接共享,只占元数据空间。另据官方口径:启动时 zot 按 dedupe 状态对存量 blob 强制执行去重(true 则全量去重;false 则还原,本地硬链接无需还原)。

数据目录整体形态(实测 ls /var/lib/registry,扩展目录与元数据文件一目了然):

_cosign
_notation
_sessions
_trivy
alpine
busybox
busybox-clone
busybox-copy
cache.db
hello
hello-world
library
meta.db
zot-audit.log
zot-tls.log
zot.log

_cosign/_notation 是签名公钥库,_trivy 是 CVE 数据库,meta.db/cache.db 是搜索与元数据 SQLite 库——排障时这些目录是重要线索。

💡 技术拓展:「去重」在镜像生态里有三层含义,zot 的 dedupe 只是第一层——① 存储层去重(zot dedupe):同一 blob 内容在仓库磁盘上只存一份,跨仓库硬链接共享,本文实测对象;② 客户端拉取去重:skopeo/podman 等按 digest 缓存层,同一层只下载一次,与 zot 配置无关;③ 构建层复用:构建器复用历史层缓存,属于构建系统概念。三者都以 digest 为键,但作用位置完全不同,容量规划时只有 ① 由 zot 配置决定。

2.4 gc / gcDelay / gcInterval:内联垃圾回收

删除 manifest 后 blob 不会立即消失(Distribution Spec 的 blob 与 manifest 生命周期非事务),需要 GC 回收。zot 的 GC 是内联的:不需要把仓库下线。三个参数:gc 开关、gcDelay 删除宽限期(给慢客户端上传留时间,按网络与 blob 大小调优)、gcInterval 回收周期。

未显式配置时的默认值从 1.5 节启动快照实测读出:GC:true、GCDelay:1h、GCInterval:1h(快照中为纳秒值 3600000000000)。生产模板把 gcInterval 放宽到 24h,减少高峰期回收开销。

2.5 commit:立即落盘

现代文件系统默认缓冲写、延迟刷盘。commit: true 让 zot 每次写立即提交到磁盘,牺牲吞吐换持久性,官方点名适合 Raspberry Pi 这类 RAM 紧张的内嵌设备。实测快照默认 Commit:false。

2.6 retention:保留策略(简述)

启动快照中可见 Retention 对象含 DryRun、Delay、Policies 字段,用于按规则清理旧镜像;本文实验环境未启用该策略,字段语义以官方 Storage Planning 文档为准。

三、http:网络、TLS 与认证

http 段决定监听地址、传输安全与「谁能进门」。生产模板:

"http": {
  "address": "0.0.0.0",
  "port": "8080",
  "realm": "zot",
  "compat": ["docker2s2"],
  "auth": {
    "htpasswd": { "path": "/etc/zot/htpasswd" },
    "failDelay": 3
  }
}

3.1 address / port / realm

参数说明实测口径
address监听 IP,0.0.0.0 表示全部网卡官方默认示例为 127.0.0.1
port监听端口(字符串)8080
realm认证域,出现在 401 质询头里zot

匿名访问 /v2/ 的质询实测,Www-Authenticate 头里的 realm 值与配置一致:

$ curl -s -D- -o /dev/null http://localhost:8080/v2/ | head -8
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Www-Authenticate: Basic realm="zot"
Date: Sun, 01 Nov 2026 08:59:45 GMT
Content-Length: 253

401 的响应体(超长单行 JSON,仅引行首节选):

{"code":"UNAUTHORIZED","message":"authentication required","detail":{"description":"The access controller was unable to authenticate the client.

3.2 tls:cert / key 与 HSTS

http.tls 只有两个字段——cert(服务端证书)与 key(私钥)。启用 TLS 的独立配置实测(/etc/zot/config-tls.json 片段):

"http": {
  "address": "0.0.0.0",
  "port": "8080",
  "realm": "zot",
  "tls": {
    "cert": "/etc/zot/certs/server.cert",
    "key": "/etc/zot/certs/server.key"
  }
}

HTTPS 生效后实测响应头,注意 zot 在每个 HTTPS 响应上附带 HSTS(官方口径 max-age=63072000; includeSubDomains):

$ curl -sk -D- -o /dev/null https://localhost:8080/v2/ | head -8
HTTP/2 401 
content-type: application/json
strict-transport-security: max-age=63072000; includeSubDomains
www-authenticate: Basic realm="zot"
content-length: 253
date: Sun, 01 Nov 2026 08:58:36 GMT
$ curl -s -k -u admin:q1w2e3r4 https://localhost:8080/v2/_catalog
{"repositories":["alpine","busybox","busybox-clone","busybox-copy","hello/artifact","hello-world","library/redis"]}

不带 -k(自签证书不在客户端信任库)时 curl 直接失败,HTTP 码为 000:

$ curl -s -u admin:q1w2e3r4 https://localhost:8080/v2/ -o /dev/null -w "%{http_code}\n"  # 不带 -k
000

官方警告:includeSubDomains 会把 HSTS 策略扩散到所有子域,用父域对外服务前确认全部子域都已 HTTPS-ready。mTLS 在 tls 段加 cacert 即可开启,客户端证书身份默认取 CN,可用 auth.mtls.identityAttributes 改从 URI SAN 提取(SPIFFE 场景),详见官方 authn-authz 文档。

3.3 auth.htpasswd:账号文件与独立 watcher

官方建档写法(bcrypt 是 zot 唯一支持的散列算法,-B 必须带):

$ htpasswd -bBn <username> <password> >> /etc/zot/htpasswd

配置指向文件路径:"auth": { "htpasswd": { "path": "/etc/zot/htpasswd" } }。

htpasswd 文件有独立于 config.json 的 watcher,账号增删免重启即时生效。实测(向 /etc/zot/htpasswd 追加临时账号 demo3,全程不重启 zot):

$ htpasswd -bnB demo3 q1w2e3r4 >> /etc/zot/htpasswd
$ wc -l /etc/zot/htpasswd
4 /etc/zot/htpasswd

约 2 秒后日志出现两行热加载记录(原文):

{"time":"2026-11-01T09:41:13.485029076Z","level":"info","message":"htpasswd file changed, trying to reload config","htpasswd-file":"/etc/zot/htpasswd","reason":"debounced file change","caller":"zotregistry.dev/zot/v2/pkg/api/htpasswd.go:501","func":"zotregistry.dev/zot/v2/pkg/api.(*HTPasswdWatcher).reloadWatchedFile","goroutine":71}
{"time":"2026-11-01T09:41:13.48514094Z","level":"info","message":"loaded htpasswd file","htpasswd-file":"/etc/zot/htpasswd","users":3,"caller":"zotregistry.dev/zot/v2/pkg/api/htpasswd.go:70","func":"zotregistry.dev/zot/v2/pkg/api.(*HTPasswd).Reload","goroutine":71}

新账号立即可用:

$ curl -s -u demo3:q1w2e3r4 http://localhost:8080/v2/ -o /dev/null -w "demo3 HTTP %{http_code}\n"
demo3 HTTP 200

把账号文件恢复为两行后,demo3 立即失效、admin 不受影响(同样免重启):

$ curl -s -u demo3:q1w2e3r4 http://localhost:8080/v2/ -o /dev/null -w "demo3 HTTP %{http_code}\n"
demo3 HTTP 401
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/ -o /dev/null -w "admin HTTP %{http_code}\n"
admin HTTP 200

Kubernetes 场景官方口径:Secret 卷以原子替换符号链接的方式更新文件,zot 能识别,Secret 挂载的 htpasswd 免重启更新。但把宿主机单个文件直接映射给服务(单文件 bind mount)时 inotify 事件可能不透传,watcher 收不到变更——部署篇踩过的这个坑及解法见 《Zot 2.1.22 Docker Compose 部署指南》 的 FAQ。

3.4 auth.failDelay:认证失败延迟

failDelay 指定认证失败后等待 N 秒再回 401,拖垮在线爆破脚本。配置 "failDelay": 3 后实测:

$ time curl -s -u admin:wrongpass http://localhost:8080/v2/ -o /dev/null -w "%{http_code}"
HTTP 401

实测响应被延迟约 3 秒返回(time 注记:耗时 3 秒,与 failDelay=3 一致)。

3.5 其他认证方式与 Docker 客户端兼容

zot 支持的认证方式全景(官方口径,本文仅实测 htpasswd):htpasswd、LDAP、Bearer(JWT)、OpenID/OAuth2 社交登录(github/google/gitlab/dex)、mTLS、API key。两条值得写进运维手册的规则:

  • htpasswd 与 LDAP 并存时 LDAP 优先,htpasswd 作为 LDAP 不可用时的本地兜底;
  • Docker 客户端 /v2/ 质询兼容:启用 basic 认证且策略为混合模式(存在匿名策略也需鉴权策略)时,zot 对带 Docker-Client User-Agent 的请求在 /v2/ 上直接回 401 + WWW-Authenticate: Basic,触发 Docker 的凭据流程;全部仓库匿名可读时 /v2/ 免凭据回 200。Podman/skopeo 等按资源质询,无需此兼容。认证方式选型对比见 《Zot 介绍与选型指南》。

四、accessControl:授权策略

认证解决「你是谁」,http.accessControl 解决「你对哪些仓库能做什么」。v2.0.0 起仓库策略统一收在 repositories 键下(旧版直接挂在 accessControl 下,迁移时注意)。

4.1 五类策略与求值顺序

策略类型键语义
默认策略defaultPolicy已认证但未命中任何专属策略的用户,允许的动作列表
用户专属policies[].users + actions显式点名用户的动作
组专属policies[].groups + actions组(可在 accessControl.groups 定义,LDAP 组须用 FQDN)
匿名anonymousPolicy未认证用户允许的动作,开放只读镜像站的标准做法
metricsmetrics.users允许读取 /metrics 端点的用户
adminadminPolicy全局管理员,跨所有仓库,压过其他一切策略

单个仓库的求值顺序(官方口径):用户专属 → 组专属 → 默认 → admin。

4.2 repositories、glob 与最长路径匹配

repositories 的键是仓库路径模式,支持单层通配 * 与递归通配 **;** 匹配任意路径,等效全局默认。官方示例的骨架(节选自 admin-configuration 文档):

"accessControl": {
  "groups": {
    "group2": { "users": ["alice", "mallory", "jim"] }
  },
  "metrics": { "users": ["john"] },
  "repositories": {
    "**": {
      "policies": [{ "users": ["charlie"], "groups": ["group2"], "actions": ["read", "create", "update"] }],
      "defaultPolicy": ["read", "create"]
    },
    "tmp/**": {
      "anonymousPolicy": ["read"],
      "defaultPolicy": ["read", "create", "update"]
    },
    "repos2/repo": {
      "defaultPolicy": ["read"]
    }
  },
  "adminPolicy": {
    "users": ["admin"],
    "actions": ["read", "create", "update", "delete"]
  }
}

三条硬规则(官方):

  • 最长路径优先:repos2/repo 同时命中 ** 与自身时,用更长的 repos2/repo 策略;
  • 动作集合只有 read/create/update/delete 四种,且任何策略必须含 read——create/update/delete 不能脱离 read 单独使用;
  • tmp/** 配 anonymousPolicy: ["read"] 即「匿名只读、登录可写」的公开镜像配方。

4.3 metrics 策略实测:403 → 热重载 → 200

metrics.users 只含 admin 时,reader 读 /metrics 被授权层拒绝(403,注意与 401 区分——reader 认证是过的,只是无权):

$ curl -s -u reader:q1w2e3r4 http://localhost:8080/metrics -w "\nHTTP %{http_code}\n"
{"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 403

把 reader 追加进 accessControl.metrics.users 后不重启,约 2 秒后日志出现完整重载链(原文节选):

{"time":"2026-11-01T08:58:06.8107497Z","level":"info","message":"config file changed, trying to reload config","config":"/etc/zot/config.json","reason":"debounced file change","caller":"zotregistry.dev/zot/v2/pkg/cli/server/config_reloader.go:245","func":"zotregistry.dev/zot/v2/pkg/cli/server.(*HotReloader).reloadConfig","goroutine":42}
{"time":"2026-11-01T08:58:06.821533438Z","level":"info","message":"loaded htpasswd file","htpasswd-file":"/etc/zot/htpasswd","users":2,"caller":"zotregistry.dev/zot/v2/pkg/api/htpasswd.go:70","func":"zotregistry.dev/zot/v2/pkg/api.(*HTPasswd).Reload","goroutine":42}

(第三行 loaded new configuration settings 为超长单行 JSON 快照,见 1.5 节行首节选。)复测:

$ curl -s -u reader:q1w2e3r4 http://localhost:8080/metrics -o /dev/null -w "%{http_code}\n"
200

官方补充:metrics.anonymousPolicy 给 read 时未认证也能拉指标;未列入 metrics 名单的认证用户仍然 403。

4.4 adminPolicy 与 admin-only 路由

/v2/_zot/pprof/ 性能剖析端点、cosign/notation 信任公钥上传路由,要求请求身份命中 accessControl.adminPolicy——比仓库策略更严:未认证 401,认证但不在 admin 名单 403。配置了认证却没配 adminPolicy 时,这些路由对所有人关闭。传统 Bearer token 携带的是仓库 scope 而非 zot 用户身份,无法匹配 adminPolicy,管理员须用 htpasswd/LDAP/API key/OpenID 等身份类方式。

4.5 CEL 网络条件:只用 req.client.ip

策略 conditions[].expression 支持 CEL 检查客户端网络信息。官方安全警告:req.client.forwardedFor 取自 X-Forwarded-For 头、任何客户端都可伪造,绝不能单独作为授权依据;直连场景用 req.client.ip.startsWith("10."),反代场景先校验 req.client.ip 是可信代理再查 forwarded 链,且代理必须覆写而非追加 X-Forwarded-For。

五、ratelimit 与 compat

5.1 ratelimit:全局限速与按方法限速(不可热重载)

官方结构:ratelimit.rate 全局每秒请求数,ratelimit.methods[] 可对指定 HTTP 方法覆写:

"ratelimit": {
  "rate": 10,
  "methods": [
    { "method": "GET", "rate": 5 }
  ]
}

实测在 http 段加入 "ratelimit": { "rate": 2 },config.json 触发了热重载,但日志立刻给出 warn(原文):

{"time":"2026-11-01T09:42:11.404895912Z","level":"warn","message":"config changes are outside the reloadable set and need a restart to take effect","fields":["HTTP.Ratelimit"],"caller":"zotregistry.dev/zot/v2/pkg/cli/server/config_reloader.go:356","func":"zotregistry.dev/zot/v2/pkg/cli/server.(*HotReloader).warnNonReloadableChanges","goroutine":24}

未重启前连发 10 次,限流未生效:

$ for i in $(seq 10); do curl -s -o /dev/null -u admin:q1w2e3r4 -w "%{http_code} " http://localhost:8080/v2/_catalog; done
200 200 200 200 200 200 200 200 200 200

重启 zot 服务后复测,rate=2 生效(令牌桶:前 2 个放行,后续 429):

$ for i in $(seq 10); do curl -s -o /dev/null -u admin:q1w2e3r4 -w "%{http_code} " http://localhost:8080/v2/_catalog; done
200 200 429 429 429 429 429 429 429 429

等待约 3 秒令牌回填,请求恢复:

$ curl -s -u admin:q1w2e3r4 http://localhost:8080/v2/_catalog -w "\nHTTP %{http_code}\n"
{"repositories":["alpine","busybox","busybox-clone","busybox-copy","hello/artifact","hello-world","library/redis"]}
HTTP 200

恢复原配置并再次重启后,连发 10 次全部 200(实测与首轮一致)。结论:ratelimit 属于「改了必重启」集合,与 1.5 节热重载判据互相印证。

5.2 compat:接收 Docker schema 2 清单

zot 默认只收 OCI 格式清单,Docker 格式(application/vnd.docker.distribution.manifest.v2+json)push 直接拒。兼容值必须写精确字符串 docker2s2(官方明确 docker 等别名无效):

"http": {
  "compat": ["docker2s2"]
}

v2.1.22 的行为口径(官方):sync 一律保留上游 media type 与 digest,旧的 preserveDigest 设置已废弃并被忽略;未配 compat 时同步 Docker 格式内容会被拒。官方给出的启用判据三选一:客户端直接推 Docker schema2;镜像自上游的 Docker 格式仓库;被同步的 Docker 格式镜像带有绑定其 manifest digest 的 cosign/notation 签名与 referrers。做 Docker 拉取式缓存时,compat 要与 sync 的 onDemand: true 组合使用(官方 config-docker-compat-sync.json 配方)。

六、log 与 audit

"log": {
  "level": "info",
  "output": "/var/lib/registry/zot.log",
  "audit": "/var/lib/registry/zot-audit.log"
}

6.1 level 与 output

level 取 debug/info/warn/error;生产用 info 即可——本文引用的 warn/error 行在 info 级别下全部可见。output 指定日志文件(缺省输出到 stdout)。实测运行数小时后 zot.log 约 1.9 MB(-rw------- 1 root root 1923935 Nov 1 09:45 /var/lib/registry/zot.log),JSON 单行格式天然适配 ELK/Loki 采集。

6.2 audit:HTTP API 审计流水

audit 指定审计文件,记录每一次 API 调用的主体/动作/对象/状态码。实测文件与首尾行(超长单行 JSON,此处各引一行完整行):

$ ls -la /var/lib/registry/zot-audit.log
-rw------- 1 root root   27868 Nov  1 09:01 /var/lib/registry/zot-audit.log
{"time":"2026-11-01T07:51:35.075350674Z","level":"info","message":"HTTP API Audit","component":"session","clientIP":"127.0.0.1:50826","subject":"admin","action":"POST","object":"/v2/hello/artifact/blobs/uploads/","status":202,"caller":"zotregistry.dev/zot/v2/pkg/api/session.go:165","func":"zotregistry.dev/zot/v2/pkg/api.SessionAuditLogger.func1.1"}
{"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"}

(首尾两行,实测节选。)subject 字段直接是登录用户名,追责链路完整。

七、extensions:扩展逐项

扩展只在 full 构建里有效,minimal 构建会忽略整个 extensions 段(与 1.1 节 binary-type 回显对照)。启用语义:出现该属性即启用,显式 "enable": false 才关闭。两个 API-only 扩展不在配置里开关:mgmt 随 search 启用,userprefs 随 search+ui 同时启用(官方)。

7.1 search 与 cve

"search": {
  "enable": true,
  "cve": {
    "updateInterval": "2h",
    "trivy": {
      "dbRepository": "ghcr.m.daocloud.io/aquasecurity/trivy-db:2",
      "javaDBRepository": "ghcr.m.daocloud.io/aquasecurity/trivy-java-db:1"
    }
  }
}
参数默认口径(官方)
enable缺属性即启用置 false 关闭增强搜索
cve.updateInterval2hTrivy 数据库刷新间隔,最小 2h,低于自动抬到 2h;不控制扫描频率
trivy.dbRepositoryghcr.io/aquasecurity/trivy-db可换镜像源(实验环境换为 daocloud 镜像)
trivy.javaDBRepositoryghcr.io/aquasecurity/trivy-java-dbJava 专用库地址

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 实测输出 TOTAL 4 的漏洞表(完整回显见《Zot 2.1.22 客户端操作与签名校验指南》第四章):

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 行下方还有受影响包名、安装版本与修复版本等续行。)后台周期性活动同样落在日志里(原文两行):

{"time":"2026-11-01T09:42:48.467994069Z","level":"info","message":"updating cve-db","caller":"zotregistry.dev/zot/v2/pkg/extensions/search/cve/update.go:101","func":"zotregistry.dev/zot/v2/pkg/extensions/search/cve.(*dbUpdateTask).DoWork","goroutine":53}
{"time":"2026-11-01T09:42:48.820800587Z","level":"info","message":"finished scanning available images during scheduled cve scan"

(第二行为超长单行 JSON 的行首节选。)注意扫描对象是仓库内已有镜像,新推入的镜像要等下一轮扫描;「No CVEs found」与「真有 0 个 CVE」的区分见 FAQ 11.3。

7.2 ui

"ui": { "enable": true }

唯一参数就是 enable;开启后 Web 界面与 search 后端联动(镜像列表、漏洞视图、星标/收藏)。full 官方镜像默认开启。

7.3 sync:周期同步与 onDemand 拉取式缓存

"sync": {
  "enable": true,
  "registries": [
    {
      "urls": ["https://docker.m.daocloud.io"],
      "onDemand": true,
      "maxRetries": 3,
      "retryDelay": "5m"
    }
  ]
}

关键字段(官方口径):urls 上游列表按序容错;onDemand: true 表示本地缺失时实时向上游拉并回给请求方,false 则只服务已同步内容;pollInterval 配置周期轮询(官方警告:Docker Hub 限流且不支持 catalog,只能用 onDemand 模式不要配轮询);maxRetries 失败重试次数,retryDelay 重试间隔(配了 maxRetries 则必填);另有 manifestCheckInterval(已缓存 tag 的上游检查最小间隔)、onDemandInBackground(本地缺失立即 404 并后台补,客户端须能回落其他仓库)与 content 规则(前缀映射、glob 过滤、platforms 稀疏同步)。

onDemand 实测:客户端首次请求 library/redis:7-alpine(本地原本没有)时,服务端日志打出完整四行链路(原文):

{"time":"2026-11-01T08:12:49.2609243Z","level":"info","message":"trying to get updated image by syncing on demand","repository":"library/redis","reference":"7-alpine","caller":"zotregistry.dev/zot/v2/pkg/api/routes.go:2950","func":"zotregistry.dev/zot/v2/pkg/api.getImageManifest","goroutine":40}
{"time":"2026-11-01T08:12:49.261733848Z","level":"info","message":"sync: syncing image","remote":"docker.m.daocloud.io","repo":"library/redis","reference":"7-alpine","caller":"zotregistry.dev/zot/v2/pkg/extensions/sync/service.go:484","func":"zotregistry.dev/zot/v2/pkg/extensions/sync.(*BaseService).SyncImage","goroutine":40}
{"time":"2026-11-01T08:12:49.727416066Z","level":"info","message":"syncing image","remote image":"docker.m.daocloud.io/library/redis@sha256:8da19c7399dca900c6822a1cf30f9d2285585e2c4b457ff4095b0f4500054d3f","local image":"library/redis:7-alpine","caller":"zotregistry.dev/zot/v2/pkg/extensions/sync/service.go:1001","func":"zotregistry.dev/zot/v2/pkg/extensions/sync.(*BaseService).syncRef","goroutine":40}
{"time":"2026-11-01T08:12:49.86631365Z","level":"info","message":"pushing synced local image to local registry","syncTempDir":"/var/lib/registry/library/redis/.sync/14bdb7a1-0a4f-4626-9f84-0a2e52cfb2f5/library/redis","repository":"library/redis","caller":"zotregistry.dev/zot/v2/pkg/extensions/sync/destination.go:132","func":"zotregistry.dev/zot/v2/pkg/extensions/sync.(*DestinationRegistry).CommitAll","goroutine":40}

四行覆盖「命中缺失→定位上游→按远端 digest 同步→提交入库」全流程,约 0.6 秒完成。客户端侧 copy 实测输出节选(命令形态同 2.3 节 skopeo copy,源为本仓库 library/redis:7-alpine):

Getting image source signatures
Copying blob sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
Copying config sha256:98c6ca8f397f86e469be752c3e7ed5eb8b17f2479840cb69a2e413037038e9f4
Writing manifest to image destination
exit=0

7.4 scrub:周期完整性巡检

"scrub": { "enable": true, "interval": "24h" }

interval 最小 2h(官方表口径)。scrub 遍历文件系统重算 blob 哈希,捕捉 bit rot,运行期间轻微拉低性能。除周期任务外还有离线命令 zot scrub <config>,实测在服务运行中直接拒绝:

$ zot scrub /etc/zot/config.json
{"time":"2026-11-01T09:45:55.703496847Z","level":"warn","message":"server is running, in order to perform the scrub command the server should be shut down","error":"server is running","caller":"zotregistry.dev/zot/v2/pkg/cli/server/root.go:176","func":"zotregistry.dev/zot/v2/pkg/cli/server.newScrubCmd.func1","goroutine":1}
Error: server is running
exit=1

即:zot scrub 是停机维护命令,在线校验交给周期任务。

7.5 trust:签名校验开关

"trust": { "enable": true, "cosign": true, "notation": true }

trust 只负责服务端校验能力:开启后 zot 用用户上传的公钥(cosign)/证书(notation)验证签名,公钥落 _cosign/_notation 目录(见 2.3 节目录实测)。上传公钥的路由受 adminPolicy 保护(4.4 节)。签名与验签的端到端操作见《Zot 2.1.22 客户端操作与签名校验指南》第九章展开。

7.6 metrics:Prometheus 端点

"metrics": { "enable": true, "prometheus": { "path": "/metrics" } }

admin 实测可读,全量 290 行 Prometheus 文本:

$ curl -s -u admin:q1w2e3r4 http://localhost:8080/metrics | wc -l
290
$ curl -s -u admin:q1w2e3r4 http://localhost:8080/metrics | head -12
# HELP go_gc_duration_seconds A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 6.2115e-05
go_gc_duration_seconds{quantile="0.25"} 7.101e-05
go_gc_duration_seconds{quantile="0.5"} 8.6294e-05
go_gc_duration_seconds{quantile="0.75"} 0.000154669
go_gc_duration_seconds{quantile="1"} 0.001577185
go_gc_duration_seconds_sum 0.005166647
go_gc_duration_seconds_count 26
# HELP go_gc_gogc_percent Heap size target percentage configured by the user, otherwise 100. This value is set by the GOGC environment variable, and the runtime/debug.SetGCPercent function. Sourced from /gc/gogc:percent.
# TYPE go_gc_gogc_percent gauge
go_gc_gogc_percent 100

zot 自有指标在尾部(实测 tail 4 行,存储锁延迟直方图):

$ curl -s -u admin:q1w2e3r4 http://localhost:8080/metrics | tail -4
zot_storage_lock_latency_seconds_bucket{lockType="RWLock",storageName="/var/lib/registry",le="60"} 38
zot_storage_lock_latency_seconds_bucket{lockType="RWLock",storageName="/var/lib/registry",le="+Inf"} 38
zot_storage_lock_latency_seconds_sum{lockType="RWLock",storageName="/var/lib/registry"} 0.19786515699999996
zot_storage_lock_latency_seconds_count{lockType="RWLock",storageName="/var/lib/registry"} 38

读取权限由 4.3 节 accessControl.metrics 单独控制;minimal 部署可用官方 zxp exporter 补指标。

7.7 lint、userprefs、mgmt

  • lint:"lint": { "enable": true, "mandatoryAnnotations": ["annot1"] },push 时校验 manifest 必须携带的 annotation,缺失即拒收。实验环境未开启。
  • userprefs:无配置字段,search+ui 同时开启即自动可用;提供星标/收藏两个 API。实测给 busybox 加星:
$ 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
  • mgmt:search 开启即启用,为 UI/zli 提供管理 API。

八、scheduler 与 cluster

8.1 scheduler.numWorkers

GC、sync、cve 扫描等后台任务统一进调度器队列。官方口径:并发 worker 上限默认为 4 × CPU 核数,一般无需调整;需要时:

"scheduler": { "numWorkers": 3 }

8.2 cluster

高可用方案:多个无状态 zot 副本 + 前置负载均衡 + 共享远端后端存储(S3/GCS),个别副本宕机不影响服务,多副本还能叠加网络吞吐。实验环境为单机部署,cluster 未实测,参数与拓扑以官方 zot Clustering 文档为准。注意它与 storage 的多目录是不同概念:cluster 解决可用性,多 rootDirectory 解决单机存储扩展。

九、生产模板逐项解说

本文实验环境的完整 /etc/zot/config.json(与 1.2 节 zot verify 通过的是同一文件):

{
  "distSpecVersion": "1.1.0",
  "storage": {
    "rootDirectory": "/var/lib/registry",
    "dedupe": true,
    "gc": true,
    "gcDelay": "1h",
    "gcInterval": "24h"
  },
  "http": {
    "address": "0.0.0.0",
    "port": "8080",
    "realm": "zot",
    "compat": [
      "docker2s2"
    ],
    "auth": {
      "htpasswd": {
        "path": "/etc/zot/htpasswd"
      },
      "failDelay": 3
    },
    "accessControl": {
      "metrics": {
        "users": [
          "admin"
        ]
      },
      "repositories": {
        "**": {
          "defaultPolicy": [
            "read",
            "create"
          ]
        }
      },
      "adminPolicy": {
        "users": [
          "admin"
        ],
        "actions": [
          "read",
          "create",
          "update",
          "delete"
        ]
      }
    }
  },
  "log": {
    "level": "info",
    "output": "/var/lib/registry/zot.log",
    "audit": "/var/lib/registry/zot-audit.log"
  },
  "extensions": {
    "search": {
      "enable": true,
      "cve": {
        "updateInterval": "2h",
        "trivy": {
          "dbRepository": "ghcr.m.daocloud.io/aquasecurity/trivy-db:2",
          "javaDBRepository": "ghcr.m.daocloud.io/aquasecurity/trivy-java-db:1"
        }
      }
    },
    "ui": {
      "enable": true
    },
    "sync": {
      "enable": true,
      "registries": [
        {
          "urls": [
            "https://docker.m.daocloud.io"
          ],
          "onDemand": true,
          "maxRetries": 3,
          "retryDelay": "5m"
        }
      ]
    },
    "scrub": {
      "enable": true,
      "interval": "24h"
    },
    "trust": {
      "enable": true,
      "cosign": true
    },
    "metrics": {
      "enable": true,
      "prometheus": {
        "path": "/metrics"
      }
    }
  }
}

逐段设计要点:

段取值为什么
storagededupe+gc,gcInterval 24h省盘;回收错峰(默认 1h 太密,见 2.4)
http.authhtpasswd + failDelay 3本地账号兜底;爆破每请求多等 3 秒(3.4 实测)
http.compatdocker2s2兼容 Docker 格式 push 与镜像同步(5.2)
accessControl** 默认 read+create;admin 全权;metrics 仅 admin登录即可推拉自有镜像,删除与指标收紧到管理员(4.2/4.3)
loginfo + 双文件运行日志与审计分离,audit 留追责证据(6.2)
extensions.searchcve 2h + trivy 库走国内镜像ghcr 直连慢的环境把 dbRepository 换源(7.1)
extensions.syncdaocloud + onDemandDocker Hub 官方禁止轮询镜像,只做拉取式缓存(7.3)
extensions.scrub24hbit-rot 日常巡检,在线跑(7.4)
extensions.trustcosign为签名校验预留服务端能力(7.5)

对外服务前把 tls 段补上(3.2 的 cert/key 两行),其余结构不变。

十、热重载能力矩阵小结

把全文实测与官方口径汇总成一张「改了参数要不要重启」速查表:

变更项免重启生效?证据类型
htpasswd 账号增删是(独立 watcher)实测:users:2→3→2,demo3 即增即用即删即失效(3.3)
accessControl(含 metrics.users)是实测:reader 403→改配置→200(4.3)
http.ratelimit否实测:warn 点名 HTTP.Ratelimit,重启后 429 才出现(5.1)
监听地址/端口、tls、storage 路径不在已验证的可重载集合内官方:以 warn 日志为准,无 warn 且快照出现新值才算生效
extensions 各参数视具体参数官方:非 reloadable 集合会 warn 并沿用旧值

标准变更流程:zot verify 预检 → 落盘修改 → 等 2 秒 grep 日志三条链(config file changed / 有无 warn / loaded new configuration settings 快照含新值)→ 行为级复测(curl 或重启后复测)。Kubernetes 下 ConfigMap/Secret 卷的符号链接原子替换 zot 都能识别(官方口径),无需额外适配。

十一、FAQ

11.1 htpasswd 加了账号,服务不认?

先分清挂载形态:目录级挂载(裸机 /etc/zot 即天然如此、K8s Secret 卷)下 zot 的独立 watcher 秒级热加载,实测免重启(3.3);但把宿主机单个文件直接映射给服务时,inotify 事件可能不透传,watcher 收不到变更,只能重启加载——部署篇实验踩过并记录在 《Zot 2.1.22 Docker Compose 部署指南》 FAQ。判据:改完文件 grep 日志有没有 htpasswd file changed 那行。

11.2 改了 ratelimit 没反应?

看 warn:config changes are outside the reloadable set and need a restart to take effect,fields 点名 HTTP.Ratelimit(5.1 实测原文)。ratelimit 必须重启服务;重启前旧值继续生效(实测连发 10 次全 200,重启后 429 才出现)。

11.3 zli cve list 报「No CVEs found for image」,是镜像真没漏洞?

不一定。实测该回显出现在 cve 数据库重新下载完成、镜像尚未重扫的窗口期(同一命令此前扫出 4 个 CVE,见 7.1)。区分三种状态:cve-db is not present=库没就绪;No CVEs found=库就绪但本轮扫描未覆盖;TOTAL 表=扫描完成。等一轮 finished scanning available images during scheduled cve scan 日志再查。

11.4 启动日志 warn「config dist-spec version differs from version actually used」要紧吗?

无害。verify 照常通过、serve 照常启动,zot 自动改用二进制支持的实际版本(2.1 实测:写 1.0.1,快照生效 1.1.1)。强迫症可把配置值与 zot --version 的 distribution-spec 字段对齐。

11.5 配好 TLS 后 curl 返回 000?

000 不是 HTTP 状态码,是 TLS 校验失败:自签证书不在客户端信任库(3.2 实测:带 -k 正常 401/200,不带即 000)。测试用 -k,生产把签发 CA 导入系统信任库;同时注意 zot 强制下发的 HSTS includeSubDomains 对全部子域的影响。

11.6 zot scrub 报「server is running」?

预期行为(7.4 实测):scrub 命令是离线完整性校验工具,要求服务停机;在线校验交给 extensions.scrub 周期任务(interval 最小 2h)。

十二、参考链接