Self-Service Password 1.8.2 Docker Compose 部署指南

Self-Service Password 1.8.2 与 OpenLDAP 的 Docker Compose 全链路部署实录:镜像选型与版本实查、目录初始化、SSP 对接、自助改密、密码策略校验、邮件重置与安全问题重置实测,附 keyphrase 强制检查、SMTP STARTTLS、ACL 权限三处排障。

Self-Service Password密码管理LDAP身份认证PHP 应用2026/10/24

Self-Service Password 1.8.2 Docker Compose 部署指南

目标:用 Docker Compose 把 Self-Service Password(下文简称 SSP)1.8.2 与其强依赖的 LDAP 目录(OpenLDAP)一次拉起,完成目录初始化、SSP 对接、自助改密、密码策略校验、安全问题重置与邮件重置链路的全流程验证。本文全部命令与回显均在 Ubuntu 22.04.4 LTS(x86_64)+ Docker 29.8.1 + Docker Compose v5.5.1 环境实测通过,SSP 与 OpenLDAP 版本、页面响应、LDAP 落库结果均为实测回显。

一、环境信息

本文实测环境的关键信息如下,读者复现时请对照自己的版本:

  • 操作系统:Ubuntu 22.04.4 LTS(x86_64)
  • Docker:29.8.1;Docker Compose:v5.5.1
  • SSP:ltbproject/self-service-password:1.8.2(内部基于 PHP 8.5.12 + Apache,实测 php -v 回显 PHP 8.5.12 (cli) (built: Oct 21 2026 15:49:40) (NTS))
  • OpenLDAP:osixia/openldap:latest(实测 slapd -V 回显 $OpenLDAP: slapd 2.4.57+dfsg-1~bpo10+1,即 slapd 2.4.57)
  • 邮件测试通道:axllent/mailpit:v1.31.6(实测 /mailpit --version 回显 /mailpit v1.31.6 compiled with go1.27.2)
  • 网络规划:独立 bridge 网络 self-service-password-182-net,子网 10.89.76.0/24
  • 对外端口:SSP Web 服务映射宿主机 8138 端口(HTTP)
  • 目录基准:dc=example,dc=org,管理员 cn=admin,dc=example,dc=org,测试用户 alice、bob
  • 相关背景认知见《Self-Service Password 1.8.2 介绍与选型指南》

二、镜像选型说明

2.1 SSP 官方镜像实查

SSP 官方维护 Docker 镜像 ltbproject/self-service-password,发布在 Docker Hub。本文拉取并实测的 1.8.2 tag 关键元数据如下(来自镜像 label,实测 docker inspect 回显):

label值
org.opencontainers.image.base.namephp:8.5.12-apache-trixie
org.opencontainers.image.descriptionSelf Service Password is a simple PHP application that allows users to change their password on an LDAP directory
org.opencontainers.image.documentationhttps://self-service-password.readthedocs.io/
org.opencontainers.image.licensesGPL-2+
org.opencontainers.image.sourcehttps://github.com/ltb-project/self-service-password/
org.opencontainers.image.vendorLTB-project.org
org.opencontainers.image.version1.8.2

容器内应用代码同样自带版本号(实测回显):

docker exec self-service-password-182-web grep -m1 'version' /var/www/htdocs/index.php
$version = "1.8.2";

页面页脚亦显示版本(实测):LDAP Tool Box Self Service Password - version 1.8.2。三处证据互相印证,镜像内容与 1.8.2 版本一致。

PHP 扩展方面实测(php -m):ldap 扩展已内置,sockets 扩展没有内置——这会影响后续自写 SMTP 测试桩的可行性(见 4.3 节排障)。

2.2 OpenLDAP 依赖镜像实查

SSP 本身不带目录服务,必须对接一个 LDAPv3 目录(OpenLDAP、Active Directory、389 DS 等均可,官方文档 Presentation 章节列出兼容清单)。本文选 osixia/openldap 做目录端,原因:社区使用最广、bootstrap LDIF 初始化机制成熟。实测版本:

