Self-Service Password 1.8.2 安全加固指南

Self-Service Password 1.8.2 安全加固指南:专用只读绑定账号与 ACL 最小权限、密码策略收紧、token 时效、nginx TLS 反代、审计日志 JSON 落盘、文件权限与服务降权十项加固全部实测,含 ACL 误伤改密链路排障实录。

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

Self-Service Password 1.8.2 安全加固指南

目标:把 Self-Service Password(下文简称 SSP)1.8.2 从「能跑」收敛到「可上生产」:覆盖目录侧最小权限绑定账号、SSP 防护开关(限流/验证码/信息脱敏)、TLS 传输链(LDAP/HTTPS/SMTP 三段)、网络暴露面收敛、审计日志与服务降权。所有加固项均在 Ubuntu 22.04.4 + SSP 1.8.2 + OpenLDAP 2.4.57 + nginx TLS 反代环境实测验证,加固前后行为差异均有实测回显。

一、环境信息

  • 操作系统: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,进程属主 www-data 实查确认)
  • 目录端:osixia/openldap:latest(slapd 2.4.57),基准 DN dc=example,dc=org
  • TLS 反代:nginx:alpine + 自签证书(RSA 2048,TLSv1.2/1.3,对外 8443)
  • 测试用户:alice、bob(inetOrgPerson);专用搜索账号 cn=ssp-bind,ou=services,dc=example,dc=org
  • 栈网络:self-service-password-182-net(10.89.76.0/24),SSP 宿主端口 8138、TLS 端口 8443
  • 前置部署与配置全集见《Self-Service Password 1.8.2 Docker Compose 部署指南》《Self-Service Password 1.8.2 配置参数详解》

二、身份认证与访问控制

2.1 SSP 自身的认证与防滥用面

SSP 没有管理员后台,所有功能页面面向最终用户,其防滥用机制由以下配置承载(均为 1.8.2 实测生效项):

加固项配置加固前(默认)加固后(推荐)实测证据
表单防 CSRF内置 formtoken默认开启保持缺失 formtoken 的 POST 返回 Missing token;日志显示 token 有效期 120 秒
尝试限流$use_ratelimitfalsetrue + 阈值同一用户连错 2 次后第 3 次返回「太快了!请稍后再试(如果真的是您手动操作)」
验证码$use_captchafalsetrue(对公网)InternalCaptcha 无外部依赖;FriendlyCaptcha/ReCaptcha 可选
用户枚举防护$obscure_usernotfound_sendtokentrue(默认开)保持sendtoken 对不存在用户返回与存在用户一致的「如果用户信息正确,您将收到重置密码邮件」
登录名注入防护$login_forbidden_chars"*()&|"保持过滤 LDAP filter 特殊字符,防注入
密码黑名单$pwd_forbidden_words / $pwd_forbidden_ldap_fields空填充见 2.3 实测
token 加密$crypt_tokens + $keyphrasekeyphrase=secret(拒绝服务)随机串默认值直接报错拒绝服务,实测见部署指南十一.1

2.2 目录侧最小权限绑定账号(核心加固项)

默认配置里 SSP 用目录管理员(cn=admin,即 rootdn)做搜索绑定——rootdn 无视一切 ACL,权限面过大。加固方案:建专用只读账号 ssp-bind,改密走用户自身凭据($who_change_password = "user" 默认语义)。

第一步:创建专用账号(bootstrap LDIF,部署指南 4.2 节机制):

dn: cn=ssp-bind,ou=services,dc=example,dc=org
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: ssp-bind
description: SSP read-only search account
userPassword: B1nd!0nly2026

第二步:收紧版 ACL(完整 06-acl.ldif):

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 by self write by dn="cn=admin,dc=example,dc=org" write by * none
olcAccess: to attrs=objectClass by self write by dn="cn=ssp-bind,ou=services,dc=example,dc=org" read by dn="cn=admin,dc=example,dc=org" write by * none
olcAccess: to dn.subtree="ou=people,dc=example,dc=org" by dn.exact="cn=ssp-bind,ou=services,dc=example,dc=org" read by self read 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

