跳转至

LDAP(OpenLDAP 2.6)在 Kubernetes 集群部署与数据恢复 SOP

本文档已按实际执行结果修订。文中命令为实际执行序列,并标注了执行中发现的镜像/Chart 缺陷与对策。 命名空间已由 ldap 迁移至 server(与 traefik、gitlab 同命名空间),迁移过程见附录 B。

  • 目标集群:kubernetes-admin@kubernetes(v1.36.4,4 节点:k8s-master-01 / k8s-node-01~03)
  • 部署命名空间:server
  • 交付目录:ldap-deploy/(相对项目根目录)
  • 实施结果:openldap、phpldapadmin 两个 release 在 server 命名空间 deployed,Pod 重启数 0;22 条目数据完整
  • 说明:按用户要求本次不使用 Python 工程实现,以本文件 shell 步骤为实现体;幂等、备份、失败判定由各步骤自身的前置状态检查承担(用户明确指令优先于默认约定)

速查与使用指引(先看这里)

本节只放变更执行相关的速查;日常运维、巡检、故障处置、备份与恢复细则见同目录 RUNBOOK.md。 本次变更的过程、风险与回滚见后续章节。

目录

取管理员明文口令

kubectl -n server get secret openldap-backup-pw -o jsonpath='{.data.password}' | base64 -d; echo
# 备选:kubectl -n server exec deploy/phpldapadmin -- printenv LDAP_PASSWORD

登录 Web 管理界面:浏览器打开 http://ldapadmin.rainchen.top,User ID 填 ldapadmin(uid 值,不是 DN),密码为上面取到的口令。

验证 LDAP 服务

# 管理员绑定(-W 交互输入口令;预期 result: 0 Success)
kubectl -n server exec -it openldap-0 -- ldapsearch -x -H ldap://localhost:3890 \
  -D "cn=admin,dc=rainchen,dc=com" -W -b "dc=rainchen,dc=com" -s base dn

# 条目总数(预期 23 = 22 原有 + 管理员条目)
kubectl -n server exec openldap-0 -- sh -c \
  'slapcat -F /etc/openldap/slapd.d -b "dc=rainchen,dc=com" 2>/dev/null | grep -c "^dn:"'

# 对外端口(经 traefik,集群内任意 Pod 执行)
POD=$(kubectl -n server get pod -l app.kubernetes.io/name=phpldapadmin -o jsonpath='{.items[0].metadata.name}')
kubectl -n server exec -it "$POD" -- ldapsearch -x -H ldap://192.168.31.10:389 \
  -D "cn=admin,dc=rainchen,dc=com" -W -b "dc=rainchen,dc=com" -s base dn

客户端接入地址:明文 ldap://192.168.31.10:389;加密 ldaps://192.168.31.10:636(自签证书,客户端需信任 ldap-deploy/certs/ca.crt)。

查看 / 手动触发备份

kubectl -n server get cronjob openldap-ldif-backup
kubectl -n server create job --from=cronjob/openldap-ldif-backup backup-$(date +%m%d%H%M)
kubectl -n server exec openldap-0 -- ls -l /var/lib/openldap/openldap-backups/   # 备份落在 backups 卷

服务状态 / 重启

kubectl -n server get pod,svc,pvc,cronjob | grep -E "openldap|phpldapadmin"
kubectl -n server rollout restart statefulset/openldap     # 重启 LDAP
kubectl -n server rollout restart deploy/phpldapadmin      # 重启管理界面

回滚

helm history openldap -n server                            # 选目标 revision
helm rollback openldap <revision> -n server --wait          # 回滚 release
helm rollback phpldapadmin <revision> -n server --wait
# 管理员条目 / ACL 的回滚见 B.1 节末(留档 rollback-config-202609191410.ldif)

关键路径与资源名

用途 位置
数据留档(22 条,含 operational 属性) ldap-deploy/rollback-export-202609191327.ldif
管理员条目 / ACL 变更 ldap-deploy/ldapadmin-user.ldif、ldap-deploy/acl-ldapadmin.ldif
配置(含口令哈希,权限 600) ldap-deploy/values-openldap.yaml、values-phpldapadmin.yaml
证书 ldap-deploy/certs/
路由与备份清单 ldap-deploy/manifests/
命名空间 / 资源名 ns server;sts openldap;pvc conf-openldap-0、data-openldap-0、backups-openldap-0
Secret openldap-tls(证书)、openldap-backup-pw(管理员明文口令)

1. 背景

旧集群 OpenLDAP(osixia/openldap v1,命名空间 server,StatefulSet openldap)已下线,遗留备份位于 ldap/: - 旧集群遗留备份目录 ldap/(已于 2026-09-19 清理,内容已转存,见下): - ldap/ldap-export.ldif:slapcat 全量导出(327 行,22 个条目)→ 现存 ldap-deploy/rollback-export-202609191327.ldif - ldap/ldap-raw-*.tar.gz:原数据目录(config/ + data/),仅供同镜像版本原样还原(未保留) - ldap/deploy-files/:旧部署 YAML(未保留;其中管理员口令已转存为集群 Secret server/openldap-backup-pw)

