按原名从内部镜像站拉取镜像
本实验在真正的 k3s 中运行
VM 中运行着一台 k3s(v1.33.3+k3s1),并安装了 skopeo、podman 和 Ubuntu 的 docker-registry 软件包(CNCF distribution 注册表)。首次启动需要几分钟。VM 起初可以访问互联网(80/443)——到第 3 步为止是联网状态下的导入准备,在第 4 步中由你封闭出站,把它变成隔离网络。评分由从外部连入 VM 的 8899 端口的代理完成,所以如果封闭了已建立的连接和回环,评分就会中断。
目标
用私有 CA 以 HTTPS 搭建内部注册表,原样搬运镜像及其摘要,然后在外部被封闭的状态下,让 podman、containerd 和 Kubernetes 在不改变镜像名称的情况下,从内部 mirror 下载。
为什么重要
在隔离网络中,如果把清单中的镜像名称全部改成内部注册表地址,chart 和清单就会与外部分道扬镳,每次获取下一个版本都要重新修改。mirror 保持名称不变,只告诉运行时“那个注册表的内容从这里下载”。陷阱在于每种运行时的配置文件不同——podman 是 registries.conf,containerd 是 hosts.toml,k3s 是 registries.yaml。而且都必须另外告知私有 CA。
预计 60 分钟。会话结束后 VM 会消失,想保留的文件请在结束前另行保存。
步骤
- 在
/root/registry/pki中创建根证书(ca.crt、ca.key,CN 为Airgap Internal Root CA)和registry.airgap.internal证书(registry.crt、registry.key,带 SAN),并在/etc/hosts中加入127.0.0.1 registry.airgap.internal。 - 通过
/etc/docker/registry/config.yml让内部注册表在127.0.0.1:5000上用该证书提供 HTTPS,并启用服务,使其开机时也会启动。注册表服务账号必须能够读取密钥。 - 联网期间,把
registry.k8s.io/e2e-test-images/busybox:1.36.1-1和registry.k8s.io/pause:3.10,以保持摘要不变的方式搬运到内部注册表的相同路径(registry.airgap.internal:5000/e2e-test-images/busybox:1.36.1-1、.../pause:3.10),并把원래이름 다이제스트(占位符依次为原始名称和摘要)逐行写入/root/registry/digests.txt。 - 用重复运行也只会留下一套规则的
/root/registry/egress.sh封闭出站(链AIRGAP-EGRESS,在 OUTPUT 和 FORWARD 中各引用一次,放行回环、已建立的连接、10.42.0.0/16 和 10.43.0.0/16)。 - podman:通过
/etc/containers/registries.conf.d/50-airgap-mirror.conf把registry.k8s.io的 mirror 设为内部注册表,告知私有 CA 之后,以原本的名称registry.k8s.io/e2e-test-images/busybox:1.36.1-1执行 pull。 - containerd:手写
/root/registry/certs.d/registry.k8s.io/hosts.toml,并用k3s ctr -n airgap-ctr images pull --hosts-dir /root/registry/certs.d registry.k8s.io/pause:3.10下载。 - Kubernetes:通过
/etc/rancher/k3s/registries.yaml让 k3s 使用内部 mirror 并重新启动 k3s,然后在命名空间airgap中,让使用原本名称、imagePullPolicy: Always的 Podmirror-busybox(清单为/root/registry/mirror-busybox.yaml)进入 Running。 - 在
/root/registry/mirror-record.json中留下导入记录——images数组的每一项包含ref、mirror_ref、digest、pulled_by(podman、ctr、kubelet 中实际完成下载的那些),最上层包含egress_blocked。
参考
- 证书:与私有 CA 模块流程相同(必须有 SAN)。
- 注册表响应:
curl --cacert /root/registry/pki/ca.crt https://registry.airgap.internal:5000/v2/_catalog - 摘要:
skopeo inspect --format '{{.Digest}}' docker://<이미지>(占位符为镜像)、保持摘要搬运:skopeo copy --all --preserve-digests ... - podman 和 skopeo 存放注册表 CA 的位置:
/etc/containers/certs.d/<호스트:포트>/ca.crt(占位符为主机:端口) - k3s 生成的 containerd 配置:
/var/lib/rancher/k3s/agent/etc/containerd/ - 常见错误 1:让注册表密钥只有 root 能读。服务以专用账号运行,读不到密钥而崩溃——
journalctl -u docker-registry。 - 常见错误 2:不加
--all就搬运。多架构列表会缩减为单一架构,摘要随之改变,用摘要固定的清单就找不到 mirror 中的镜像。
内部注册表证书
在 /root/registry/pki 中创建根证书(ca.crt、ca.key)和 registry.airgap.internal 证书(registry.crt、registry.key,带 SAN),并把该名称加入 /etc/hosts。
根证书是 CA:TRUE 的自签名,服务器证书则用根证书签名,同时通过扩展文件给出 SAN。流程与私有 CA 模块相同。
用 HTTPS 提供内部注册表
通过 /etc/docker/registry/config.yml 让注册表在 127.0.0.1:5000 上用该证书提供 HTTPS,并启用服务,使其开机时也会启动。
在配置的 http 部分给出地址和 tls(证书和密钥路径)。服务以专用账号运行,所以必须把密钥放在该账号能够读取的位置并设置相应权限。如果没有启动,请先看 journalctl -u docker-registry。
保持摘要不变地搬运
把两个镜像以保持摘要不变的方式搬运到内部注册表的相同路径,并把 원래이름 다이제스트(占位符依次为原始名称和摘要)逐行写入 /root/registry/digests.txt。
skopeo copy 可以直接从注册表搬运到注册表。必须整体搬运多架构列表,摘要才会与外部相同。把内部注册表的 CA 放在 /etc/containers/certs.d/<主机:端口>/ca.crt 中,skopeo 和 podman 就会共同使用。
封闭出站,变成隔离网络
用重复运行也只会留下一套规则的 /root/registry/egress.sh 封闭出站。
新建一条链,并在 OUTPUT 和 FORWARD 的最前面转到这条链,采用清空再重新填充的方式,无论运行几次,结果都是同样的形态。请先放行评分代理和 Kubernetes 网段。
podman:registries.conf 中的 mirror
通过 /etc/containers/registries.conf.d/50-airgap-mirror.conf 把 registry.k8s.io 的 mirror 设为内部注册表,并以原本的名称 pull busybox。
registries.conf 的 [[registry]] 用 prefix 写明适用于哪个名称,[[registry.mirror]] 写明先尝试的位置。不改变名称。私有 CA 直接使用第 3 步中放置的 certs.d。
containerd:手写 hosts.toml
编写 /root/registry/certs.d/registry.k8s.io/hosts.toml,并用 k3s ctr -n airgap-ctr images pull --hosts-dir /root/registry/certs.d registry.k8s.io/pause:3.10 下载。
目录名称是原本的注册表,文件中的 server 是原本的地址,[host."..."] 是先尝试的 mirror。在 mirror 中写入 pull、resolve 权限和 CA 路径。
Kubernetes:registries.yaml 与 Always Pod
通过 /etc/rancher/k3s/registries.yaml 让 k3s 使用内部 mirror 并重新启动,然后在命名空间 airgap 中,让 imagePullPolicy: Always 的 Pod mirror-busybox(清单为 /root/registry/mirror-busybox.yaml)进入 Running。
在 mirrors 中写原本的注册表和 endpoint,在 configs 中写 mirror 地址的 tls ca_file。如 k3s 文档所述,修改之后必须重新启动 k3s。镜像名称保持原样——即使是 Always,只要能在 mirror 中解析标签,就能启动。
把导入记录与注册表、运行时核对
在 /root/registry/mirror-record.json 中,为每个镜像留下 ref、mirror_ref、digest、pulled_by,并在最上层留下 egress_blocked。
摘要向内部注册表查询后提取,pulled_by 写 podman、ctr、kubelet 中实际下载了该镜像的那一方。值不要手抄,请从命令输出中收集。