containerd接入Harbor私有仓库:hosts.toml配置与K8s镜像拉取实战解析
发布时间:2026/10/5 18:33:20 作者:尧图编辑部 阅读量:1,286

先给你还原一个常见的现场集群里 kubelet 一直报ImagePullBackOff你登录节点想用crictl pull手动拉一下镜像验证结果抛出一串x509: certificate signed by unknown authority或者更诡异一点nerdctl pull能通但 kubelet 就是拉不下来。装好 containerd、也装了 Harbor为什么私有仓库就是不通大多数人第一反应是“证书问题”或“账号问题”但真正的原因往往是你没有把 containerd 生态里那几个工具的配置搞清楚。这篇文章就围绕 containerd 和 Harbor 的集成这件事从底层配置逻辑讲到端到端验证最后把我在生产环境踩过的坑和长期维护经验一并梳理出来。不管是 Kubernetes 节点上要拉 Harbor 的镜像还是你只是想用 containerd 原生环境推送私有镜像这套配置路径都适用。1. 为什么 containerd 不像 Docker 那样天然认得 Harbor 仓库很多从 Docker 切过来的人会有一个惯性思维只要装了 containerd在/etc/docker/daemon.json里写上insecure-registries就能访问私有仓库。先别急containerd 和 Docker daemon 根本不是一回事。containerd 只是个底层容器运行时它自己并不像 dockerd 那样内置一套完整的镜像仓库客户端逻辑。镜像从哪里拉、怎么认证这些决定权在上面那层“工具”手里。1.1 一张表看懂 ctr、nerdctl、crictl 的配置来源平时你面对 containerd至少有三种操作镜像的方式它们的配置来源完全不一样工具谁在用读取配置的位置认证方式ctr直接调试 containerd 的 CLI默认不读 CRI registry 配置需要--hosts-dir指定 hosts 目录hosts.toml 中的identitytoken/headernerdctl类 Docker 体验的 containerd 客户端会读取/etc/containerd/certs.d/下的 hosts.toml也会读取/root/.docker/config.jsonnerdctl login写入的 docker configcrictlCRI 调试工具kubelet 也走同一套 CRI 插件逻辑读取 containerd 配置文件中[plugins.io.containerd.grpc.v1.cri.registry]指定的config_pathconfig.toml 中的registry.configs或 crictl 命令的--creds也就是说你在一个终端里用nerdctl login harbor.example.com登录成功了这只影响 nerdctl 自己以及它调用的 containerd 客户端逻辑kubelet 不会因为你登录过 nerdctl 就自动拿到 Harbor 的凭据。这是整个集成过程中最容易被忽略的一条分界线。1.2 一个常见的失败现场证书与授权的双重报错我见过一个很典型的报错组合。先是证书crictl pull harbor.example.com/library/nginx:1.25 FATA[0000] failed to pull and unpack image: failed to resolve reference harbor.example.com/library/nginx:1.25: failed to do request: x509: certificate signed by unknown authority你想着“那就跳过证书验证”转头把config_path换成insecure_registries结果报错变成了unauthorized: authentication required这两个报错串起来正好说明了 containerd 接入 Harbor 需要同时解决三件事地址解析harbor.example.com能解析到 Harbor 服务的 IPTLS 信任containerd 要信任 Harbor 用的 CA 证书认证凭据Harbor 是私库必须给 containerd 或 kubelet 提供能读项目的账号信息。但这三件事并非都写在同一个文件里。接下来我们先把 Harbor 侧准备到位再回到 containerd 侧做配置。2. Harbor 侧准备域名、项目、机器人账号与升级注意事项很多人部署 Harbor 后都喜欢用 IP 地址访问比如https://192.168.1.10:8443。这种方式做测试无所谓但要在 containerd 里长期集成我强烈建议你给它一个正式域名比如registry.internal.example.com。理由有两个证书的 SAN 必须和仓库地址匹配用域名方便签发和轮换正规证书containerd 的 hosts.toml 是按 registry host 名匹配配置的用 IP 加端口会让配置路径变得很绕虽然也能写但排查问题时很容易眼花。2.1 先确认 Harbor 自身的服务正常在配置 containerd 之前先在任意一台有 Harbor 访问权限的机器上验证curl https://registry.example.com/v2/如果返回401 Unauthorized或{}说明服务正常因为/v2/本身就要求认证。如果返回503或超时先别急着配 containerd去查 Harbor 的容器状态和数据库。如果 Harbor 用的是自签名证书curl 时需要加-k但只用于验证不要让 containerd 也跟着-k。2.2 创建项目和最小化权限的机器人账号Harbor 里的权限边界很清晰项目、用户、机器人账号。给 containerd 集成用的凭据我建议一律用机器人账号不要直接用管理员账号更不要拿管理员密码到处写。创建机器人账号时有几个关键点选择对应的项目比如library勾选权限类型pull-only 还是 push/pull记住 UI 上展示的完整用户名通常长这样robot$librarypullbot注意中间有$和后面跟的就是 token 密码机器人账号 token 只会完整显示一次丢失后要重新生成。为什么强调机器人账号因为它可以按项目、按动作pull / push精细授权出了问题可以单独回收不影响其他项目的镜像拉取。生产环境里你不可能因为某个镜像仓库应用轮换了密钥就去改 Harbor 所有用户的密码。2.3 Harbor 版本升级对集成配置的冲击回到热词“harbor 版本升级”。如果你是从 Harbor 1.x 升到 2.x或者 2.x 内的大版本升级有一个特别容易踩的雷机器人账号 token 的兼容性不是百分百继承的。Harbor 2.0 重新设计了机器人账号体系旧的系统级 robot account 和新的项目级 robot account 在 token 生成逻辑上完全不同。很多团队升级完成后旧的 token 看着还在但其实已经失效或者权限映射不对。反映到 containerd 侧就是原来配置的identitytoken突然不好使拉取时报 401。所以在升级 Harbor 之前建议先做三件事备份/etc/harbor或你的 Harbor 安装目录以及数据库目录记录当前所有机器人账号的用户名和权限但不要依赖旧 token升级完成后到 UI 里逐个重新生成机器人 token再更新到 containerd 的配置里。我们把 Harbor 这侧的状态理顺后再来看 containerd 该怎么配。3. 正确姿势用 hosts.toml 管理 TLS 信任把认证交给上层我第一次配 containerd 和 Harbor 的集成时也走了一些弯路。当时还在 containerd 1.4 时代大家喜欢直接在config.toml里写[plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.example.com] endpoint [https://registry.example.com]这种配置只解决了“允许从哪里拉”并没有解决证书和认证。所以后来 containerd 1.6 引入了config_path情况才变得可控。3.1 config_path 的作用与配置方法config_path让 containerd 从/etc/containerd/certs.d/registry-host/hosts.toml读取每个仓库的独立配置。这个机制的好处是每个镜像仓库一份配置互不干扰TLS 证书、CA、认证 header 都可以写在 hosts.toml 里kubelet、crictl、nerdctl 只要走 CRI 插件逻辑就会统一读到这份配置。在 containerd 的config.toml中找到 CRI 段加上version 2 root /var/lib/containerd [plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d然后重启 containerdsystemctl restart containerd注意如果你的 containerd 是 1.6.0 之前的版本config_path还不存在。我建议直接升级到 1.7 或更高版本因为新版本对 hosts.toml 的支持更完整也避免你在旧配置模型里浪费时间。3.2 hosts.toml 的语法server、ca、header 怎么配合假设你的 Harbor 域名是registry.example.com先建目录mkdir -p /etc/containerd/certs.d/registry.example.com接着编写/etc/containerd/certs.d/registry.example.com/hosts.tomlserver https://registry.example.com [host.https://registry.example.com] ca /etc/containerd/certs.d/registry.example.com/ca.crt [host.https://registry.example.com.header] authorization Basic dXNlcm5hbWU6cGFzc3dvcmQ这里的authorization是 Base64 编码后的username:password。我建议你把这个字段留给机器人账号。生成 Base64 的方式很简单echo -n robot$librarypullbot:你的token | base64如果你不想在 hosts.toml 里明文写认证信息还有另一个办法使用identitytoken字段直接把 Harbor 机器人 token 作为 Bearer token[host.https://registry.example.com] ca /etc/containerd/certs.d/registry.example.com/ca.crt identitytoken 这里填机器人token两种方式都可以区别在于authorization是 Basic 认证identitytoken是 Bearer Token。Harbor 两种都接受你自己选一种即可。3.3 为什么我不建议一上来就开 insecure_registries见过很多同事为了图省事在 containerdconfig.toml里写[plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d [plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.example.com] endpoint [https://registry.example.com]然后为了跳过证书又把 hosts.toml 里的skip_verify设成true。诚然内网环境里这样也许能拉通但代价是镜像下载过程变成明文传输一旦网络被劫持镜像内容可以被篡改skip_verify无法对 Harbor 服务器的身份做校验很容易成为中间人攻击目标排查问题时你分不清到底是因为证书、认证还是配置路径。正确的做法是使用完整 CA 证书链尽量别开skip_verify。后面排障部分我会具体讲证书链的问题。4. 端到端验证链路登录、推送、拉取、接进 K8s配置是一回事能不能真正用起来是另一回事。下面我从头到尾走一遍。4.1 分发 CA 证书到所有节点Harbor 使用的 CA 证书可能是自建 CA 签发的你需要把 CA 文件复制到所有需要拉镜像的节点上。通常放在/etc/containerd/certs.d/registry.example.com/ca.crt。如果你的证书链包含中间 CA就把根 CA 和中间 CA 按顺序拼接在同一个文件里cat root-ca.crt intermediate-ca.crt ca.crt拼接时注意顺序先根证书后中间证书Harbor 服务端返回的证书链才能被正确验证。复制完成后重启 containerdsystemctl restart containerd4.2 用 nerdctl 完成登录、打 tag、推送、拉取节点上如果没有 nerdctl先装一个。它和 Docker CLI 的命令高度相似用来调测非常顺手。nerdctl login registry.example.com -u robot$librarypushbot -p 你的token登录成功后随便拿一个本地镜像打 tag 并推送nerdctl pull nginx:1.25 nerdctl tag nginx:1.25 registry.example.com/library/nginx:1.25 nerdctl push registry.example.com/library/nginx:1.25推送成功后再把刚才推送的镜像删掉重新拉一遍nerdctl rmi registry.example.com/library/nginx:1.25 nerdctl pull registry.example.com/library/nginx:1.25如果这两条命令都成功说明 hosts.toml 里的证书信任和 nerdctl 的登录凭据已经能配合工作了。4.3 让 kubelet 通过 imagePullSecrets 使用机器人账号到了 Kubernetes 环境kubelet 通过 CRI 拉镜像时不会自动读取/root/.docker/config.json。我们可以在节点上的 hosts.toml 里配置header/identitytoken把凭据写死在里面这样 kubelet 能直接拉。但我不推荐这么做原因很简单节点上的凭据是静态的一旦机器人 token 轮换你要去改每一台节点风险很大。更干净的做法是hosts.toml 只负责证书信任和连接参数认证凭据通过 Kubernetes Secret 交给 kubelet。先在集群里创建镜像拉取专用 Secretkubectl create secret docker-registry harbor-registry \ --docker-serverregistry.example.com \ --docker-usernamerobot$librarypullbot \ --docker-password你的token \ --namespacedefault然后在 Pod 或 Deployment 里引用spec: template: spec: imagePullSecrets: - name: harbor-registry这样 kubelet 会把这个 Secret 中的凭据传给 containerd 的 CRI 插件用于 Harbor 认证。而节点上的 hosts.toml 依然提供 CA 证书配置两件事各管各的。4.4 验证 crictl 与 kubelet 的拉取在节点上想手动验证 crictl 能否拉取可以用crictl pull registry.example.com/library/nginx:1.25 \ --creds robot$librarypullbot:你的token注意--creds里的用户名是完整机器人用户名中间有$和所以建议外层用单引号包裹。如果你已经在 hosts.toml 里配好了identitytoken也可以直接crictl pull registry.example.com/library/nginx:1.25这种情况下 kubelet 不需要 Pod 挂 Secret 也能拉取前提是机器人账号的权限足够但我还是建议走 Secret 流程毕竟明文 token 散落在 hosts.toml 里不是一个长期可维护的方案。整个链路走通之后还有一个大家经常忽略的点Harbor 上删除或覆盖镜像后containerd 里可能还有缓存。你修改镜像 tag 并重新推送再到节点拉取时containerd 可能返回旧镜像。遇到这种情况手动清理一下节点上的镜像缓存crictl rmi registry.example.com/library/nginx:1.25再重新拉。5. 踩坑实录Harbor 升级、token 失效、配置冲突的逐级定位配置集成和排障是两回事。很多问题不是配置一次就能一劳永逸的我把生产环境里碰到的高频问题按“定位链路”列出来供你参考。5.1 升级 Harbor 后镜像拉取 401先查机器人 token场景Harbor 从 2.6 升到 2.8 后所有节点开始报401 Unauthorized而之前一切正常。定位思路先用 curl 直接测试 Harbor 的 v2 APIcurl -u robot$librarypullbot:原token https://registry.example.com/v2/如果返回 401基本可以确认是 token 失效或账号状态异常。登录 Harbor UI找到指定项目下的机器人账号重新生成 token再用新 token 测试 curl。如果 curl 通了更新 containerd 侧配置。如果你用的是 hosts.toml 里的identitytoken直接替换为新 token如果你用的是 Kubernetes Secret就更新 Secretkubectl create secret docker-registry harbor-registry \ --docker-serverregistry.example.com \ --docker-usernamerobot$librarypullbot \ --docker-password新token \ --namespacedefault \ --dry-runclient -o yaml | kubectl apply -f -更新完成后在节点上重新拉一遍镜像验证。这个坑的根源是 Harbor 版本升级后旧机器人 token 的签名算法或存储格式可能已经变化不要盲目怀疑 containerd 配置。5.2 nerdctl 正常但 crictl 失败配置作用域不同这是我同事遇到最多的问题。明明nerdctl pull registry.example.com/xxx能成功但是crictl pull registry.example.com/xxx就报unauthorized。原因很简单nerdctl用的是~/.docker/config.json里的登录凭据而crictl走的是 containerd 的 CRI 插件配置。两者读取的认证来源并不同。验证思路执行cat /root/.docker/config.json确认里面有auths条目查看/etc/containerd/config.toml确认config_path是否存在以及 hosts.toml 路径是否正确如果 hosts.toml 里没有配置认证信息直接执行crictl pull就会拉到认证失败。所以你选择哪种工具做验证就要在对应的配置层放好凭据。没有“一处登录处处拉取”的白嫖选项。5.3 hosts.toml 与 CRI registry.mirrors 的冲突排查还有一个比较容易混淆的点旧配置里用registry.mirrors指定 endpoint新配置里又加了config_path两者同时存在时containerd 的行为可能让人摸不着头脑。我曾经见过一个节点config_path已经指向/etc/containerd/certs.d但config.toml里还残留着[plugins.io.containerd.grpc.v1.cri.registry.mirrors.registry.example.com] endpoint [http://registry.example.com:80]结果 containerd 优先尝试了 HTTP 的 endpoint导致拉取时报http: server gave HTTP response to HTTPS client。排查时先确认配置文件中是否还有endpoint相关的旧字段。思路是grep -A5 -B5 registry.example.com /etc/containerd/config.toml如果同时存在config_path和mirrors建议删掉mirrors段统一用config_path管理。containerd 对这两类配置的优先级在不同版本里并不完全透明越早统一越少踩坑。5.4 证书链不完整报 unknown authority最后一个高频问题明明ca.crt文件存在路径也写对了但 containerd 仍然报x509: certificate signed by unknown authority。排查链路如下先确认 Harbor 服务端下发的证书链openssl s_client -showcerts -connect registry.example.com:443 /dev/null查看输出里的Certificate chain部分如果只有一层证书而你的ca.crt里只有一个根 CA且 Harbor 没有启用中间证书那么验证链可能不完整。检查ca.crt内容尝试用openssl verify验证openssl verify -verbose -CAfile /etc/containerd/certs.d/registry.example.com/ca.crt registry.crt一般解决方法是把 Harbor 使用的完整证书链文件含根 CA 和中间 CA放入ca.crt。不要图省事直接改 hosts.toml 里的skip_verify true那样等于掩耳盗铃。问题解决之后你会发现后续验证一切都顺了。6. 生产环境维护这些配置时我坚持的几个原则集成搞定只是开始真正考验人的是长期维护。下面这些原则是我在实际运维中沉淀下来的每次换节点、升级组件时都用得上。6.1 把配置当代码管理/etc/containerd/certs.d/目录和config.toml的修改我强烈建议纳入配置管理工具比如 Ansible、Puppet或者至少纳入 Git 仓库。节点少的时候还可以手工登录去改节点一多手工分发 CA 证书和 hosts.toml 必然出错。Ansible 的剧本大概长这样- name: 分发 Harbor CA 证书到节点 copy: src: files/harbor-ca.crt dest: /etc/containerd/certs.d/registry.example.com/ca.crt owner: root group: root mode: 0644 notify: restart containerd - name: 确保 hosts.toml 配置正确 copy: src: files/registry.example.com.hosts.toml dest: /etc/containerd/certs.d/registry.example.com/hosts.toml notify: restart containerd这样每次调整配置都有迹可循也能用 CI/CD 在测试环境先验证。6.2 机器人账号按项目分禁止共享我看到过最危险的操作把某一个项目的机器人账号同时给 5 个不同业务集群用。一旦其中一个业务需要临时拉取另一个项目的镜像权限越界或者有人误删 token所有业务都会同时挂掉。我的习惯是push 镜像走单独的机器人账号集群拉取走单独的 pull-only 账号每个项目至少两个账号权限分开账号命名和用途直接写在 Harbor 备注里。这样任何一次 token 轮换影响面都可控。6.3 定期健康检查和 Harbor GCcontainerd 与 Harbor 集成后镜像拉取链路会长期静默运行。我建议定期做一次健康检查不一定很复杂crictl pull registry.example.com/library/health-check:v1或者用 Prometheus 的 probe 定期请求 Harbor 的/v2/端点。另外Harbor 的镜像删除不会立刻释放存储空间。开发环境经常更新镜像 tagHarbor 里的不可见 tag 会堆积。记得定期让运维在 Harbor UI 或 API 里触发垃圾回收GC确保镜像仓库不会因为存储爆满而拒绝 containerd 的推送请求。6.4 升级前的最小化变更清单无论是升级 Harbor 还是升级 containerd我都建议先攥一张清单确认新版本兼容当前使用的 hosts.toml 语法备份 Harbor 的配置目录和数据库记录当前所有机器人账号用途在测试环境完整跑一遍nerdctl push/crictl pull/ kubelet 拉取升级完成后重新生成机器人 token 并同步到 containerd 侧滚动重启所有节点 containerd观察日志。这张清单我每次都会用虽然看起来繁琐但能避免很多半夜被叫醒的重复劳动。这套 containerd 与 Harbor 的集成配置本质上没有太高深的技术但它横跨了 CRI、registry、证书、认证好几个层面任何一个环节不匹配就会让整个链路失灵。我个人的体会是先把 hosts.toml 和 config.toml 的分工搞清楚再动手配置后面那些报错基本上都能一眼定位。希望这份经验能帮你少走点弯路。