自 ldap/ 清理后,重建所需的一切只依赖两处:ldap-deploy/ 下的留档文件 与 集群内 Secret(见第 3、5.4、5.6、5.9 节)。

新集群无 LDAP 服务,需要重建并让 dc=rainchen,dc=com 的客户端恢复可用。版本选择:osixia 官方 Helm Chart + OpenLDAP 2.6.10(旧环境为 2.4.57,已 EOL)。Bitnami chart 因 2025-09 起免费镜像迁入 bitnamilegacy 且 chart 仓库停更而排除。

2. 目标

  1. server 命名空间内 openldap 与 phpldapadmin 两个 release,Pod 全部 Ready。
  2. 基线 dc=rainchen,dc=com,管理员 cn=admin,dc=rainchen,dc=com,口令沿用旧环境。
  3. 22 个条目(1 后缀 + 5 ou + 8 posixGroup + 8 用户)全部导入,用户 {SSHA} 口令哈希保留。
  4. 集群外经 k8s-master-01 的 389 / 636 可访问 LDAP/LDAPS(经 traefik TCP 透传)。
  5. http://ldapadmin.rainchen.top 经 traefik 可访问 phpLDAPadmin。
  6. conf/data/backups 三个卷落在 nfs-csi-hdd,每日 LDIF 逻辑备份生效。
  7. 重复执行部署类步骤零变更、无报错。

3. 前置条件

类别 要求
权限 kubectl 集群管理员;helm v3.16.3
网络 集群节点可拉取 docker.io 镜像;执行端无法访问 osixia.github.io(实测超时),Chart 需从 GitHub Release 手工下载
存储 nfs-csi-hdd SC(server 192.168.31.249,share /srv/nfs/k8s-hdd,回收策略 Delete)
入口 traefik release 位于 server 命名空间(chart 41.6.0 / v3.7.13),单副本 + hostPort 80/443,固定 k8s-master-01
数据 ldap-deploy/rollback-export-202609191327.ldif(22 条目,含 operational 属性)
口令 集群 Secret server/openldap-backup-pw(键 password);若该 Secret 不存在则由操作者现场输入(敏感,不入库、不写入本文档)
执行前检查 见 5.0

4. 影响范围

对象 影响
traefik(server 命名空间) 新增 ldap/ldaps 两个 TCP entryPoint,traefik Pod 滚动重启;单副本 maxSurge=0,故 80/443 入口中断数秒
k8s-master-01 新增监听 389/636(hostPort)。执行前已用临时 Pod 声明 hostPort 验证无冲突
其他节点 无变更
LDAP 客户端 指向 192.168.31.10:389(或该 IP 的 DNS);用户原口令不变
在跑业务 集群内 gitlab / cnpg / minio 等未接入 LDAP,不受影响
停服 无(新建服务);仅 traefik 重启、以及路由切换到新命名空间时各有数秒中断
窗口 低峰执行;数据量 276K,恢复为秒级

5. 分步操作

本节即本次实现体(无独立 .sh 交付)。敏感值现场读取,不落盘明文、不进 shell 历史。

5.0 执行前检查

kubectl config current-context                  # kubernetes-admin@kubernetes
kubectl get nodes -o wide                       # 4 节点 Ready
kubectl get sc nfs-csi-hdd                      # 存在
helm list -A                                    # traefik 在 server 命名空间
grep -c "^dn:" ldap-deploy/rollback-export-202609191327.ldif    # 22(数据源未改)
kubectl -n server get svc,sts,deploy | grep -E "openldap|phpldapadmin"   # 应无同名资源
# 宿主端口占用检查(本机无免密 ssh,改用临时 Pod 声明 hostPort 探测)
kubectl run portcheck-ldap --image=docker.io/traefik:v3.7.13 \
  --overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"k8s-master-01"},"tolerations":[{"key":"node-role.kubernetes.io/control-plane","operator":"Exists","effect":"NoSchedule"}],"containers":[{"name":"probe","image":"docker.io/traefik:v3.7.13","command":["sleep","90"],"ports":[{"name":"l","containerPort":389,"hostPort":389},{"name":"s","containerPort":636,"hostPort":636}]}]}}'
kubectl get pod portcheck-ldap -o wide          # 预期 Running(即端口空闲)
kubectl delete pod portcheck-ldap

5.1 备份 traefik 现状

helm get values traefik -n server -o yaml   > ldap-deploy/traefik-values-before.yaml
helm get manifest traefik -n server         > ldap-deploy/traefik-manifest-before.yaml
kubectl get svc -n server traefik -o yaml   > ldap-deploy/traefik-svc-before.yaml
helm history traefik -n server               # 记录 revision
cp traefik_deploy/values.yaml "traefik_deploy/values.yaml.bak.$(date +%Y%m%d%H%M%S)"

