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. 目标¶
server命名空间内openldap与phpldapadmin两个 release,Pod 全部 Ready。- 基线
dc=rainchen,dc=com,管理员cn=admin,dc=rainchen,dc=com,口令沿用旧环境。 - 22 个条目(1 后缀 + 5 ou + 8 posixGroup + 8 用户)全部导入,用户
{SSHA}口令哈希保留。 - 集群外经
k8s-master-01的 389 / 636 可访问 LDAP/LDAPS(经 traefik TCP 透传)。 http://ldapadmin.rainchen.top经 traefik 可访问 phpLDAPadmin。- conf/data/backups 三个卷落在
nfs-csi-hdd,每日 LDIF 逻辑备份生效。 - 重复执行部署类步骤零变更、无报错。
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 路由。
执行要点:
- 迁移前留档:
kubectl -n ldap exec openldap-0 -- slapcat > ldap-deploy/rollback-export-202609191327.ldif(22 条目 + 8 个口令哈希) - 交付清单内
namespace: ldap、openldap.ldap.svc.cluster.local全部改为server - 在
server重建 Secret(openldap-tls、openldap-backup-pw)与两个 ConfigMap helm install两个 release(新的空 PVC 自动创建)→ 按 5.6 恢复数据(slapadd,验证 22 条目)- 先删除
ldap命名空间的 IngressRouteTCP/IngressRoute,再创建server命名空间的(避免同一 entryPoint 重复路由),此间 LDAP 中断约数秒 - 迁移后验证:389 查询 22 条目、636 LDAPS
result: 0 Success、管理界面/loginHTTP 200;幂等复核helm upgrade×2 与manifests apply×3 全部零变更 - 旧实例处理:卸载
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.",即使输入正确的管理员口令。
排查过程(只读取证):
- 用留档口令做 LDAP 绑定 →
result: 0 Success,LDAP 服务与口令本身正常 - 应用日志仅见启动瞬间的
Can't contact LDAP server,与本次登录无关 - 在应用容器内用 PHP 直连 LDAP:绑定成功、rootDSE 可读;但匿名查询
dc=rainchen,dc=com返回No such object - 用应用自身的 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同步,请勿在站点侧修改)