该 ACL 的权限语义:ssp-bind 只能读 ou=people 子树的用户属性(含 objectClass——搜索过滤必需),读不到 userPassword(散列不外泄)、读不到 info(安全问题密文不外泄)、写不了任何条目;用户自身(self)只能改自己的 userPassword(改密)与 info/objectClass(注册安全问题答案)。

第三步:SSP 切换到专用账号并收窄搜索范围:

$ldap_binddn = "cn=ssp-bind,ou=services,dc=example,dc=org";
$ldap_bindpw = "B1nd!0nly2026";
$ldap_base = "ou=people,dc=example,dc=org";   # 搜索基准收窄到人员 OU

第四步:最小权限验证(加固效果实测):

  1. 专用账号 bind 成功,但读不到密码散列(实测 ldapsearch (uid=alice) userPassword 回显仅有 search: 2 状态行,无 userPassword 属性返回);
  2. 专用账号写操作被拒(实测回显):
ldap_modify: Insufficient access (50)
modifying entry "uid=bob,ou=people,dc=example,dc=org"
  1. 切换后 SSP 全流程不受影响(实测):alice 以旧密码登录改密成功(「您的密码已修改」)、邮件重置发送成功(「如果用户信息正确,您将收到重置密码邮件」)。

⚠️ 注意(实测排障实录):收紧 ACL 过程中踩到两个坑,都是「加固误伤」的典型案例: ① 改密报「用户名或密码不正确」,日志 LDAP - User alice not found——SSP 改密页的搜索 filter 是 (&(objectClass=inetOrgPerson)(uid=alice)),filter 中出现的 objectClass 属性同样要求读权;第一版 ACL 把 objectClass 连同 info 一起 by * none,导致含 objectClass 条件的搜索对 ssp-bind 返回零条目(实测 PHP 复现:filter (uid=alice) 返回条目、(&(objectClass=inetOrgPerson)(uid=alice)) 返回空)。修复:单独给 objectClass 开 ssp-bind 只读(上文 ACL 第 4 条),info 保持完全不暴露。 ② 搜索基准收窄:$ldap_base 直接写 ou=people,dc=example,dc=org 而不是整棵目录根——ssp-bind 对根节点没有 search 权,搜索基准落在有权限的子树上,同时缩小信息暴露面。

2.3 密码策略收紧项

策略全集见《Self-Service Password 1.8.2 配置参数详解》5.3 节,加固推荐组合(本文实测值):

$pwd_min_length = 12;          # 默认 0,收紧到 12
$pwd_max_length = 128;
$pwd_min_lower = 1; $pwd_min_upper = 1; $pwd_min_digit = 1; $pwd_min_special = 1;
$pwd_diff_last_min_chars = 3;  # 新旧密码至少差 3 个字符
$pwd_no_reuse = true;          # 默认开
$pwd_diff_login = true;        # 默认开
$pwd_forbidden_words = array("example", "password", "qwer");
$pwd_forbidden_ldap_fields = array('cn', 'sn', 'mail');

$pwd_forbidden_ldap_fields 实测:alice 的条目 sn: Wang,提交含姓名片段的新密码 Wang!Pass2026 → 页面返回实测回显:您的密码包含来自您的 LDAP 条目中的内容,拒绝生效。该项把「姓名/邮箱进密码」这类社工弱点直接关死。

三、传输加密(TLS)

SSP 体系有三段明文风险,逐段实测加固:

3.1 用户 → SSP:nginx TLS 反代(实测)

SSP 容器自身只提供 HTTP。对外用 nginx 做 TLS 终结,配置(自签证书 openssl req -x509 -newkey rsa:2048 -nodes -days 30 -subj '/CN=ssp.example.org' 生成,生产换 CA 证书):