5.2 traefik 增加 LDAP TCP entryPoint

在项目权威 values traefik_deploy/values.yaml 的 ports: 下新增(避免下次 upgrade 丢失):

  ldap:                      # 明文 LDAP:宿主机 389 → 容器 1389
    port: 1389
    hostPort: 389
    expose: { default: true }
    exposedPort: 389
    protocol: TCP
  ldaps:                     # LDAPS 透传:宿主机 636 → 容器 1636
    port: 1636
    hostPort: 636
    expose: { default: true }
    exposedPort: 636
    protocol: TCP
# 使用本地 chart 目录,可离线渲染
helm upgrade traefik traefik_deploy/chart/traefik -n server \
  -f traefik_deploy/values.yaml --dry-run | grep -E "entryPoints\.(ldap|ldaps)"
# 预期:--entryPoints.ldap.address=:1389/tcp 与 --entryPoints.ldaps.address=:1636/tcp

helm upgrade traefik traefik_deploy/chart/traefik -n server \
  -f traefik_deploy/values.yaml --wait --timeout 5m
kubectl get pod -n server -l app.kubernetes.io/name=traefik -o wide   # 预期 1/1 Running
# 失败判定:5 分钟内未 deployed / Pod 未 Ready → 见 7.1 回滚

5.3 自签证书与 Secret(server 命名空间)

mkdir -p ldap-deploy/certs && cd ldap-deploy/certs
cat > ldap-cert.cnf <<'EOF'
[req]
distinguished_name = dn
x509_extensions = v3_req
prompt = no
[dn]
CN = ldap.rainchen.com
O  = rainchen
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = ldap.rainchen.com
DNS.2 = openldap.server.svc
DNS.3 = openldap.server.svc.cluster.local
IP.1  = 192.168.31.10
EOF
openssl genrsa 2048 > tls.key && chmod 600 tls.key
openssl req -new -x509 -key tls.key -out tls.crt -days 3650 -config ldap-cert.cnf -extensions v3_req
cp tls.crt ca.crt
cd -

kubectl -n server create secret generic openldap-tls \
  --from-file=tls.crt=ldap-deploy/certs/tls.crt \
  --from-file=tls.key=ldap-deploy/certs/tls.key \
  --from-file=ca.crt=ldap-deploy/certs/ca.crt \
  --dry-run=client -o yaml | kubectl apply -f -

# 备份任务所需的口令 Secret(若已存在则跳过;明文不出现在命令行与 shell 历史中)
read -rs -p "LDAP admin password: " PW && \
kubectl -n server create secret generic openldap-backup-pw \
  --from-literal=password="$PW" --dry-run=client -o yaml | kubectl apply -f - && unset PW

# 两个用于覆盖镜像缺陷的 ConfigMap(源文件在交付目录)
kubectl -n server create configmap openldap-pre-upgrade-hook \
  --from-file=startup.sh=ldap-deploy/pre-upgrade-startup.sh --dry-run=client -o yaml | kubectl apply -f -
kubectl -n server create configmap openldap-cron-process-hook \
  --from-file=process.sh=ldap-deploy/cron-process-placeholder.sh --dry-run=client -o yaml | kubectl apply -f -

5.4 生成管理员口令哈希(明文不落盘)

# 口令来源:集群内 Secret(重建/迁移场景);若 Secret 也不存在,则由操作者交互输入
kubectl -n server get secret openldap-backup-pw -o jsonpath='{.data.password}' | base64 -d > /tmp/.ldappw
chmod 600 /tmp/.ldappw
# 交互输入备选:read -rs -p "LDAP admin password: " PW && printf '%s' "$PW" > /tmp/.ldappw && unset PW

python3 -c "
import hashlib,base64,os
pw=open('/tmp/.ldappw','rb').read(); salt=os.urandom(4)
print('{SSHA}'+base64.b64encode(hashlib.sha1(pw+salt).digest()+salt).decode())
" > ldap-deploy/.admin-hash.tmp
chmod 600 ldap-deploy/.admin-hash.tmp && rm -f /tmp/.ldappw
# 备选:集群内一次性 Pod 执行 slappasswd -h '{SSHA}'(本机无 openldap 工具时)

5.5 安装 OpenLDAP

# Chart 从 GitHub Release 下载(执行端访问不到 osixia.github.io)
mkdir -p ldap-deploy/charts && cd ldap-deploy/charts
curl -fL -o openldap-0.1.0.tgz \
  https://github.com/osixia/helm-charts/releases/download/openldap-0.1.0/openldap-0.1.0.tgz
cd -

