Passbolt 5.16.0 安全加固指南

Passbolt 5.16.0 安全加固指南:GPGAuth 认证与伪造令牌实测、TLS 证书与 HSTS、防火墙限源规则增删实测、用户组共享与密码轮换全链路实操、审计日志聚合、备份恢复三件套与服务端密钥权限加固,全部命令与回显实测于 2026-10-19。

Passbolt安全加固安全审计2026/10/19

Passbolt 5.16.0 安全加固指南

目标:围绕 Passbolt 5.16.0 生产运行的安全面逐项实测——认证与访问控制、传输加密、网络暴露面、用户组共享与密码轮换、审计日志、备份恢复、密钥文件权限——每项加固措施都给出可复现的验证回显。本文全部命令与回显实测于 2026-10-19,部署形态见《Passbolt 5.16.0 Docker Compose 部署指南》,配置体系见《Passbolt 5.16.0 配置参数详解》。

一、环境信息

项目值
Passbolt 版本5.16.0(CE,官方最新稳定版)
服务器 IP192.168.146.128
对外服务https://192.168.146.128:8113(HTTPS 强制开启)
内部网段10.89.41.0/24(自定义隔离网段)
测试账号admin@passbolt.local(管理员)、bob@passbolt.local(普通用户)
验证工具REST API、bin/cake CLI、openssl、ss、iptables

安全基线先行确认:bin/cake passbolt healthcheck 尾部实测输出 [PASS] No error found. Nice one, sparky!,且 Application configuration 段确认 Passbolt is configured to force SSL use、Selenium API endpoints are disabled、Search engine robots are told not to index content(完整回显见《Passbolt 5.16.0 配置参数详解》3.1 节)。

二、身份认证与访问控制

2.1 GPGAuth 认证协议

Passbolt 不使用「口令哈希比对」登录,而是 GPGAuth(协议版本 1.3.0,实测响应头 x-gpgauth-version: 1.3.0):用户私钥永不上传服务器,登录时服务端用用户公钥加密一次性令牌,用户端解密后回传,全程验证私钥持有。登录页响应头声明协议端点(实测摘录):

x-gpgauth-login-url: /auth/login
x-gpgauth-logout-url: /auth/logout
x-gpgauth-verify-url: /auth/verify
x-gpgauth-pubkey-url: /auth/verify.json

实测以管理员账号完成两段握手后,服务端返回:

You are successfully logged in.

响应头携带 x-gpgauth-authenticated: true 并下发会话 Cookie,随后即可调用业务 API。

2.2 伪造认证令牌的实测拒止

对认证协议做负向测试:直接伪造 Stage 2 的解密结果提交(user_token_result 填入伪造字符串):

curl -sk -X POST -H "Content-Type: application/json" \
  -d '{"gpg_auth":{"keyid":"5E730509BBEFA9E0DF1D01A691AFA3DF59437293","user_token_result":"forged-garbage-token"}}' \
  https://192.168.146.128:8113/auth/login.json

实测回显(HTTP 400):

{"header":{"id":"3d596588-d767-49a3-9787-0bff116001eb","status":"error","servertime":1792393923,"action":"a3c19ad2-8920-5395-86d0-8567cb34f382","message":"The authentication failed.","url":"\/auth\/login.json","code":400},"body":null}

要点:伪造令牌只得到统一的 The authentication failed.,不泄露具体失败原因;且这类异常请求被计入审计日志(见第六节 Error.error 计数)。

2.3 注册面收敛

对外暴露的注册入口是攻击面之一。v5 通过 Self Registration 插件收敛:默认 provider: null 即「注册关闭,仅管理员可添加用户」(healthcheck 回显 Registration is closed, only administrators can add users)。如需开放,务必配置邮箱域白名单(实测 PUT {"provider":"email_domains","data":{"allowed_domains":["<组织域名>"]}},全流程回显见《Passbolt 5.16.0 配置参数详解》5.6 节),禁止无白名单的开放注册。

2.4 角色与权限最小化

内置角色三级(admin / user / guest),实测用户角色分布:

admin@passbolt.local → admin
bob@passbolt.local → user
charlie@passbolt.local → admin

加固原则:日常运维使用 user 角色账号,admin 角色仅保留给必须管理组织的账号;创建用户时用 bin/cake passbolt register_user ... -r user 指定普通角色。细粒度的资源级权限通过用户组共享模型实现(见第五节)。

三、传输加密(TLS)

3.1 当前证书实测

对外服务使用部署时自动生成的自签证书,实测证书信息:

