主题
Kubernetes 部署带 HTTPS 与认证的本地 Registry
结论
在没有动态存储供应器的 Kubernetes 集群中,可以使用 通用静态 StorageClass + Local PV,把 Registry 数据固定保存到指定节点的宿主机目录。
本实践最终得到:
text
registry.cluster.local:30443
│ HTTPS + Basic Auth
▼
Kubernetes Service(NodePort)
▼
Registry Pod(固定运行在 master)
▼
PVC registry/registry-data
▼
PV registry-local-pv
▼
master:/var/lib/registry已验证的关键链路:
- Registry Pod 删除后自动重建;镜像数据仍然存在。
master可以通过 HTTPS 认证 push 镜像。node可以通过 containerd 认证 pull 镜像。- Kubernetes Pod 在
node上使用imagePullSecret成功拉取私有镜像并启动。
适用范围与边界
- 适合小型内网、测试环境和单实例 Registry。
- 使用节点本地磁盘,不是分布式存储,也不是高可用方案。
- Registry 数据实际位于
master节点;master 故障时,数据不会自动迁移到 node。 - PV 的
Retain策略用于避免误删 PVC 时自动清理 Registry 数据。 - 本文使用 Registry
3.0.0、containerd2.2.3、Kubernetesv1.35.4的实际环境验证。
一、前置条件
示例集群:
| 项目 | 值 |
|---|---|
| 存储节点 | master |
| master IP | <MASTER_IP> |
| node IP | <NODE_IP> |
| 宿主机数据目录 | /var/lib/registry |
| Registry 域名 | registry.cluster.local |
| Registry 访问端口 | 30443 |
| Registry 地址 | registry.cluster.local:30443 |
| StorageClass | local-storage |
| PV 容量声明 | 50Gi |
在继续前确认:
bash
kubectl get nodes -o wide
kubectl get storageclass
kubectl get svc -A | grep 30443如果 30443 已被占用,需要换 NodePort,并同步修改证书、containerd 配置、Registry 地址和后续命令。
二、在 master 创建宿主机目录
以下命令在 master 节点执行:
bash
hostname
mkdir -p /var/lib/registry
chmod 0755 /var/lib/registry
ls -ld /var/lib/registry
df -h /var/lib/registry确认目录确实位于目标节点,并检查磁盘剩余空间。50Gi 是 Kubernetes 对 PV 的容量声明,不会自动限制宿主机目录的实际使用量,必须自行监控根分区空间。
确认节点标签:
bash
kubectl get node master -L kubernetes.io/hostname必须使用:
text
kubernetes.io/hostname=master三、创建通用 StorageClass
StorageClass 不指定宿主机目录。目录应该写在每一个 Local PV 中,这样同一个通用 StorageClass 可以复用于 /var/lib/registry、/data/mysql 等其他目录。
创建 /data/storageclass/local-storageclass.yaml:
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain应用并检查:
bash
kubectl apply -f local-storageclass.yaml
kubectl get storageclass local-storage说明:
kubernetes.io/no-provisioner:使用静态 Local PV,不依赖动态 provisioner。WaitForFirstConsumer:结合实际 Pod 调度节点后再完成绑定。Retain:删除 PVC 后保留 PV 和宿主机数据。- 后续新增本地目录时,创建新的 PV,继续使用
local-storage。
四、创建 Registry Local PV
创建 registry-pv.yaml:
yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: registry-local-pv
spec:
capacity:
storage: 50Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /var/lib/registry
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master应用并确认:
bash
kubectl apply -f registry-pv.yaml
kubectl get pv registry-local-pv -o wide初始状态应为 Available。
五、创建 Namespace 和 PVC
创建 registry-pvc.yaml:
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
namespace: registry
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
volumeName: registry-local-pv
resources:
requests:
storage: 50Gi先创建 Namespace:
bash
kubectl create namespace registry再创建 PVC:
bash
kubectl apply -f registry-pvc.yaml
kubectl get pvc -n registry
kubectl get pv registry-local-pv因为 PV 和 PVC 都显式指定了名称,PVC 会绑定到 registry-local-pv。如果使用 WaitForFirstConsumer,尚未创建消费者 Pod 时短暂 Pending 属于正常现象;本实践中最终状态应为:
text
registry-data Bound registry-local-pv 50Gi RWO local-storage六、配置内部域名
本实践没有使用外部 DNS,而是在两个 Kubernetes 节点的 /etc/hosts 中添加:
text
<MASTER_IP> registry.cluster.local在 master 和 node 分别执行:
bash
grep -q 'registry.cluster.local' /etc/hosts || \
echo '<MASTER_IP> registry.cluster.local' >> /etc/hosts
getent hosts registry.cluster.local应返回:
text
<MASTER_IP> registry.cluster.local推送镜像的其他客户端也必须能解析该域名。更正式的环境应在内网 DNS 中创建 A 记录,而不是依赖每台机器的 /etc/hosts。
七、生成内部 CA 和 Registry 证书
证书必须在 SAN 中包含真正使用的域名;本实践同时加入 IP SAN,便于必要时使用 IP 访问。
在 master 执行:
bash
mkdir -p /data/registry/registry-certs
cd /data/registry/registry-certs
openssl genrsa -out registry-ca.key 4096
openssl req -x509 -new -sha256 \
-key registry-ca.key -days 3650 \
-subj "/CN=Registry Internal CA" \
-out registry-ca.crt
openssl genrsa -out registry.key 4096创建 registry-openssl.cnf:
ini
[req]
prompt = no
distinguished_name = dn
req_extensions = req_ext
[dn]
CN = registry.cluster.local
[req_ext]
subjectAltName = @alt_names
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[alt_names]
DNS.1 = registry.cluster.local
IP.1 = <MASTER_IP>签发服务端证书:
bash
openssl req -new -key registry.key \
-out registry.csr -config registry-openssl.cnf
openssl x509 -req -in registry.csr \
-CA registry-ca.crt -CAkey registry-ca.key -CAcreateserial \
-out registry.crt -days 825 -sha256 \
-extensions req_ext -extfile registry-openssl.cnf
chmod 0600 registry-ca.key registry.key验证:
bash
openssl verify -CAfile registry-ca.crt registry.crt
openssl x509 -in registry.crt -noout -subject -issuer -ext subjectAltName预期包含:
text
registry.crt: OK
DNS:registry.cluster.local
IP Address:<MASTER_IP>安全要求:
- 不要把
registry-ca.key、registry.key、密码或 Secret 内容提交 Git 或写入文章。 - Kubernetes 只需要服务端证书和私钥;CA 私钥应留在受控位置。
八、创建 TLS 和认证 Secret
创建 TLS Secret:
bash
kubectl create secret tls registry-tls \
--namespace registry \
--cert=registry.crt \
--key=registry.key \
--dry-run=client -o yaml | kubectl apply -f -创建 bcrypt 认证文件。密码使用交互式输入,不要出现在命令参数或 Shell 历史中:
bash
mkdir -p /data/registry/auth
cd /data/registry/auth
htpasswd -B -c htpasswd admin
chmod 0600 htpasswd
cut -d: -f1 htpasswd创建认证 Secret:
bash
kubectl create secret generic registry-auth \
--namespace registry \
--from-file=htpasswd=./htpasswd \
--dry-run=client -o yaml | kubectl apply -f -检查时只显示类型和元数据,不输出 Secret 内容:
bash
kubectl get secret registry-tls -n registry
kubectl get secret registry-auth -n registry九、部署 Registry
创建 registry-deployment.yaml。示例使用 registry:3.0.0、PVC、TLS Secret、认证 Secret,并固定到 master:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry
namespace: registry
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: registry
template:
metadata:
labels:
app: registry
spec:
nodeSelector:
kubernetes.io/hostname: master
containers:
- name: registry
image: registry:3.0.0
imagePullPolicy: IfNotPresent
ports:
- name: https
containerPort: 5000
env:
- name: REGISTRY_HTTP_ADDR
value: ":5000"
- name: REGISTRY_HTTP_TLS_CERTIFICATE
value: /certs/tls.crt
- name: REGISTRY_HTTP_TLS_KEY
value: /certs/tls.key
- name: REGISTRY_AUTH
value: htpasswd
- name: REGISTRY_AUTH_HTPASSWD_REALM
value: Registry Realm
- name: REGISTRY_AUTH_HTPASSWD_PATH
value: /auth/htpasswd
- name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
value: /var/lib/registry
readinessProbe:
tcpSocket:
port: https
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: https
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: data
mountPath: /var/lib/registry
- name: tls
mountPath: /certs
readOnly: true
- name: auth
mountPath: /auth
readOnly: true
volumes:
- name: data
persistentVolumeClaim:
claimName: registry-data
- name: tls
secret:
secretName: registry-tls
- name: auth
secret:
secretName: registry-auth
---
apiVersion: v1
kind: Service
metadata:
name: registry
namespace: registry
spec:
type: NodePort
selector:
app: registry
ports:
- name: https
protocol: TCP
port: 5000
targetPort: https
nodePort: 30443应用并确认:
bash
kubectl apply -f registry-deployment.yaml
kubectl rollout status deployment/registry -n registry --timeout=180s
kubectl get pod,svc,pvc -n registry -o wide应看到 Registry Pod 在 master 上 1/1 Running,Service 为 5000:30443/TCP,PVC 为 Bound。
十、验证 HTTPS 和认证
匿名访问必须被拒绝:
bash
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-i https://registry.cluster.local:30443/v2/预期:401 Unauthorized。
认证访问应成功:
bash
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-u admin -i https://registry.cluster.local:30443/v2/交互输入密码,预期:200 和 {}。
十一、配置两个节点的 containerd 信任
两个节点的运行时均为 containerd 2.2.3。每个节点都需要:
/etc/containerd/certs.d/registry.cluster.local:30443/registry-ca.crt/etc/containerd/certs.d/registry.cluster.local:30443/hosts.toml- CRI Registry 配置指向
/etc/containerd/certs.d
hosts.toml 内容:
toml
server = "https://registry.cluster.local:30443"
[host."https://registry.cluster.local:30443"]
capabilities = ["pull", "resolve", "push"]
ca = "/etc/containerd/certs.d/registry.cluster.local:30443/registry-ca.crt"复制 CA 到两个节点后,比较校验和:
bash
sha256sum /etc/containerd/certs.d/registry.cluster.local:30443/registry-ca.crt两个节点必须一致。
在 /etc/containerd/config.toml 中,只修改这一处:
toml
[plugins.'io.containerd.cri.v1.images'.registry]
config_path = '/etc/containerd/certs.d'注意:containerd v2 配置中可能还有其他名为 config_path 的字段,不能用全局 sed 盲目替换。尤其不要修改:
toml
[plugins.'io.containerd.transfer.v1.local']
config_path = ''修改前备份并检查差异:
bash
cp -a /etc/containerd/config.toml /etc/containerd/config.toml.before-registry重启每个节点的 containerd 后检查:
bash
systemctl restart containerd
systemctl is-active containerdmaster 上确认:
bash
kubectl get nodes如果 CRI 仍然报 x509: certificate signed by unknown authority,将 CA 加入系统信任库:
bash
cp /etc/containerd/certs.d/registry.cluster.local:30443/registry-ca.crt \
/usr/local/share/ca-certificates/registry-internal-ca.crt
update-ca-certificates
systemctl restart containerd十二、验证 push/pull
本实践的环境只有 ctr,没有 Docker、nerdctl 或 skopeo。用 ctr 时必须指定:
bash
--hosts-dir /etc/containerd/certs.d多架构镜像可能只存在索引而缺少引用内容。可先拉取 amd64,再转换成真正的单平台标签:
bash
ctr -n k8s.io images pull --platform linux/amd64 \
docker.io/curlimages/curl:8.7.1
ctr -n k8s.io images rm \
registry.cluster.local:30443/test/curl:8.7.1
ctr -n k8s.io images convert --platform linux/amd64 --oci \
docker.io/curlimages/curl:8.7.1 \
registry.cluster.local:30443/test/curl:8.7.1推送:
bash
ctr -n k8s.io images push \
--hosts-dir /etc/containerd/certs.d \
--user admin \
registry.cluster.local:30443/test/curl:8.7.1检查:
bash
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-u admin https://registry.cluster.local:30443/v2/_catalog
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-u admin https://registry.cluster.local:30443/v2/test/curl/tags/listnode 拉取:
bash
ctr -n k8s.io images pull \
--hosts-dir /etc/containerd/certs.d \
--user admin \
registry.cluster.local:30443/test/curl:8.7.1十三、验证 Kubernetes 使用 imagePullSecret 拉取
创建凭据时使用交互式变量;不要把真实密码写入命令历史:
bash
read -s REGISTRY_PASSWORD
printf '\n'
kubectl create secret docker-registry registry-pull-secret \
-n registry \
--docker-server=registry.cluster.local:30443 \
--docker-username=admin \
--docker-password="$REGISTRY_PASSWORD" \
--dry-run=client -o yaml | kubectl apply -f -
unset REGISTRY_PASSWORD测试 Pod 示例:
yaml
apiVersion: v1
kind: Pod
metadata:
name: registry-pull-test
namespace: registry
spec:
nodeSelector:
kubernetes.io/hostname: node
imagePullSecrets:
- name: registry-pull-secret
restartPolicy: Never
containers:
- name: curl
image: registry.cluster.local:30443/test/curl:8.7.1
imagePullPolicy: Always
command: ["sh", "-c", "echo registry-pull-ok; sleep 3600"]验证:
bash
kubectl apply -f registry-pull-test.yaml
kubectl wait --for=condition=Ready pod/registry-pull-test -n registry --timeout=180s
kubectl get pod registry-pull-test -n registry -o wide
kubectl logs registry-pull-test -n registry
kubectl describe pod registry-pull-test -n registry验收证据:
text
1/1 Running,节点为 node
日志包含 registry-pull-ok
Events 包含 Successfully pulled image十四、验证 Pod 重建后的持久化
只删除 Registry Pod,不删除 Deployment、PVC 或 PV:
bash
kubectl delete pod -n registry -l app=registry
kubectl rollout status deployment/registry -n registry --timeout=180s重新查询仓库:
bash
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-u admin https://registry.cluster.local:30443/v2/_catalog
curl --cacert /data/registry/registry-certs/registry-ca.crt \
-u admin https://registry.cluster.local:30443/v2/test/curl/tags/list本实践实际结果仍能看到:
json
{"repositories":["test/curl"]}
{"name":"test/curl","tags":["8.7.1"]}十五、测试资源清理
只删除测试 Pod 和本地测试清单:
bash
kubectl delete pod registry-pull-test -n registry
rm -f /data/registry/registry-pull-test.yaml不要误删正式 Registry Deployment。当前正式服务资源包括:
text
registry Deployment
registry Service
registry-data PVC
registry-local-pv PV
local-storage StorageClass
registry-tls Secret
registry-auth Secret
registry-pull-secret Secret删除 Deployment 会停止 Registry 服务,但不会因为 PV 的 Retain 策略自动删除 /var/lib/registry 数据。
十六、完整卸载(谨慎执行)
以下命令会停止 Registry。确认不再需要服务、并已备份镜像数据后再执行:
bash
kubectl delete deployment registry -n registry
kubectl delete service registry -n registry
kubectl delete pvc registry-data -n registry
kubectl delete pv registry-local-pv
kubectl delete secret registry-tls registry-auth registry-pull-secret -n registry
kubectl delete namespace registry
kubectl delete storageclass local-storage由于 PV 使用 Retain,删除 PV/PVC 不等于删除宿主机目录;如需彻底删除数据,必须在 master 上明确执行:
bash
rm -rf /var/lib/registry该命令不可逆,执行前必须确认备份和删除范围。
验收清单
- [x]
local-storage通用静态 StorageClass 创建成功 - [x]
registry-local-pv绑定到master:/var/lib/registry - [x]
registry-dataPVC 为Bound - [x] 内部域名在 master 和 node 可解析
- [x] 证书包含
DNS:registry.cluster.local和IP:<MASTER_IP> - [x] 匿名 Registry API 返回
401 Unauthorized - [x] 认证 Registry API 返回
200 {} - [x] master push 成功
- [x] node ctr pull 成功
- [x] Registry Pod 重建后镜像仍存在
- [x] Kubernetes Pod 在 node 上使用 imagePullSecret 拉取并运行成功
后续部署私有镜像
后续业务镜像统一使用:
text
registry.cluster.local:30443/<project>/<image>:<tag>每个需要拉取私有镜像的命名空间都必须配置对应的 docker-registry Secret,并在 Pod、Deployment 或 ServiceAccount 中引用 imagePullSecrets。不要只修改 operator 镜像;还要检查 sidecar、工作负载和 CustomResource 中指定的镜像地址。