# values 见 ldap-deploy/values-openldap.yaml(600 权限,含口令哈希)
# 关键项与原因(本环境实测):
#   openldap.preUpgrade.enabled: false        —— 镜像内 pre-upgrade 脚本缺执行位,开启会崩溃循环(缺陷 1)
#   openldap.bootstrap.schemas 用 nis.ldif    —— rfc2307bis 下 posixGroup 非 structural,导入必失败(缺陷 2)
#   tlsSecret.existingSecret: ""              —— chart 挂成 tls.crt,镜像只认 cert.crt(缺陷 3)
#   extraVolumes/extraVolumeMounts            —— 证书改名挂载、/etc/crontabs 可写、pre-upgrade 脚本 0755 覆盖、cron process.sh 占位
#   backupCronJob.enabled: false              —— 该 Job 会另起 Pod 并发访问同一 LMDB(备份见 5.9)
#   persistence.storageClass: nfs-csi-hdd,accessModes: [ReadWriteMany]

helm template openldap ldap-deploy/charts/openldap-0.1.0.tgz -n server \
  -f ldap-deploy/values-openldap.yaml >/dev/null            # 渲染校验
helm install openldap ldap-deploy/charts/openldap-0.1.0.tgz -n server \
  -f ldap-deploy/values-openldap.yaml --wait --timeout 8m
kubectl -n server get pod,pvc
# 预期:openldap-0 1/1 Running;conf-openldap-0 / data-openldap-0 / backups-openldap-0 均 Bound

5.6 数据恢复(离线 slapadd)

# 1) 停库(避免 slapadd 与运行中的 slapd 争用 LMDB)
kubectl -n server scale statefulset openldap --replicas=0
kubectl -n server wait --for=delete pod/openldap-0 --timeout=120s

# 2) 临时 Pod 独占挂载 conf/data/backups 三卷
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata: { name: ldap-restore, namespace: server }
spec:
  restartPolicy: Never
  securityContext: { fsGroup: 911 }
  containers:
    - name: restore
      image: osixia/openldap:2.6.10-alpha
      command: ["sleep", "1800"]
      securityContext: { runAsUser: 911, runAsGroup: 911 }
      volumeMounts:
        - { name: data,    mountPath: /var/lib/openldap/openldap-data }
        - { name: conf,    mountPath: /etc/openldap/slapd.d }
        - { name: backups, mountPath: /var/lib/openldap/openldap-backups }
  volumes:
    - name: data
      persistentVolumeClaim: { claimName: data-openldap-0 }
    - name: conf
      persistentVolumeClaim: { claimName: conf-openldap-0 }
    - name: backups
      persistentVolumeClaim: { claimName: backups-openldap-0 }
EOF
kubectl -n server wait --for=condition=Ready pod/ldap-restore --timeout=120s

# 3) 【高危】清空 data 目录(bootstrap 已建过 dc=rainchen,dc=com,与 LDIF 首条冲突)
#    先落持久备份到 backups 卷,确认文件存在再删
kubectl -n server exec ldap-restore -- sh -c '
tar czf /var/lib/openldap/openldap-backups/data-before-import.tar.gz -C /var/lib/openldap openldap-data &&
ls -l /var/lib/openldap/openldap-backups/data-before-import.tar.gz &&
rm -f /var/lib/openldap/openldap-data/data.mdb /var/lib/openldap/openldap-data/lock.mdb &&
ls -la /var/lib/openldap/openldap-data'
# 失败判定:备份文件不存在,或删除后仍残留 *.mdb → 停止并检查卷权限

# 4) 导入(保留 operational 属性与用户口令哈希;slapadd 不校验父子顺序)
#    使用 ldap-deploy/rollback-export-202609191327.ldif(原 ldap/ 目录已于 2026-09-19 清理)
kubectl -n server cp ldap-deploy/rollback-export-202609191327.ldif ldap-restore:/tmp/restore.ldif
kubectl -n server exec ldap-restore -- \
  slapadd -F /etc/openldap/slapd.d -b "dc=rainchen,dc=com" -l /tmp/restore.ldif
# 预期:退出码 0、无 already exists / no structural object class 报错
kubectl -n server exec ldap-restore -- sh -c \
  'slapcat -F /etc/openldap/slapd.d -b "dc=rainchen,dc=com" 2>/dev/null | grep -c "^dn:"'   # 预期 22

# 5) 收尾:删临时 Pod、恢复服务(必须先导入后启库,否则 bootstrap 会重建基线)
kubectl -n server delete pod ldap-restore --wait=true
kubectl -n server scale statefulset openldap --replicas=1
kubectl -n server rollout status statefulset/openldap --timeout=300s

schema 变更时的重建顺序:若需更换 schemas,必须在 restore Pod 内一并备份并清空 conf (find /etc/openldap/slapd.d -mindepth 1 -delete)与 data,再 helm upgrade 让 bootstrap 用新 schema 重建 conf 与基线,之后重复 5.6 的导入。

5.7 安装 phpLDAPadmin

curl -fL -o ldap-deploy/charts/phpldapadmin-0.1.0.tgz \
  https://github.com/osixia/helm-charts/releases/download/phpldapadmin-0.1.0/phpldapadmin-0.1.0.tgz