server {
    listen 8443 ssl;
    server_name ssp.example.org;
    ssl_certificate     /etc/nginx/tls/ssp.crt;
    ssl_certificate_key /etc/nginx/tls/ssp.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    location / {
        proxy_pass http://self-service-password-182-web:80;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

实测:curl -sk https://localhost:8443/ 返回 HTTP/1.1 200 OK、页面标题 自助密码服务;同端口打明文 HTTP 返回 400(TLS 端口拒绝明文)。配合 $reset_url = "https://ssp.example.org/",邮件重置链接全程 HTTPS。

💡 说明:X-Forwarded-Proto 头务必透传——SSP 官方注释给出反代场景 $reset_url 按 $_SERVER['HTTP_X_FORWARDED_PROTO'] 动态推导的写法,缺头会导致重置链接退化为 http。

3.2 SSP → 目录:LDAP STARTTLS / LDAPS

内网目录也建议加密。SSP 侧 $ldap_starttls = true 在 bind 前发起 STARTTLS;目录端需配好 TLS 证书(osixia 镜像支持 LDAP_TLS=true 环境变量启用)。实测基准:本文实验环境 $ldap_starttls = false(明文),生产模板置 true——注意开启后要求目录端证书被 SSP 侧信任,否则连接直接失败,需先在 SSP 容器内装好 CA。

3.3 SSP → SMTP:发信通道加密

官方默认 $mail_smtp_secure = 'tls'。公网 SMTP(587)保持默认;内网无 TLS 的中继必须显式置空(否则 SMTP connect() failed,实测见部署指南十一.2)——加密与可用性按真实通道能力取舍,不要盲抄默认值。

四、网络与防火墙

加固原则:只暴露必须暴露的端口,其余全部内网化。本文栈的暴露面实测:

服务端口映射加固姿态
SSP Web宿主 8138(HTTP,内网验证用)→ 生产走 8443 TLS生产仅暴露 443/8443,8138 收回
TLS 反代宿主 8443对外唯一入口
OpenLDAP无宿主映射仅栈内网可达(实测 ss -ltn 无 389 监听)
mailpit无宿主映射仅栈内网可达(测试桩,生产应为真实 SMTP)

实测确认(宿主机 ss -ltn | grep -E '389|8025' 无输出——LDAP 与 SMTP 均未映射宿主);SSP 与目录间流量限制在自定义 bridge 网段内。生产防火墙建议:8443 仅放行办公网/VPN 段;目录 389/636 仅放行 SSP 所在网段;SSP 容器禁止直接对外。

五、审计日志

5.1 审计日志开启(实测)

默认无独立审计文件,行为只在 Apache 访问日志留痕。开启 $audit_log_file = "/var/log/self-service-password/audit.log"; 后,每次改密/重置动作写一行 JSON(实测目录需先建并授权 Web 进程属主):

mkdir -p /var/log/self-service-password && chown www-data:www-data /var/log/self-service-password

实测审计行(同一用户两次真实动作):

{"date":"Sat, 24 Oct 2026 09:37:35","ip":"10.89.76.1","user_dn":"uid=alice,ou=people,dc=example,dc=org","done_by":"alice","action":"change","result":"throttle"}
{"date":"Sat, 24 Oct 2026 09:38:47","ip":"10.89.76.1","user_dn":"uid=alice,ou=people,dc=example,dc=org","done_by":"alice","action":"change","result":"passwordchanged"}

第一行是限流拦截(throttle)、第二行是改密成功(passwordchanged)——成功与被拒动作都有留痕,ip/user_dn/result 三要素齐备,可直接进 SIEM。

5.2 日志面盘点

日志位置内容
审计日志$audit_log_fileJSON 结构化动作记录(实测见上)
Web 访问日志Apache access log全部 HTTP 请求(含 POST /index.php?action=change)
应用错误php:notice 级邮件发送失败、LDAP 搜索错误、限流触发等(实测样例:LDAP - User alice not found、LDAP - Modify answer (error 50 (Insufficient access))
重置请求日志$reset_request_log(可选)重置链接请求单独落盘

生产建议:audit.log 按日切割 + 异地采集;$debug = false(默认)避免泄露敏感细节;$mail_smtp_debug = 0 保持关闭。

六、服务降权与最小权限

  1. 进程身份(实查):Apache 主进程 root、工作进程 www-data(实测 ps aux 回显),SSP 无需额外降权;容器内可再叠加 read_only rootfs + 可写区白名单(/var/log/self-service-password、会话目录)。
  2. 配置文件权限(实测现状与加固):/var/www/conf/config.inc.local.php 实查为 -rw-r--r-- root root(644),其中含 LDAP 绑定密码——收紧为 640 root:www-data(实测命令 chmod 640 + chown root:www-data,Web 进程可读、其他用户不可读);证书私钥 ssp.key 实查 600。
  3. 只读挂载:config 与 LDIF 均以 :ro 挂载(实测 compose),防止运行时篡改。
  4. 加密密钥管理:$keyphrase 用 openssl rand -hex 16 生成(本文实测值 88eb2fee3d7b4d1dbbe99573a8be9972 仅用于实验),生产环境从密管系统注入环境变量;变更会作废全部存量 token 与安全问题答案,须安排重置窗口并提前公告。
  5. 绑定账号最小化:见 2.2,核心三原则——搜索与写分离(ssp-bind 只读)、密码散列不外泄(userPassword 不给搜索账号)、搜索基准收窄(ou=people)。

加固核对清单(逐项实测通过项汇总):

#项状态
1keyphrase 改随机串(1.8.2 强制)✅ 实测
2专用只读绑定账号 + 收窄基准 DN✅ 实测
3ACL:userPassword/info 不给搜索账号、self 写权最小化✅ 实测
4密码策略 12 位四类字符 + LDAP 字段禁词✅ 实测
5限流开启(per_user/per_ip 阈值)✅ 实测
6token 时效收紧(本文实测 30 秒过期)✅ 实测
7HTTPS 反代(TLSv1.2+,明文端口拒绝)✅ 实测
8审计日志 JSON 落盘✅ 实测
9conf 640 + 只读挂载 + 私钥 600✅ 实测
10目录/SMTP 端口不对宿主暴露✅ 实测

七、补丁与版本策略

  • 版本锚定:镜像 tag 固定到补丁位(1.8.2,不要 latest);容器内版本三重实查($version = "1.8.2" 源码行、页脚 version 1.8.2、镜像 label org.opencontainers.image.version)建立基线,升级后复核。
  • 升级通道:LTB 官方渠道(Debian/RPM 仓库、GitHub Releases、Docker Hub)跟踪 1.8.x 补丁版;本文版本节奏溯源见《Self-Service Password 1.8.2 介绍与选型指南》(1.8.0→1.8.1→1.8.2 间隔约 1-3.5 个月,2026 年活跃维护)。
  • 1.8.2 的安全基线变化:keyphrase 出厂值强制检查(拒绝 secret 默认值)说明项目在收紧默认安全姿态——升级到更高版本时应在测试环境先跑一遍本文全部实测项,确认默认行为变化。
  • 回滚:镜像 tag 回退 + $ldap_base/ACL 保持不变即可;token 与答案数据依赖 keyphrase,回滚不丢(keyphrase 不变时)。

八、常见问题

Q1:切到专用绑定账号后改密报「用户名或密码不正确」,日志 User alice not found? ACL 把 objectClass 挡了(搜索 filter 含 (&(objectClass=inetOrgPerson)…) 需要对该属性有读权)。见 2.2 排障实录:给 ssp-bind 单独开 attrs=objectClass 的 read 即恢复。定位技巧:用 PHP ldap_search 以 ssp-bind 身份分别测 (uid=alice) 与 (objectClass=inetOrgPerson) 两个 filter,后者空即为此坑。

Q2:开了 audit_log_file 但没有文件生成? 日志目录不存在或 Web 进程无写权。先 mkdir -p 并 chown www-data:www-data(实测修复过程见五.1),再重启 SSP。

Q3:ssp-bind 读 userPassword 返回空是不是坏了? 不是,这正是最小权限的预期行为(条目返回但 userPassword 属性被 ACL 剥离)。SSP 改密不依赖读散列:旧密码校验走用户 bind,新密码经 EXOP/Modify 提交。

Q4:HTTPS 反代后邮件里的重置链接还是 http? $reset_url 是静态配置。反代场景用官方注释写法按 X-Forwarded-Proto 动态推导,或直接写死 https://ssp.example.org/,并确认 nginx 透传该头。

Q5:token 过期后用户看到什么? 实测($token_lifetime = "30",35 秒后打开重置链接):页面回落到发送页提示「邮件发送密码重置链接」,不呈现重置表单——过期 token 无法重用,用户需重新申请。

九、参考链接