echo | openssl s_client -connect 192.168.146.128:8113 2>/dev/null | openssl x509 -noout -subject -issuer -dates -fingerprint -sha256
subject=C = FR, ST = Denial, L = Springfield, O = Dis, CN = www.passbolt.local
issuer=C = FR, ST = Denial, L = Springfield, O = Dis, CN = www.passbolt.local
notBefore=Oct 19 06:27:18 2026 GMT
notAfter=Oct 19 06:27:18 2027 GMT
sha256 Fingerprint=8B:C7:C7:BB:D6:6F:AB:AF:95:E0:D3:02:9B:56:2A:9D:7C:EA:B1:5F:2A:E6:A5:11:F5:3B:1F:CC:00:F0:00:CC

自签证书(issuer 与 subject 相同)有效期 1 年。自签证书仅适合内部验证:用户浏览器首次访问需手工信任,且无法防御中间人替换证书。生产加固:用组织域名申请正式证书(Let's Encrypt 或企业 CA)替换,并通过反向代理终止 TLS 或将证书链配置进服务端。

3.2 HTTPS 强制与 HSTS

PASSBOLT_SSL_FORCE=true 生效(healthcheck:Passbolt is configured to force SSL use)。实测任意页面响应头均携带:

strict-transport-security: max-age=31536000; includeSubDomains

即使用户输入 http:// 地址也会被强制跳转 HTTPS,且 HSTS 声明让浏览器在后续访问中拒绝明文连接。证书更换方法与反代组网(配合《Authelia 4.39 介绍与选型指南》等统一认证网关)见部署指南与相关组件文档。

四、网络与防火墙

4.1 暴露面审计

加固第一步是确认服务监听面。实测对外只暴露 8113 一个端口:

ss -ltn | grep 8113
LISTEN 0      4096         0.0.0.0:8113       0.0.0.0:*
LISTEN 0      4096            [::]:8113          [::]:*

数据库 3306 未向宿主网络发布(ss -ltn 中无非回环的 3306 监听;本机出现的 127.0.0.1:3306 属宿主自有本地数据库,与 Passbolt 无关),仅在其隔离网段内可达。

4.2 网段隔离

服务运行在自定义隔离网段 10.89.41.0/24(实测回显 passbolt-net local 10.89.41.0/24,核对命令见《Passbolt 5.16.0 Docker Compose 部署指南》3.2 节),与宿主物理网卡路由互不干扰,创建后 ip route get 验证外部路由仍走物理网卡(实测回显见部署指南 3.2 节)。数据库不出隔离网段,Web 服务是唯一对外触点。

4.3 防火墙限源规则

在防火墙上将 8113 的访问来源限制到可信网段。实测规则的添加与删除(iptables 规范化回显):

iptables -A INPUT -p tcp --dport 8113 -s 192.168.146.0/24 -j ACCEPT
iptables -S INPUT
-P INPUT ACCEPT
-A INPUT -s 192.168.146.0/24 -p tcp -m tcp --dport 8113 -j ACCEPT

验证后删除(恢复原状):

iptables -D INPUT -p tcp --dport 8113 -s 192.168.146.0/24 -j ACCEPT
iptables -S INPUT
-P INPUT ACCEPT

生产落地时将规则持久化(iptables-packages 的 save 机制或 firewalld/ufw 等价规则),源网段填组织办公网段;公网实例建议叠加反向代理与 WAF,仅对反代回源网段放行 8113。

五、用户组共享与密码轮换(最小权限实操)

5.1 用户组共享模型

Passbolt 的授权单位是「资源 + 权限」,生产上以用户组为共享载体,避免逐用户授权。实测流程(管理员会话):

创建组并指定组管理器(bob 为 dev-team 的组管理器,可自行维护组员):

curl -kfs -b <管理员会话> -X POST -H "Content-Type: application/json" -H "X-CSRF-Token: <csrfToken>" \
  -d '{"name":"dev-team","groups_users":[{"user_id":"<bob用户ID>","is_admin":true}]}' \
  https://192.168.146.128:8113/groups.json

实测组现状(GET /groups.json):

2152d7e9-f208-49c7-a1f5-5b1bfaf9e0ca dev-team

把资源共享到组:POST /share/resource/<资源ID>.json,请求体携带 permissions(组级 READ/UPDATE 权限)与 secrets 数组——必须携带全部新获得访问权用户的再加密密文(漏带时实测报错 The secrets of all the users having access to the resource are required.)。实测共享后,普通用户 bob 登录即可在 GET /resources.json 中看到「数据库 Root 口令」,并用自己的私钥解密其密文(实测解出明文),未授权用户列表中不可见。

这个模型的安全意义:服务端只存「针对每个用户分别加密的密文」,共享行为只是把新密文分发给新用户;任何人不持有他人私钥就无法读取口令,组管理员的移交也不会导致明文经过服务端。

5.2 密码轮换实操

口令泄露或人员变动后应轮换。v5 的轮换入口是 PUT /resources/<资源ID>.json(实测 /secrets/resource/<ID>.json 仅支持 GET,无更新路由;更新走资源更新接口)。实测轮换「数据库 Root 口令」到新值:

第一步,分别用每个有权用户的公钥加密新明文(各生成一份密文,单人单份):

echo -n 'VaultRoot-v3#D3-2026' | gpg --batch --yes --pinentry-mode loopback \
  --passphrase '<用户私钥口令>' --trust-model always -a -e -r <该用户密钥指纹>

第二步,提交更新(secrets 数组每人一条,data 字段放 ASCII armor 原文,不要做 base64 包装):

{"name":"数据库 Root 口令","username":"root","uri":"https://db.passbolt.local",
 "resource_type_id":"669f8c64-242a-59fb-92fc-81f660975fd3",
 "secrets":[{"user_id":"<管理员用户ID>","data":"-----BEGIN PGP MESSAGE-----..."},
            {"user_id":"<bob用户ID>","data":"-----BEGIN PGP MESSAGE-----..."}]}

实测回显:

success The resource has been updated successfully.

第三步,普通用户侧验证:bob 请求 GET /secrets/resource/76c01266-17b8-4f02-82a7-80821b097d96.json 取到针对自己的新密文,用私钥解密:

VaultRoot-v3#D3-2026

轮换闭环成立:旧密文被全部替换,bob 无需管理员介入即可解出新口令。

六、审计日志

Passbolt 服务端自动记录关键动作到数据库 action_logs 表(关联 actions 字典表),含登录、增删改、共享、注册完成等。实测聚合查询:

mariadb -u passbolt -p'<数据库口令>' passbolt \
  -e "SELECT a.name AS action, COUNT(*) AS cnt FROM action_logs l JOIN actions a ON a.id = l.action_id GROUP BY a.name ORDER BY cnt DESC LIMIT 10;"

实测回显(本实例从部署到测试结束时点):

action                          cnt
Error.error                     19
AuthLogin.loginPost             14
GroupsAdd.addPost                5
ResourcesAdd.add                 5
SelfRegistrationGetSettings.getSettings  4
UsersIndex.index                 4
SetupComplete.complete           3
Share.share                      3
SelfRegistrationSetSettings.setSettings  3
SetupStart.start                 3

日志量合计 74 条(SELECT COUNT(*) FROM action_logs)。解读要点:

  • AuthLogin.loginPost 覆盖每次登录尝试,异常频次上涨是账号爆破的第一信号;
  • Error.error 计 19 条——实测确认异常请求(如 2.2 节的伪造令牌、各类 400 校验失败)都会被记入审计,排障时可按时间轴定位攻击或误用;
  • Share.share、GroupsAdd.addPost、SetupComplete.complete 分别对应共享、建组、注册完成,人员变动审计即核对这些动作的时间与操作者。

加固建议:对 action_logs 表纳入每日巡检(导出前一日增量并归档);Passbolt 另有邮件通知体系,关键动作(共享、用户加入)可同步告警到管理员邮箱(通知策略在管理界面调整,healthcheck 中 Some email notifications are disabled by the administrator 的 WARN 即对应此项)。

七、备份与恢复

7.1 备份三件套

Passbolt 的完整备份 = 数据库转储 + 服务端 GPG 密钥 + JWT 密钥。实测导出:

数据库转储(gzip 压缩):

mariadb-dump -u passbolt -p'<数据库口令>' passbolt | gzip > /backup/passbolt-db-$(date +%F).sql.gz

实测当日转出文件 28928 字节(数据量随使用增长)。

密钥文件备份(/etc/passbolt/gpg/ 与 /etc/passbolt/jwt/ 两个目录打包):

tar -czf /backup/passbolt-keys-$(date +%F).tar.gz -C /etc/passbolt gpg jwt

实测密钥目录四文件原始大小合计 11619 字节(serverkey.asc 2456 + serverkey_private.asc 5091 + jwt.key 3272 + jwt.pem 800),gzip 打包实测 8605 字节。密钥包属最高敏资产:备份文件权限收紧为 600,存储介质加密,严禁与数据库备份同放一处明文保管——持有 gpg 目录即持有服务端身份。

7.2 恢复要点

恢复序列(新实例上按序执行):

  1. 先恢复密钥文件:将 gpg/jwt 两个目录原样放回 /etc/passbolt/(属主 www-data、权限 660/770,见第八节);
  2. 再恢复数据库:gunzip < 转储文件 | mariadb -u passbolt -p passbolt;
  3. 启动服务后跑 bin/cake passbolt healthcheck,GPG 段全 PASS 且指纹与原实例一致(226CD536A314A96C54E0EBCBA5968499BA1A4E66)即恢复成功;
  4. 用户侧无感知:用户私钥都在各自终端,服务端密钥一致则登录握手与解密不受影响。

恢复演练应在受控窗口定期执行——只有被验证过的备份才算备份。

八、服务降权与密钥文件权限

8.1 运行身份与文件权限实测

Passbolt 服务以专用低权用户 www-data 运行,不使用 root。实测密钥文件权限:

ls -la /etc/passbolt/gpg /etc/passbolt/jwt
/etc/passbolt/gpg:
drwxrwx--- 2 root     www-data 4096 Oct 19 05:35 .
-rw-rw-r-- 1 www-data www-data 2456 Oct 19 05:35 serverkey.asc
-rw-rw-r-- 1 www-data www-data 5091 Oct 19 05:35 serverkey_private.asc

/etc/passbolt/jwt:
drwxr-x--- 2 root www-data 4096 Oct 19 05:35 .
-rw-r----- 1 root www-data 3272 Oct 19 05:35 jwt.key
-rw-r----- 1 root www-data  800 Oct 19 05:35 jwt.pem

三个要点:目录属主 root:www-data 且组可写位受控(gpg 目录 770、jwt 目录 750);文件属主 www-data 权限 660/640,其他用户不可读;JWT 目录对运行用户不可写(healthcheck 回显 The /etc/passbolt/jwt/ directory is not writable. 属 PASS 项——运行用户能读不能改,防止密钥被运行时篡改)。healthcheck 同时确认 The directory /var/lib/passbolt/.gnupg containing the keyring is writable by the webserver user,密钥环仅运行用户可用。

8.2 服务端 GPG 私钥的特别保护

serverkey_private.asc 是整个信任体系的根:泄露它,攻击者可冒充服务端完成 GPGAuth 握手诱导用户。加固清单:

  • 确认指纹固化在配置中(PASSBOLT_GPG_SERVER_KEY_FINGERPRINT,回显核对方法见《Passbolt 5.16.0 配置参数详解》5.4 节),防止静默换钥;
  • 任何导出、复制操作先审批,操作后核对文件大小与权限不变;
  • healthcheck 中 GPG 段任一 FAIL 都应立即排查(曾实测:指纹变量缺失时该段直接 FAIL,修正后全 PASS)。

九、补丁与版本策略

  • 只上稳定版:官方 GitHub Releases 中的预发布(如 v5.17.0-test.1)不进生产;实测 healthcheck 回显 Using latest passbolt version (5.16.0),升级后先跑 healthcheck 再放流量。
  • 升级前备份三件套:见第七节,跨版本回退依赖完整备份。
  • 关注安全通告:官方公告与 Release Note 中的安全修复条目应在评估后尽快跟进(https://github.com/passbolt/passbolt_api Releases)。
  • 浏览器扩展同步升级:客户端扩展承担加解密,服务端升级后提醒用户更新扩展,避免协议参数不匹配。

十、常见问题(含实测原始报错)

10.1 登录报 The authentication failed.

GPGAuth 握手失败的统一回显(HTTP 400,body: null)。排查方向:用户私钥是否可用、服务器公钥是否被替换(核对 x-gpgauth-pubkey-url 拉到的公钥指纹)、系统时间偏差。伪造令牌同样触发此回显(见 2.2 节实测)。

10.2 注册时 The key size must match security recommendations.

实测 HTTP 400:用户 GPG 密钥低于 RSA 3072 被服务端强校验拒绝。属预期安全策略,让用户选 RSA 3072 重新生成。

10.3 轮换/共享报 Could not validate resource data.

实测两种触发原因,回显均为 secrets[].data.isValidOpenPGPMessage: "The message should be a valid ASCII-armored OpenPGP message.":

  1. data 字段填了 armor 的 base64 包装——正确做法是直接放 -----BEGIN PGP MESSAGE----- 原文(见 5.2 节);
  2. 请求打到了不存在的路由 PUT /secrets/resource/<ID>.json(该路径仅支持 GET,实测 404)——更新应走 PUT /resources/<ID>.json。

10.4 共享报 The secrets of all the users having access to the resource are required.

实测 HTTP 400:共享请求漏带了新获权用户的再加密密文。按 5.1 节为每个新获权用户单独加密并全部携带。

10.5 POST 提示 Missing or incorrect CSRF cookie type

会话类 POST/PUT 请求需携带 X-CSRF-Token 请求头,令牌取自登录页下发的 csrfToken Cookie(实测 GET /auth/login 才下发,访问首页不下发)。缺头或令牌过期即报此错。

10.6 healthcheck 出现 FAIL

逐段定位:GPG 段 FAIL 先核对指纹与密钥文件权限(第八节);Database 段 FAIL 核对连接参数与库服务状态;SSL 段 FAIL 核对 PASSBOLT_SSL_FORCE 与 fullBaseUrl。修正基线:全 PASS(见第一节)。

十一、参考链接