helm install phpldapadmin ldap-deploy/charts/phpldapadmin-0.1.0.tgz -n server \
  -f ldap-deploy/values-phpldapadmin.yaml --wait --timeout 5m
# values 中两项关键覆盖(缺陷 4:chart 硬编码容器端口 80,镜像实际监听 8080)
#   ingress.enabled: false                 后续由自建 IngressRoute 承接
#   livenessProbe/readinessProbe.httpGet.port: 8080
#   app.ldap.host: openldap.server.svc.cluster.local
kubectl -n server get pod -l app.kubernetes.io/name=phpldapadmin    # 预期 1/1 Ready

5.7.1 目录管理员账号(phpLDAPadmin 登录必需)

phpLDAPadmin 2.x 的认证模型是"先在目录中检索到登录账号条目,再校验该条目的密码", 因此 rootdn(cn=admin,dc=rainchen,dc=com)不能用于 Web 登录——它是配置里的 rootdn,不是目录条目,按属性检索必然为空。 需在目录中创建专用管理员条目并授予 manage 权限:

# 1) 创建条目(密码沿用 LDAP 管理员口令;哈希生成方式见 5.4,交付文件内已含哈希)
kubectl -n server cp ldap-deploy/ldapadmin-user.ldif openldap-0:/tmp/ldapadmin-user.ldif
kubectl -n server exec openldap-0 -- ldapadd -x -H ldap://localhost:3890 \
  -D "cn=admin,dc=rainchen,dc=com" -y /tmp/pw -f /tmp/ldapadmin-user.ldif

# 2) 授予该条目 manage 权限:插入到 ACL 首位,by * break 保证原有规则继续生效
kubectl -n server cp ldap-deploy/acl-ldapadmin.ldif openldap-0:/tmp/acl-ldapadmin.ldif
kubectl -n server exec openldap-0 -- ldapmodify -x -H ldap://localhost:3890 \
  -D "cn=admin,cn=config" -y /tmp/pw -f /tmp/acl-ldapadmin.ldif

# 3) 验证:全量可读 23 条(22 原有 + ldapadmin),并检查 ACL 顺序
kubectl -n server exec openldap-0 -- ldapsearch -x -H ldap://localhost:3890 \
  -D "uid=ldapadmin,ou=it,ou=eda,dc=rainchen,dc=com" -y /tmp/pw \
  -b "dc=rainchen,dc=com" -s sub "(objectClass=*)" dn | grep -c "^dn:"   # 23
kubectl -n server exec openldap-0 -- sh -c \
  'slapcat -n 0 -F /etc/openldap/slapd.d | grep -A4 "dn: olcDatabase={1}mdb" | grep olcAccess'

登录方式:浏览器打开 http://ldapadmin.rainchen.top,User ID 填 ldapadmin(uid 值,不是 DN),密码为 LDAP 管理员口令。

5.8 注册对外路由(traefik CRD)

kubectl apply -f ldap-deploy/manifests/ldap-ingressroutes.yaml      # LDAP 389 / LDAPS 636
kubectl apply -f ldap-deploy/manifests/phpldapadmin-web.yaml        # Service(targetPort 8080) + IngressRoute
kubectl apply -f ldap-deploy/manifests/openldap-backup-cronjob.yaml # 每日 LDIF 备份
kubectl -n server get ingressroutetcp,ingressroute,svc

注意:同一个 entryPoint 上只能存在一条 match 相同的 IngressRouteTCP。若曾在其他命名空间部署过, 必须先删除旧路由再创建新路由,否则 traefik 会因重复路由报错或行为不确定。

5.9 幂等复核

# upgrade 时 chart 强制校验 previousImage.tag 非空(已在 values 中设为当前版本)
helm upgrade openldap     ldap-deploy/charts/openldap-0.1.0.tgz     -n server -f ldap-deploy/values-openldap.yaml     --wait
helm upgrade phpldapadmin ldap-deploy/charts/phpldapadmin-0.1.0.tgz -n server -f ldap-deploy/values-phpldapadmin.yaml --wait
kubectl apply -f ldap-deploy/manifests/    # 预期全部 unchanged
kubectl -n server get pod -o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount
# 预期:全部 true,RESTARTS 均为 0

6. 验证

6.1 条目完整性

kubectl -n server exec openldap-0 -- sh -c 'slapcat -F /etc/openldap/slapd.d -b "dc=rainchen,dc=com" 2>/dev/null | grep -c "^dn:"'            # 22
kubectl -n server exec openldap-0 -- sh -c 'slapcat -F /etc/openldap/slapd.d -b "dc=rainchen,dc=com" 2>/dev/null | grep -c "^userPassword::"'   # 8

6.2 管理员绑定与 LDAPS(容器内)

read -rs -p "LDAP admin password: " PW && PW_FILE=$(mktemp) && chmod 600 "$PW_FILE" \
  && printf '%s' "$PW" > "$PW_FILE" && unset PW