docker exec self-service-password-182-openldap slapd -V
@(#) $OpenLDAP: slapd 2.4.57+dfsg-1~bpo10+1 (Jan 30 2021 06:59:51) $

⚠️ 注意:osixia/openldap 在镜像加速源上只有 latest tag 可拉取,1.5.8 等版本化 tag 实测返回 404(MANIFEST_UNKNOWN)。本文实际使用 osixia/openldap:latest 并以 slapd -V 实查为准(2.4.57),不臆造版本号。

2.3 非容器渠道(deb/rpm/tar)并陈

不使用容器部署时,SSP 1.8.2 的官方发行渠道(与《Self-Service Password 1.8.2 介绍与选型指南》溯源结论一致,查询日期 2026-10-24):

  • Debian/Ubuntu:LTB 官方 apt 仓库(ltb-project.org 提供源配置),apt install self-service-password;
  • RHEL/CentOS:LTB 官方 rpm 仓库,yum install self-service-password;
  • tar.gz 归档:LTB 下载页 ltb-project.org/download.html 提供源码/发行包,解压到 Web 根目录配合 PHP-FPM/Apache 部署;
  • Docker:即本文的 ltbproject/self-service-password。

容器方式的优势是 PHP 运行时、扩展(ldap)、Web 服务器全部预置好,开箱即用;deb/rpm 方式适合已有 Apache + PHP 平台的复用场景。

三、前置条件

  1. 一台可运行 Docker 的 Linux 主机(本文 Ubuntu 22.04.4),Docker 与 Compose 插件已装好;
  2. 宿主机目标端口空闲(本文用 8138,ss -ltn 确认未被占用);
  3. 目标网段未被现有网络占用(本文子网 10.89.76.0/24,创建前用 docker network inspect 复核);
  4. 规划好目录基准 DN 与管理员密码(本文 dc=example,dc=org、管理员密码 q1w2e3r4);
  5. 若需要真实邮件投递,准备一个 SMTP 服务器;本文用 mailpit 在同栈内起一个只收不发 SMTP sink,用于验证邮件链路。

四、部署步骤

4.1 Compose 编排文件

部署目录约定为 /opt/self-service-password-1.8.2/,其下 docker-compose.yml 全文如下(实测可直接启动):

name: self-service-password-182-stack
services:
  openldap:
    image: osixia/openldap:latest
    container_name: self-service-password-182-openldap
    hostname: openldap
    environment:
      LDAP_ORGANISATION: "Example Org"
      LDAP_DOMAIN: "example.org"
      LDAP_BASE_DN: "dc=example,dc=org"
      LDAP_ADMIN_PASSWORD: "q1w2e3r4"
      LDAP_CONFIG_PASSWORD: "q1w2e3r4"
      LDAP_RFC2307_SCHEMA: "false"
    volumes:
      - ./openldap/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro
    command: ["--copy-service"]
    networks:
      - sspnet

  ssp:
    image: ltbproject/self-service-password:1.8.2
    container_name: self-service-password-182-web
    ports:
      - "8138:80"
    volumes:
      - ./ssp/config.inc.local.php:/var/www/conf/config.inc.local.php:ro
    depends_on:
      - openldap
    networks:
      - sspnet

  mailsink:
    image: axllent/mailpit:v1.31.6
    container_name: self-service-password-182-mailsink
    hostname: mailsink
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: "1"
      MP_SMTP_AUTH_ALLOW_INSECURE: "1"
    networks:
      - sspnet

networks:
  sspnet:
    name: self-service-password-182-net
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.76.0/24

💡 说明:compose 顶层必须显式写 name:(本文 self-service-password-182-stack),避免多套部署共用目录前缀时误删其他栈。

4.2 目录初始化 LDIF

./openldap/ldif/ 下放两个 bootstrap LDIF,OpenLDAP 首次启动时自动导入。01-base.ldif(组织单元):

dn: ou=people,dc=example,dc=org
objectClass: organizationalUnit
ou: people

dn: ou=services,dc=example,dc=org
objectClass: organizationalUnit
ou: services

02-users.ldif(测试用户,初始密码均为 q1w2e3r4):

dn: uid=alice,ou=people,dc=example,dc=org
objectClass: inetOrgPerson
uid: alice
cn: Alice
sn: Wang
mail: alice@example.org
mobile: 13800000001
userPassword: q1w2e3r4

dn: uid=bob,ou=people,dc=example,dc=org
objectClass: inetOrgPerson
uid: bob
cn: Bob
sn: Li
mail: bob@example.org
mobile: 13800000002
userPassword: q1w2e3r4

⚠️ 注意:测试用户同时带 mail 属性——SSP 的邮件重置与改密通知都依赖从目录读取用户邮箱,缺 mail 会导致 sendtoken 流程找不到投递地址。

另有 06-acl.ldif(ACL 调整,安全问题重置链路必需,否则会报 Insufficient access,见 4.5 与十一.3):

dn: olcDatabase={1}mdb,cn=config
changetype: modify
delete: olcAccess
-
add: olcAccess
olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcAccess: to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: to attrs=info,objectClass by self write by dn="cn=admin,dc=example,dc=org" write by * none
olcAccess: to * by self read by dn="cn=admin,dc=example,dc=org" write by * none

SSP 的配置覆盖文件 ./ssp/config.inc.local.php 全文见第七节。

4.3 启动栈

docker compose up -d

实测启动回显:

 Network self-service-password-182-net Created
 Container self-service-password-182-openldap Created
 Container self-service-password-182-mailsink Created
 Container self-service-password-182-web Created
 Container self-service-password-182-mailsink Started
 Container self-service-password-182-openldap Started
 Container self-service-password-182-web Started

状态确认:

docker compose ps --format '{{.Name}} {{.Status}}'
self-service-password-182-mailsink Up 16 seconds (healthy)
self-service-password-182-web Up About a minute
self-service-password-182-openldap Up About a minute

排障实录(mailsink 首选方案被否):最初计划复用 SSP 镜像跑一个自写 PHP SMTP sink,结果容器退出,docker logs 实测回显:

PHP Fatal error:  Uncaught Error: Call to undefined function socket_create() in /work/smtp_sink.php:7

定位:docker exec self-service-password-182-web php -m | grep -i socket 无输出——官方 SSP 镜像未编译 sockets 扩展。解决:改用本地已有 axllent/mailpit:v1.31.6 作为 SMTP sink(收信验证用途完全等价),即上文 compose 中的 mailsink 服务。

4.4 验证:SSP 连接 LDAP 与页面可用性

  1. 目录初始化结果(在 OpenLDAP 服务器本机执行):
ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D cn=admin,dc=example,dc=org -w q1w2e3r4 '(uid=*)' uid mail

实测回显(节选):

dn: uid=alice,ou=people,dc=example,dc=org
uid: alice
mail: alice@example.org

dn: uid=bob,ou=people,dc=example,dc=org
uid: bob
mail: bob@example.org

# numEntries: 2
  1. ACL 落库确认(olcDatabase={1}mdb 的 olcAccess,实测 ldapsearch -b cn=config 回显节选):
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcAccess: {1}to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: {2}to attrs=info,objectClass by self write by dn="cn=admin,dc=example,dc=org" write by * none
olcAccess: {3}to * by self read by dn="cn=admin,dc=example,dc=org" write by * none
  1. SSP Web 页面(宿主机 8138 端口):
curl -s -o /tmp/ssp_page.html -w 'http=%{http_code} size=%{size_download}\n' http://localhost:8138/
grep -o '<title>[^<]*</title>' /tmp/ssp_page.html

实测回显:

http=200 size=4084
<title>Self service password</title>

至此「SSP → LDAP」依赖链全部打通:目录里有用户、SSP 页面可访问、SSP 配置指向 ldap://openldap:389。

4.5 全链路实测:自助改密 + 策略校验 + 邮件通知

以下操作全部通过 HTTP 接口实测(等价于浏览器操作)。

(1) 弱密码被策略拦截。alice 以旧密码 q1w2e3r4 登录改密表单,提交过弱新密码 abc123:

curl -s -c /tmp/ssp_a.jar -o /dev/null 'http://localhost:8138/index.php?action=change'
curl -s -b /tmp/ssp_a.jar -o /tmp/t1_weak.html \
  -d 'login=alice' --data-urlencode 'oldpassword=q1w2e3r4' \
  --data-urlencode 'newpassword=abc123' --data-urlencode 'confirmpassword=abc123' \
  'http://localhost:8138/index.php?action=change'

页面实测返回(alert 区域):

Your password does not have enough special characters

(2) 合规密码改密成功。提交 N3wPass!2026:

RESULT: Your password was changed

落库验证(SSHA 散列由目录端生成,实测 ldapsearch (uid=alice) userPassword 回显):

userPassword:: e1NTSEF9cGVBNks4bDBHanhKbVVlL21sRmFCcTQyaER6MFUvZFk=

base64 解码首段为 {SSHA}…,确认目录端 SSHA 散列。新旧密码双向验证:

ldapwhoami -x -H ldap://localhost -D uid=alice,ou=people,dc=example,dc=org -w q1w2e3r4
ldap_bind: Invalid credentials (49)
ldapwhoami -x -H ldap://localhost -D uid=alice,ou=people,dc=example,dc=org -w 'N3wPass!2026'
dn:uid=alice,ou=people,dc=example,dc=org

旧密码立即失效、新密码认证通过。

(3) 改密通知邮件。改密后 mailpit 实测收到(mailpit API 回显):

total: 1
TO: alice@example.org | SUBJ: Your password has been changed

⚠️ 注意:首次实测邮件未发出,docker logs 报 send_mail: SMTP connect() failed.。根因与修复见 4.6 与十一.2。

4.6 邮件重置与安全问题重置链路实测

(1) 邮件 token 重置全链路。GET index.php?action=sendtoken 后提交 login=alice(需携带页面下发的 formtoken 隐藏域,实测缺失时返回 Missing token):

RESULT: If the account exists, a confirmation email has been sent to the associated email address

mailpit 收到第二封邮件(实测):

SUBJ: Reset your password

邮件正文中的重置链接(实测原文节选):

http://ssp.example.com/?action=resetbytoken&token=3vUCAAkWZC9OyfkK8KEpX1EeY5bUBnGupaPmfRf8NB%2FvkZfP5ZmJaXe7YmpnqBCUvNBI%2BYRTir%2FcA1UYQIZXSL8U4ZfleNVl611WqpGjc5wsRv7e72v6RgZGaqkGvVNFF7Z2SmiMtu0qBxeveQ3Pm6F80ZX8z0%2F6vvJNUFk69joKw6x7G7wT5hLbHtQtu4rVuwPlCg%3D%3D

💡 说明:链接主机名来自 $reset_url 配置(默认 http://ssp.example.com),生产环境必须改成对外可达的真实地址;token 参数为加密串,与发送时的一致性由 keyphrase 保证。

打开重置链接后表单自带 token 隐藏域与已锁定的用户名,提交新密码 R3s3t!Ok77x:

RESULT: Your password was changed

双向验证(实测):新密码 ldapwhoami 返回 dn:uid=alice,...;旧密码 N3wPass!2026 返回 Invalid credentials (49)。

(2) 安全问题重置全链路。先登录设置答案(index.php?action=setquestions,用当前密码鉴权,问题选 What is your favorite color?,答案 teal):

SET RESULT: Your answer has been registered

落库验证(实测 ldapsearch (uid=alice) info objectClass 节选):

objectClass: inetOrgPerson
objectClass: extensibleObject
info: 3vUCACQAzkDYd9EtvhkWagaFWLMkQ/mpTC2xpJuQ3J6zBve+L2siiUWGfnyPxDYob7EoDUgP

答案以 keyphrase 加密后写入 info 属性,同时条目自动追加 extensibleObject 辅助类(对应 $answer_objectClass 配置)。随后走 index.php?action=resetbyquestions:

  • 正确答案 teal + 新密码 Q9Reset!Fine2 → 实测返回 Your password was changed,且 ldapwhoami -w 'Q9Reset!Fine2' 认证通过;
  • 错误答案 → 实测返回 Your answer is incorrect,密码保持不变。

至此 B 篇要求的四条实测链路(登录改密、策略校验、安全问题重置、邮件通知/邮件重置)全部实测通过。

五、数据持久化说明

本文 compose 的持久化策略与生产建议:

数据本文栈生产建议
LDAP 目录数据(/var/lib/ldap)未挂载(容器内)挂载 named volume 或 host path,避免目录数据随实例销毁
LDAP 配置(/etc/ldap/slapd.d)未挂载同上;ACL 变更一并持久化
SSP 应用配置./ssp/config.inc.local.php 只读挂载同本文(配置文件本身即持久化)
SSP 会话/缓存容器内 /var/lib/php单实例可不挂载;多实例需共享存储

⚠️ 注意:本文为验证型部署,LDAP 数据未落盘是有意为之(重建即恢复初始测试数据)。生产部署务必给 OpenLDAP 加数据卷,并配合定期 slapcat 备份。

六、环境变量表

6.1 OpenLDAP(osixia/openldap)常用环境变量(本文实测所用)

变量本文值作用
LDAP_ORGANISATIONExample Org组织名(o 属性)
LDAP_DOMAINexample.org域名,推导基准 DN
LDAP_BASE_DNdc=example,dc=org显式指定基准 DN
LDAP_ADMIN_PASSWORDq1w2e3r4根 DN(cn=admin,<base>)密码
LDAP_CONFIG_PASSWORDq1w2e3r4cn=admin,cn=config 配置管理员密码
LDAP_RFC2307_SCHEMAfalse是否启用 RFC2307bis 辅助 schema
command--copy-service启用 bootstrap LDIF 复制执行机制

6.2 mailpit(mailsink)环境变量

变量值作用
MP_SMTP_AUTH_ACCEPT_ANY1接受任意 SMTP 认证凭据
MP_SMTP_AUTH_ALLOW_INSECURE1允许明文认证(仅测试环境)

SSP 侧无环境变量,一切运行参数走 config.inc.local.php(见第七节)。

七、配置参数说明

SSP 支持在 /var/www/conf/config.inc.local.php 覆盖默认配置(官方主配置 config.inc.php 不建议直接改,升级会被覆盖)。本文实测用的 config.inc.local.php 全文:

<?php
# LDAP directory
$ldap_url = "ldap://openldap:389";
$ldap_starttls = false;
$ldap_binddn = "cn=admin,dc=example,dc=org";
$ldap_bindpw = "q1w2e3r4";
$ldap_base = "dc=example,dc=org";
$ldap_login_attribute = "uid";
$ldap_fullname_attribute = "cn";
$ldap_filter = "(&(objectClass=inetOrgPerson)(uid={login}))";

# Password handling
$ad_mode = false;
$hash = "SSHA";
$ldap_use_exop_passwd = true;
$pwd_show_policy = "always";
$pwd_show_policy_pos = "above";

# Password policy (lab)
$pwd_min_length = 8;
$pwd_max_length = 64;
$pwd_min_lower = 1;
$pwd_min_upper = 1;
$pwd_min_digit = 1;
$pwd_min_special = 1;
$pwd_special_chars = "^a-zA-Z0-9";
$pwd_no_reuse = true;
$pwd_diff_login = true;
$pwd_complexity = 0;

# Features
$use_questions = true;
$use_tokens = true;
$use_sms = false;
$use_captcha = false;

# Mail
$mail_from = "ssp@example.org";
$mail_from_name = "Self Service Password";
$mail_address_use_ldap = true;
$notify_on_change = true;
$mail_smtp_host = 'mailsink';
$mail_smtp_port = 1025;
$mail_smtp_auth = false;
$mail_smtp_secure = '';
$mail_smtp_autotls = false;

# Tokens
$token_lifetime = "3600";
# 1.8.2 强制要求自定义 keyphrase(出厂值 secret 会拒绝服务)
$keyphrase = "88eb2fee3d7b4d1dbbe99573a8be9972";

参数分组说明(全部实测生效):

分组参数说明
LDAP 连接$ldap_url / $ldap_binddn / $ldap_bindpw / $ldap_base目录地址与搜索绑定账号;本文指向同栈 ldap://openldap:389
LDAP 匹配$ldap_login_attribute / $ldap_filter登录名映射 uid、过滤 inetOrgPerson
密码存储$hash=SSHA / $ldap_use_exop_passwd=true目录端散列 + LDAP EXOP PASSWD 扩展改密(实测落库 {SSHA} 散列)
密码策略$pwd_min_length=8 等长度 8-64、四类字符各至少 1、禁与登录名相同/禁复用;实测弱密码被拦、合规密码通过
功能开关$use_questions / $use_tokens / $use_sms=false安全问题与邮件 token 开启;短信通道无网关,显式关闭
邮件$mail_smtp_host=mailsink / $mail_smtp_port=1025 / $mail_smtp_secure=''SMTP 指向同栈 mailpit;明文投递(排障结论见十一.2)
Token$token_lifetime=3600 / $keyphrasetoken 有效期 3600 秒;keyphrase 为加密密钥(必须改)

💡 技术拓展:SSP 的改密写入口有三种——普通 Modify 操作(先算好散列再 replace userPassword)、LDAP EXOP PASSWD 扩展(RFC 3062,明文提交由服务端散列)、AD 模式(UnicodePwd 特殊编码)。$ldap_use_exop_passwd=true 时优先走 EXOP,OpenLDAP 服务端会按自身策略散列存储,这也是实测落库 SSHA 的原因。三者在密码策略执行点上有差异:EXOP 可触发服务端 ppolicy 校验,普通 Modify 则完全由 SSP 侧策略把关。

八、配置文件挂载

容器内路径宿主路径说明
/var/www/conf/config.inc.local.php./ssp/config.inc.local.phpSSP 配置覆盖文件,只读挂载
/container/service/slapd/assets/config/bootstrap/ldif/custom./openldap/ldifOpenLDAP bootstrap LDIF(含 ACL 调整),配合 --copy-service 首次启动执行

修改配置后的生效方式:

docker compose restart ssp

⚠️ 注意:bootstrap LDIF 只在目录数据初始化时执行一次。若目录数据卷已初始化,改动 ACL LDIF 需要 docker compose up -d --force-recreate openldap(未挂数据卷时等价重建)或手工 ldapmodify 到 cn=config。

九、等价 docker run

不用 Compose 时,三个服务等价的 docker run 命令(网络需先建):

docker network create --subnet 10.89.76.0/24 self-service-password-182-net

docker run -d --name self-service-password-182-openldap \
  --hostname openldap --network self-service-password-182-net \
  -e LDAP_ORGANISATION="Example Org" -e LDAP_DOMAIN="example.org" \
  -e LDAP_BASE_DN="dc=example,dc=org" -e LDAP_ADMIN_PASSWORD="q1w2e3r4" \
  -e LDAP_CONFIG_PASSWORD="q1w2e3r4" \
  -v /opt/self-service-password-1.8.2/openldap/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro \
  osixia/openldap:latest --copy-service

docker run -d --name self-service-password-182-mailsink \
  --hostname mailsink --network self-service-password-182-net \
  -e MP_SMTP_AUTH_ACCEPT_ANY=1 -e MP_SMTP_AUTH_ALLOW_INSECURE=1 \
  axllent/mailpit:v1.31.6

docker run -d --name self-service-password-182-web \
  --network self-service-password-182-net -p 8138:80 \
  -v /opt/self-service-password-1.8.2/ssp/config.inc.local.php:/var/www/conf/config.inc.local.php:ro \
  ltbproject/self-service-password:1.8.2

⚠️ 注意:SSP 容器启动依赖 OpenLDAP 已就绪,手工 run 时先起 openldap 再起 web;Compose 的 depends_on 只保证启动顺序,首启重试机制由 SSP 内置(连接失败页面会提示,重试即可)。

十、常用运维命令

# 栈状态
docker compose ps
# 跟踪 SSP 日志(含邮件发送、LDAP 错误的 php:notice 行)
docker compose logs -f ssp
# 重启 SSP(改配置后)
docker compose restart ssp
# 目录数据检查
ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D cn=admin,dc=example,dc=org -w q1w2e3r4 '(uid=*)' uid
# ACL 检查(cn=config)
ldapsearch -x -H ldap://localhost -b cn=config -D cn=admin,cn=config -w q1w2e3r4 '(olcDatabase={1}mdb)' olcAccess
# mailpit 收件箱(API)
docker exec self-service-password-182-mailsink wget -qO- 'http://localhost:8025/api/v1/messages?limit=5'
# 镜像版本核对
docker exec self-service-password-182-web grep -m1 'version' /var/www/htdocs/index.php
# 停止与清理
docker compose down
docker compose down -v   # 连同数据卷

十一、常见问题

Q1:页面报 "Token encryption requires a random string in keyphrase setting",改密/重置表单不渲染? 1.8.2 对 $keyphrase 做了强制安全检查:默认值 secret 不被接受,首页直接报上述错误且功能表单不渲染。解决:在 config.inc.local.php 设置自定义随机串(如 openssl rand -hex 16 生成的 88eb2fee3d7b4d1dbbe99573a8be9972),docker compose restart ssp 后恢复。keyphrase 同时用于 token 与安全问题答案的加密,一旦有存量数据不得随意更换。

Q2:改密成功但收不到通知邮件,日志报 "send_mail: SMTP connect() failed."? 官方默认 $mail_smtp_secure = 'tls' 且 $mail_smtp_autotls = true,PHPMailer 会尝试 STARTTLS;对无 TLS 的内网 SMTP(本文 mailpit)握手失败即报连接失败(实测 docker logs 回显:[php:notice] send_mail: SMTP connect() failed.、Error while sending change email to alice@example.org (user alice))。解决:内网明文 SMTP 环境显式 $mail_smtp_secure = ''; 与 $mail_smtp_autotls = false;,重启后实测邮件到达(mailpit total: 1,主题 Your password has been changed)。

Q3:安全问题答案保存失败,日志报 "LDAP - Modify answer (error 50 (Insufficient access)"? 两层原因叠加(均为实测复现):

  1. SSP 默认以用户自身身份写答案,而 OpenLDAP 默认 ACL 只给 self 写 userPassword 的权限,写 info 被拒(error 50)——需按 4.2 节 06-acl.ldif 给 self 开放 info,objectClass 写权;
  2. 即使放开 ACL,还会暴露第二层错误:info 属性不在 inetOrgPerson 的允许属性表内(实测 alice 身份 ldapmodify 回显 ldap_modify: Object class violation (65) / attribute 'info' not allowed)——SSP 的机制是同时给条目追加 extensibleObject 辅助类($answer_objectClass = "extensibleObject"),因此 ACL 必须同时覆盖 objectClass 的写权。两步都完成后实测答案写入成功(info 出现加密串、objectClass 多出 extensibleObject)。

Q4:sendtoken 提交返回 "Missing token"? 1.8.x 的所有表单都带 formtoken 隐藏域(防 CSRF,实测 120 秒有效期,日志回显 generated form token: ... valid for 120 s)。脚本化调用必须先 GET 表单页、提取 formtoken 再随 POST 提交;浏览器正常操作不受影响。

Q5:osixia/openldap 版本化 tag 拉不到? 部分镜像加速源只有 latest 可用(1.5.8 等 tag 返回 404)。建议:拉 latest 后用 slapd -V 实查版本并记录(本文 2.4.57),或改用有完整版本 tag 的 OpenLDAP 镜像源。

Q6:邮件重置链接的主机名是 ssp.example.com? 链接由 $reset_url 生成,默认值即 http://ssp.example.com(实测邮件正文)。生产环境改成对外地址,如 $reset_url = "https://ssp.example.org";。

十二、参考链接