跳转到内容

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、containerd 2.2.3、Kubernetes v1.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
StorageClasslocal-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.keyregistry.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 在 master1/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。每个节点都需要:

  1. /etc/containerd/certs.d/registry.cluster.local:30443/registry-ca.crt
  2. /etc/containerd/certs.d/registry.cluster.local:30443/hosts.toml
  3. 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 containerd

master 上确认:

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/list

node 拉取:

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-data PVC 为 Bound
  • [x] 内部域名在 master 和 node 可解析
  • [x] 证书包含 DNS:registry.cluster.localIP:<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 中指定的镜像地址。

基于 MIT 许可发布