kubectl -n server cp "$PW_FILE" openldap-0:/tmp/pw

kubectl -n server exec openldap-0 -- ldapsearch -x -H ldap://localhost:3890 \
  -D "cn=admin,dc=rainchen,dc=com" -y /tmp/pw -b "dc=rainchen,dc=com" -s base dn   # result: 0 Success
kubectl -n server exec openldap-0 -- ldapsearch -x -H ldaps://localhost:6360 -o TLS_REQCERT=never \
  -D "cn=admin,dc=rainchen,dc=com" -y /tmp/pw -b "dc=rainchen,dc=com" -s base dn   # result: 0 Success

kubectl -n server exec openldap-0 -- rm -f /tmp/pw && rm -f "$PW_FILE"

6.3 对外端口(经 traefik)

kubectl -n server run ldapclient --image=osixia/openldap:2.6.10-alpha --restart=Never --command -- sleep 300
kubectl -n server cp "$PW_FILE" ldapclient:/tmp/pw       # 口令文件同上
kubectl -n server exec ldapclient -- ldapsearch -x -H ldap://192.168.31.10:389 \
  -D "cn=admin,dc=rainchen,dc=com" -y /tmp/pw -b "dc=rainchen,dc=com" -s sub "(objectClass=*)" dn | grep -c "^dn:"   # 22
kubectl -n server exec ldapclient -- ldapsearch -x -H ldaps://192.168.31.10:636 -o TLS_REQCERT=never \
  -D "cn=admin,dc=rainchen,dc=com" -y /tmp/pw -b "dc=rainchen,dc=com" -s base dn                                   # result: 0 Success
kubectl -n server delete pod ldapclient

for p in 389 636; do timeout 5 bash -c "echo > /dev/tcp/192.168.31.10/$p" && echo "$p 可连接"; done   # 执行端连通性
openssl s_client -connect 192.168.31.10:636 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -dates

6.4 管理界面

curl -sS -o /dev/null -w "%{http_code}\n" -H "Host: ldapadmin.rainchen.top" http://192.168.31.10/       # 302 → /login
curl -sS -o /dev/null -w "%{http_code}\n" -H "Host: ldapadmin.rainchen.top" http://192.168.31.10/login  # 200

6.5 备份

kubectl -n server get cronjob openldap-ldif-backup
kubectl -n server create job --from=cronjob/openldap-ldif-backup backup-check && sleep 25
kubectl -n server logs job/backup-check --tail=5      # 预期:导出完成,条目数=22
kubectl -n server delete job backup-check

7. 回滚

7.1 服务回退(切回旧实例)

旧 ldap 命名空间的三个 PVC(含数据)与留档 LDIF 均已保留,必要时可快速切回:

# 1) 在 ldap 命名空间重建(PVC 复用,数据仍在)
kubectl -n ldap create secret generic openldap-tls --from-file=... # 同上 5.3
kubectl -n ldap create configmap openldap-pre-upgrade-hook --from-file=startup.sh=ldap-deploy/pre-upgrade-startup.sh
kubectl -n ldap create configmap openldap-cron-process-hook --from-file=process.sh=ldap-deploy/cron-process-placeholder.sh
helm install openldap ldap-deploy/charts/openldap-0.1.0.tgz -n ldap -f ldap-deploy/values-openldap.yaml --wait
# 2) 摘除 server 命名空间路由,改在 ldap 命名空间创建(见 5.8,namespace 需同步改回)

7.2 traefik

helm rollback traefik <5.1 记录的 revision> -n server --wait
# 同步回退项目 values(移除 ports.ldap / ports.ldaps),否则下次 upgrade 会重新引入
kubectl -n server get svc traefik -o yaml | diff - ldap-deploy/traefik-svc-before.yaml && echo ROLLBACK_OK

7.3 完全拆除

# 留档【高危前备份】
kubectl -n server exec openldap-0 -- slapcat > ldap-deploy/rollback-export-$(date +%Y%m%d%H%M).ldif
helm uninstall phpldapadmin openldap -n server
kubectl -n server delete -f ldap-deploy/manifests/
kubectl -n server delete pvc conf-openldap-0 data-openldap-0 backups-openldap-0

【高危】nfs-csi-hdd 回收策略为 Delete:删除 PVC 会连同 NFS 后端 192.168.31.249:/srv/nfs/k8s-hdd/pvc-<uuid>/ 一并删除,不可恢复。 必须先在容器内完成 slapcat 留档。

7.4 回滚后验证

kubectl -n server get pod -l app.kubernetes.io/name=openldap     # 预期无 Pod 或已恢复
helm history traefik -n server | tail -3
kubectl get ingressroutetcp -A                                   # 路由归属符合预期

8. 风险说明

