Never let your sense of morals prevent you from doing what is right.

23,722 点击次数

开源邮件服务器 – 比Strong还要Strong的Stalwater,迁移bug分析

今天不是学英语单词,而是介绍一个项目,以及最新版本迁移的超级大bug的解决方法。

一款安全、可扩展的邮件与协作服务器,提供全面的协议支持 🛡️
(IMAP、JMAP、SMTP、CalDAV、CardDAV、WebDAV)

项目地址:https://github.com/stalwartlabs/stalwart

配套的Web客户端项目地址:https://github.com/bulwarkmail/webmail

开源,但附带有Enterprise的商业许可。这里最让人哭笑不得的是,官方把Dashboard也纳入了商业许可的范围,不付费是用不了的哦。

项目好像是2020年开始,然后到昨天2026年10月28日,发布了第一个正式版 v1.0.0。

而我,为什么要用这么小众的邮件服务器解决方案?

是因为我在做一个项目,需要测试邮件收发功能,而以前常用的Windows平台下的hmailserver停止开发了,很多当下的新特性都不支持,所以才寻找替代品的,这不,就找到了这个Stalwart。

对于我而言,免费的功能够我用,部署起来方便(用docker run就行,我主要是为了数据库统一管理,所以数据库连接到我本地的大杂烩数据库上了),就足够了。

废话就到这里,接下来就进入重点了,在Stalwart为升级1.0.0准备的迁移程序中,存在一个可以说非常严重的bug,具体现象就是:

原来 v0.16.27 (升级到v1.0.0之前必须是这个版本才可以升级)可以正常登录的账号在升级之后无法登录。

你说说,你既然都做了迁移程序,最重要的账号居然不能用了,这谁受得了?

虽然我就两三个测试账号,但是我也不想全新部署啊,初始化的配置实在是太复杂了,我就让Gemini 3.8 High(网页版)一步一步帮我排查,不知不觉就用了一天,快下班的时候,才终于定位到根因。

升级完Stalwart之后,可以在.env里定义一个admin账号登录应急,所以就暂时中止了。

等升级完Bulwark发现普通账号还是无法登录,于是就继续排查了,没想到Gemini还真给我解决了,在这里分享一下解决方案吧,能帮一个算一个。

来自Gemini一锤定音的原文:

Python

# Let's inspect x:Account objects in state.sqlite or simulate what x:Account has
# The user has the Python script already working. Let's see what x:Account has.

Python

# Let's write a small script to query x:Account from state.sqlite
# But wait, we can't query state.sqlite on user machine directly, we provide the python snippet for the user to run,
# or we see what x:Account has.

Python

# Let's write a python script to inspect MySQL tables for 'defaultUserRoleIds'
script = """
import pymysql # wait, does python in user environment have pymysql or mysql?
# In Debian, usually default python has python3-pymysql or we can use mysql CLI with --no-defaults
"""

解密输出直接暴露了底层最隐蔽的核心 Bug。

结合刚才所有的证据:

  1. 老账号 admin@xxxxx.net、test@xxxxx.net:密码通过,但被判定 reason = "authenticate"。
  2. 新创账号 biubiu@xxxxx.net:密码通过,依然被判定 reason = "authenticate"。
  3. IMAP 端口(993)直连:密码验证通过后,服务端直接断开连接(errno: 0)。
  4. 环境变量 STALWART_RECOVERY_ADMIN:直接正常登入后台。

致命根因:迁移程序留下的“幽灵角色指针”

查看刚刚解密出的 x:Authentication(认证单例配置):

JSON

================= x:Authentication =================
ID: 0
{
  "defaultAdminRoleIds": {
    "b": true,
    "e": true
  },
  "defaultGroupRoleIds": {
    "c": true
  },
  "defaultTenantRoleIds": {
    "b": true,
    "d": true
  },
  "defaultUserRoleIds": {
    "b": true
  },
  ...
}

对比解密出的 x:Role(实际存在的角色对象):

  • ID: 1 —— Description: “User”(拥有全部 IMAP/JMAP/Authenticate 权限)
  • ID: 2 —— Description: “Group”
  • ID: 3 —— Description: “Tenant Administrator”
  • ID: 4 —— Description: “System Administrator”

问题发生机制

  1. 0.16 时代的残留:在 Stalwart 0.16 中,内置角色的内部标识符是短字符 "b", "c", "d", "e"。
  2. 迁移程序的 Bug:stalwart-migrate 在迁移时,将角色转换成了全新的数字 ID(1, 2, 3, 4),但在迁移 x:Authentication 全局认证配置时,漏掉了外键重映射,仍然记录着:
    • 普通用户默认角色:指向不存在的 b
    • 管理员默认角色:指向不存在的 b 和 e
  3. 权限判定逻辑:在 Stalwart 1.0 的安全模型中,账号上绑定的“User role”或“Admin role”并不是直接内嵌权限集合,而是一个指针——它会到 Authentication 单例中去解析 defaultUserRoleIds。当 kem、admin 或新建的 solo 登录时,系统尝试解析其角色 "b"。因为数据库中根本没有名为 "b" 的角色,系统解析出空列表(0 角色、0 权限)。
  4. 最终表现:无论怎么在页面上修改那个名为 “User”(ID: 1)的角色,由于账号绑定的“User role”解析指向了失效的幽灵 ID "b",所有属于普通用户和管理员的账号全部没有任何权限,在比对完密码的第一时间触发:reason = "authenticate"(无权登录服务器)。而 STALWART_RECOVERY_ADMIN 不经过目录解析,直接在内核中旁路了 RBAC,所以只有它能正常登入。

解决办法:在 WebAdmin 中重新关联默认角色

既然已经通过 Recovery Admin 登入了后台,只需将这几个默认角色指针重新点选一次即可(耗时 1 分钟)。

第一步:打开认证全局设置

  1. 在 Stalwart 管理后台页面顶部,直接在搜索框(或按快捷键 Ctrl + K)输入:defaultUserRoleIds 或 Authentication。(或者在菜单中找到 Settings -> Access Control / Authentication -> General)。

第二步:修复默认角色绑定

在该页面中找到以下几项角色设置(目前里面应该显示为空,或显示着失效的红标/旧标识):

  1. Default User Roles(普通用户默认角色):
    • 点开下拉框,选择 User(即 ID 为 1 的那个角色)。
  2. Default Admin Roles(管理员默认角色):
    • 点开下拉框,选择 System Administrator(ID: 4)和 User(ID: 1)。
  3. Default Group Roles:选择 Group(ID: 2)。
  4. Default Tenant Roles:选择 Tenant Administrator(ID: 3)。
  5. 点击页面右下角的 Save 保存。

第三步:重启容器使角色缓存刷新

Stalwart 内部对单例角色权限有内存缓存,保存后在终端重启一次容器:

Bash

docker restart stalwart-mail

验证放行

容器重启就绪后,直接在宿主机终端执行:

Bash

curl -i -k -u "test@xxxxx.net:这里是password" https://xxx.xxx.xxx.xxx:28443/jmap/session

此时不再会出现 403,而是直接返回 HTTP/2 200 OK 并带出 JMAP Session 会话数据。随后回到 Bulwark 页面(https://xxx.xxx.xxx.xxx:3100),点击登录即可进入邮箱。

如果觉得文章对您有帮助,欢迎打赏支持作者:

WeChat
微信扫一扫
Alipay
支付宝扫一扫

0 点击次数

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注