风险 说明 缓解
镜像缺陷 1:pre-upgrade 脚本缺执行位 /container/services/openldap-pre-upgrade/startup.sh 为 rw-r--r--,非 root 运行报 permission denied;既作为 chart 的 initContainer,也被 baseimage 在主容器内启动 → 崩溃循环 关闭 chart preUpgrade;用 ConfigMap 以 defaultMode: 0755 + subPath 覆盖该脚本,保留原始逻辑(升级镜像时需比对内容)
镜像缺陷 2:证书文件名不一致 chart 挂载 tls.crt/tls.key,镜像固定读取 cert.crt/cert.key(.env.bootstrap 内硬编码)→ slapd 启动即退出 弃用 chart 的 tlsSecret,改由 extraVolumes 的 secret items 以 cert.crt/cert.key/ca.crt 挂载
镜像缺陷 3:cron 服务要求 root process.sh 校验 id -u == 0,非 root 下 fatal 并无限重启刷日志 覆盖 openldap-cron/process.sh 为占位(保持进程存活、不启动 crond);备份由 5.8 的 CronJob 承担
缺陷 4:phpLDAPadmin chart 端口 chart 硬编码 containerPort 80,镜像 2.3.11 以 www-data(82) 运行、仅监听 8080(无法绑特权端口)→ 探针全失败 探针端口覆盖为 8080;自建 Service(targetPort 8080) + IngressRoute 替代 chart 的 Service/Ingress
缺陷 5:phpLDAPadmin 连接账号与登录属性未暴露 chart 0.1.0 的 ConfigMap 无 LDAP_USERNAME/LDAP_PASSWORD/LDAP_LOGIN_ATTR → 应用以匿名连接(匿名对本目录无读权限),认证时检索不到任何用户条目 → 任何账号都登录失败 本地定制 chart:ConfigMap 增加 LDAP_USERNAME/LDAP_LOGIN_ATTR;Deployment 用 secretKeyRef 注入 LDAP_PASSWORD;并加 checksum/config 注解使配置变更自动触发滚动重启(见 5.7.1)
phpLDAPadmin 不能用 rootdn 登录 应用须先检索到登录账号条目再校验密码;cn=admin,dc=rainchen,dc=com 是 rootdn 非条目,检索必为空;且现有用户 ACL 仅允许读少数属性 创建 uid=ldapadmin 条目并以 ACL 授予 manage(5.7.1);rootdn 权限不受 ACL 影响,仍可用命令行管理
schema 兼容性 旧数据以 posixGroup 为结构类;rfc2307bis 中它非 structural,导入必报 no structural object class provided 改用镜像自带 nis.ldif;nis 与 rfc2307bis 定义同名 objectClass,只能启用其一
镜像 tag 为 alpha osixia v2 仅有 2.6.10-alpha;chart 无 digest 字段 已记录运行镜像 digest;如需严格锁定,将镜像同步到内部 registry 后改 image.repository/tag
数据导入非幂等 slapadd 到非空库会报 already exists 导入前清空 data 并留档;导入后立即计数校验
LMDB 跑在 NFS(HDD) 依赖 mmap/文件锁,且该盘同时承载 4 台 VM 的 qcow2 mountOptions hard,timeo=600,retrans=3;每日 LDIF 备份;观察 NFS 超时告警,必要时迁移 local-ssd
单点入口 389/636 由 traefik 单副本 + hostPort 固定 k8s-master-01 客户端可备用集群内 ClusterIP;高可用需扩 traefik 副本与多节点 hostPort
备份为逻辑导出 CronJob 经 LDAP 导出,不含 operational 属性(entryUUID/entryCSN) 逻辑恢复足够;需保真时用 slapcat 手工导出
口令存放 values-openldap.yaml 含 {SSHA} 哈希(600);openldap-backup-pw Secret 含明文(供备份任务) 不得入库/外传;Secret 以 defaultMode: 0440 挂载
旧命名空间残留 ldap 命名空间保留 3 个 PVC(约 17Gi 声明容量)与 NFS 数据,仅作兜底 确认 server 环境稳定后,按 7.3 单独删除 PVC 释放

附录 A:交付物清单(ldap-deploy/)

文件 说明
SOP.md 本文件(本次变更的实施档案)
RUNBOOK.md 服务运行手册:概览、接入与凭据、速查表、日常操作、配置管理、备份与恢复、监控告警、故障处置、安全与容量
values-openldap.yaml OpenLDAP chart values(含口令哈希,权限 600)
values-phpldapadmin.yaml phpLDAPadmin chart values(host 指向 openldap.server.svc)
charts/openldap-0.1.0.tgz、charts/phpldapadmin-0.1.0.tgz 本地 Chart 包(离线可复现)
certs/ 自签证书与申请配置(CN/SAN 含 ldap.rainchen.com 与 192.168.31.10;tls.key 权限 600)
pre-upgrade-startup.sh、cron-process-placeholder.sh 覆盖镜像缺陷所用脚本源(对应两个 ConfigMap)
manifests/ldap-ingressroutes.yaml LDAP/LDAPS 的 IngressRouteTCP(namespace: server)
manifests/phpldapadmin-web.yaml phpLDAPAdmin 的 Service + IngressRoute(namespace: server)
manifests/openldap-backup-cronjob.yaml 每日 LDIF 备份 CronJob(namespace: server)
charts/phpldapadmin/ 本地定制 chart 目录(相对上游 0.1.0 改了 configmap/deployment/values 三处,见 5.7.1 与第 8 章)
ldapadmin-user.ldif 目录管理员条目定义 uid=ldapadmin(含 {SSHA} 哈希,权限 600)
acl-ldapadmin.ldif 授予该条目 manage 权限的 ACL 变更
rollback-config-202609191410.ldif、rollback-data-202609191410.ldif 建管理员条目/改 ACL 前的 conf 与数据留档
rollback-export-202609191327.ldif 迁移前库内全量导出的留档(含 operational 属性,22 条目)
traefik-values-before.yaml、traefik-manifest-before.yaml、traefik-svc-before.yaml traefik 变更前快照(回滚依据)

附录 B:命名空间迁移记录(2026-09-19)

由 ldap 迁移至 server,原因是与 traefik、gitlab 统一命名空间。

迁移方式:StatefulSet 的 PVC 属命名空间级资源,无法跨命名空间搬迁,因此在 server 重新部署实例并从留档 LDIF 恢复数据,随后切换 traefik 路由。

执行要点:

  1. 迁移前留档:kubectl -n ldap exec openldap-0 -- slapcat > ldap-deploy/rollback-export-202609191327.ldif(22 条目 + 8 个口令哈希)
  2. 交付清单内 namespace: ldap、openldap.ldap.svc.cluster.local 全部改为 server
  3. 在 server 重建 Secret(openldap-tls、openldap-backup-pw)与两个 ConfigMap
  4. helm install 两个 release(新的空 PVC 自动创建)→ 按 5.6 恢复数据(slapadd,验证 22 条目)
  5. 先删除 ldap 命名空间的 IngressRouteTCP/IngressRoute,再创建 server 命名空间的(避免同一 entryPoint 重复路由),此间 LDAP 中断约数秒
  6. 迁移后验证:389 查询 22 条目、636 LDAPS result: 0 Success、管理界面 /login HTTP 200;幂等复核 helm upgrade ×2 与 manifests apply ×3 全部零变更
  7. 旧实例处理:卸载 ldap 命名空间的两个 release,删除其 Secret/ConfigMap/自建 Service,保留三个 PVC 与 NFS 数据作为兜底

遗留:ldap 命名空间的 conf/data/backups-openldap-0 三个 PVC 仍 Bound,确认稳定后可删除释放(删除即连同 NFS 子目录一起删除)。

B.1 管理界面登录排障记录(2026-09-19)

现象:浏览器登录 ldapadmin.rainchen.top 报 "These credentials do not match our records.",即使输入正确的管理员口令。

排查过程(只读取证):

  1. 用留档口令做 LDAP 绑定 → result: 0 Success,LDAP 服务与口令本身正常
  2. 应用日志仅见启动瞬间的 Can't contact LDAP server,与本次登录无关
  3. 在应用容器内用 PHP 直连 LDAP:绑定成功、rootDSE 可读;但匿名查询 dc=rainchen,dc=com 返回 No such object
  4. 用应用自身的 LdapRecord 连接复现:retrieveByCredentials(['uid' => 'rain01']) → NULL(匿名无读权限);auth()->attempt(DN, pw) → true(直接绑定可行)

根因(三层):

层 问题 证据 处理
连接 chart 未提供 LDAP_USERNAME/LDAP_PASSWORD,应用以匿名连接,而匿名 ACL 为 to * by * none 匿名查询返回 0 条;注入服务账号后 retrieveByCredentials(uid=rain01) 立即返回条目 本地定制 chart,注入服务账号 + Secret 密码
登录属性 应用默认按 uid 检索(LDAP_LOGIN_ATTR 默认 uid);期间曾误设为 dn config('pla.login.attr') 与表单字段名随该变量变化 显式设为 uid,表单字段恢复 uid
账号模型 phpLDAPadmin 2.x 必须"先检索到条目再校验密码",rootdn 不是条目 retrieveByCredentials(dn=cn=admin,...) → NULL;上游文档亦要求使用目录内账号 创建 uid=ldapadmin 条目并授予 manage

验证:uid=ldapadmin 可全量读 23 条、可写条目;HTTP 模拟登录返回 302 → http://ldapadmin.rainchen.top/,首页 200。

回滚:ACL 与条目变更前已留档(rollback-config-*.ldif);如需撤销,删除该条目并用留档恢复 ACL(均以 rootdn 身份操作,rootdn 不受 ACL 限制)。


来源:/home/rain/Documents/k8s_ins/ldap-deploy/SOP.md | 最后更新:2026-09-19 16:00(由 build/sync.py 同步,请勿在站点侧修改)