双重身份验证
双重身份验证为密码登录增加第二步:输入手机上验证器应用生成的六位数字代码。它内置于 DeviceChain,无需身份提供商,也无需额外的服务。
本指南分三部分:面向个人,说明如何为自己的账号开启;面向租户管理员,说明如何强制要求;面向运维人员,说明如何重置、在设备丢失后恢复,以及如何监视针对它的攻击。想了解它在平台其他防护中的位置,请先或随后阅读安全模型。
覆盖范围
- 每个账号一个因素,在所有租户中生效。 账号(身份)是全局的:它属于实例,而它在各租户中的成员关系是相互独立的。因此双重身份验证属于账号,在此人可以进入的每个租户中都适用。
- 仅限密码登录。 通过单点登录的登录不使用它,因为身份提供商已经证明了此人的身份。服务账号和设备也从不使用它。参见单点登录与双重验证。
- 验证器应用加恢复码。 应用生成每 30 秒变化一次的六位代码(基于时间的一次性密码标准 RFC 6238,使用 SHA-1,接受当前时间前后各一个步长的窗口)。十个一次性恢复码用于应对手机丢失。
- 各节点的时钟必须准确。 代码只在其生成的 30 秒步长及前后各一个步长内有效,因此时钟偏差超过约 30 秒的服务节点会拒绝正确的代码。请在每个节点上运行 NTP(或云厂商的时间服务)。
为你的账号开启
打开用户菜单,选择 Account security(账号安全)。
- 选择 Set up two-factor authentication(设置双重身份验证) 并输入密码。控制台会再次要求输入,因为登录令牌可能被转交,而设置这一因素需要的不只是令牌。
- 用验证器应用扫描二维码(任何支持基于时间的代码的应用都可以),或者输入 Can't scan? Enter this key(无法扫描?输入此密钥) 下显示的密钥。二维码由你的浏览器根据只在此刻显示的密钥绘制;DeviceChain 将密钥密封保存,且从不再次返回。应用用实例的显示名称标注该账号;未设置显示名称时,则使用实例的主机名。
- 输入应用显示的六位代码并选择 Turn on(开启)。在此之前,你的登录方式不会有任何变化。未确认的设置在十分钟后过期,输错五次代码会取消设置。
- 控制台会显示十个恢复码,只显示这一次。请复制或下载它们,并存放在手机以外的地方。每个恢复码可以代替应用代码登录一次。它们的形式如
ABCD-EFGH-JKMN-PQRS。
开启后,你账号的其他所有会话都会结束,无论位于何处,这样由只知道你密码的人发起的会话不会在开启后继续存在。控制台会把你当前的会话换成一个新的,因此你无需重新登录即可继续使用。
同一页面显示它是否已开启、何时设置,以及还剩多少恢复码。你还可以:
- 生成新的恢复码。 这会替换全部十个,无论是否已用过:旧的恢复码随即失效。它需要你的密码和一个代码(应用代码或未使用的恢复码)。剩余不多时页面会提醒你。
- 关闭双重验证。 需要你的密码和一个代码,会移除该因素和恢复码,并结束你账号的所有会话,包括当前会话。之后你只用密码重新登录。
登录
照常登录。当密码正确且账号设有第二因素时,控制台不会直接放你进入,而是要求输入代码:
- 输入应用中的六位代码。一个代码只能用一次。如果刚用过一个,请等应用显示下一个。
- 或者选择 Use a recovery code instead(改用恢复码) 并输入一个恢复码。写法不限:大小写均可,带不带连字符或空格都行。形近字符也会被容忍:字母 O 视为零,字母 I 或 L 视为一。每个恢复码只能用一次。
代码步骤五分钟后超时,最多允许五次尝试。之后请重新登录。凡是有密码登录的地方都会出现同样的步骤:控制台、仪表板应用(/dash)、接受邀请,以及 AI 智能体的 MCP 连接把你带到的页面。仪表板应用和 MCP 页面可以要求输入代码,但不能用来设置它:它们会告诉你到控制台的 Account security(账号安全) 中设置。
使用恢复码的登录会被如实记录。剩余恢复码不多时,Account security(账号安全) 会提醒你。
手机丢失了怎么办
- 你还有恢复码。 用它登录,然后打开 Account security(账号安全),关闭双重验证,再在新手机上重新设置。如果只是想更保险,也可以只生成一组新的恢复码。
- 你没有恢复码。 请运维人员重置你的第二因素(见下文)。租户管理员无法代你重置,因为账号由所有租户共享。重置后你只用密码登录,并可以重新设置该因素。
- 你是唯一的运维人员,且没有恢复码。 参见 break-glass。
为租户强制要求
租户管理员可以要求所有以密码登录该租户的人使用第二因素。打开 Access → Sign-in Security(访问 → 登录安全) 并选择 Require(要求)。这需要管理成员的权限,且作用范围为整个租户:仅限于部分客户的成员无法更改它。运维人员可以在 Admin → Tenants(管理 → 租户) 的租户页面上设置同一开关。
强制要求会产生以下效果:
- 密码登录未使用第二因素的成员会被拒绝进入租户。如果他有该因素,控制台会要求他重新登录并输入代码;如果没有,控制台会先把他引导到 Account security(账号安全) 去设置。
- 不会有人被锁在门外。 成员始终可以在没有第二因素的情况下登录并到达 Account security(账号安全),而设置该因素只需要他的登录状态和密码。因此开启要求不会锁住任何人,也无需配置宽限期。
- 已在租户内的会话会在下一次刷新时结束。 此时正在使用控制台的人会被要求重新登录,并看到原因。
- 服务账号不受影响,因为它们从不使用密码登录。
- 超级用户不受其约束。 仅用密码会话的超级用户可以进入该租户,每次进入都会记录为
mfa_enforcement_bypassed。运维人员仍可开启自己的第二因素,也应该这样做。 - 它不约束管理控制台或管理 API。 租户的要求决定谁可以进入该租户。能否进入运维人员的管理平面是另一个问题,租户的任何设置都不会影响它。
层级下限
运维人员可以把双重身份验证设为某个层级下所有租户的下限:打开该层级(Tenants → Tiers(租户 → 层级)),在其 Settings 选项卡上勾选 Require two-factor authentication(要求双重身份验证)。此后该层级的租户无论自身如何设置都必须使用它,其管理员也无法关闭:租户的 Security(安全) 页面会说明由层级要求。保持该项关闭,则由各租户自行决定。
单点登录与双重验证
租户的要求只约束密码登录。通过单点登录登录的人,受其连接自身的要求约束:如需更强的保证,请在连接上要求某种认证方式(requiredAmr,例如 ["mfa"]),使身份提供商必须已执行自己的第二因素。参见配置单点登录。开启 Require SSO 的租户已没有可适用该要求的密码登录。
命令行与 API 客户端
dcctl
dcctl dead-letters、dcctl presence 和 dcctl sim 会代你以某个账号登录,因此对设有第二因素的账号,它们接受 --otp <代码>。恢复码也可以。该代码仅用于首次登录,并且一个代码只能用一次:
- 如果命令必须重新登录(长时间运行期间令牌过期),它会在终端上请求输入代码,且不回显。
- 没有终端时,它会停止并提示用新的
--otp重新运行。它从不两次提交同一个代码,因为服务器会把它当作重放拒绝,而在此之上再加重试循环,正是账号被锁定的原因。
对于自动化,要么保留一个没有第二因素的超级用户,只用于脚本,要么接受提示输入。前者是流水线的务实选择,代价是该账号仅靠密码保护,所以请给它一个很长的生成密码。dcctl sim 从实例的 Kubernetes Secret 读取初始超级用户的密码,因此已启用第二因素的初始超级用户在那里也需要 --otp。
GraphQL
对设有第二因素的账号,login 不再返回令牌。它会以错误码 MFA_CHALLENGE 失败,其中携带一个质询,客户端再用 verifyMfa 完成登录。每个使用 login 登录的客户端都必须处理它。确切的形式见 GraphQL API 参考。
C# SDK
对这样的账号,LoginAsync 会抛出 MfaRequiredException。把它的 Challenge 连同代码传给 VerifyMfaAsync:
try
{
await client.LoginAsync(email, password);
}
catch (MfaRequiredException mfa)
{
// 向用户索取验证器应用中的代码,或一个恢复码。
await client.VerifyMfaAsync(mfa.Challenge, code);
}
MfaRequiredException 是一个 GraphQlRequestException,因此已经捕获该类型的代码仍会捕获到它。不要为了重试再次调用 LoginAsync:第二次调用会生成第二个质询。错误的代码是一个 GraphQlRequestException,质询仍可再尝试几次;质询过期则对应错误码 MFA_CHALLENGE_EXPIRED,此时用户重新登录。
面向运维人员
重置丢失的第二因素
打开 Admin → Identities(管理 → 身份),打开该用户,在 Two-factor authentication(双重身份验证) 区域选择 Reset two-factor(重置双重验证)。验证器、恢复码和保存的密钥都会被移除,此人的所有会话随即结束。其下次登录只需密码,并可重新设置双重身份验证。列表中的 Two-factor 列可以看出谁已开启。
- 需要管理用户的权限。重置持有系统角色的身份需要超级用户。
- 如果账号自上一次成功验证以来出现过错误代码,重置会要求提供新密码,并在同一步中设置。 错误代码说明持有密码的人一直在猜测代码,因此只移除第二因素会把账号交到他手里。服务器要求时,控制台会显示额外的字段;API 会返回
MFA_RESET_NEEDS_PASSWORD,在你提供newPassword之前不做任何更改。 - 重置会记录为
mfa_reset,并注明操作者。 - API 调用是管理 API 上的
resetIdentityMfa(email, newPassword)。
清除某个身份的密码(管理 API 上的 clearPassword,会把它转为仅单点登录)时,会在同一步中一并移除它的第二因素、恢复码和保存的密钥。该因素只保护密码登录,没有密码后它不再保护任何东西,只会成为孤立的残留。
Break-glass:无法登录的运维人员
如果唯一的超级用户设有第二因素,却同时丢失了手机和恢复码,就没人能通过管理 API 重置它。dcctl instances break-glass mfa-reset 可从集群内部完成:
dcctl instances break-glass mfa-reset --instance <instance> --email superuser@devicechain.local
- 只需要集群凭据,别无其他。 它在一个就绪的
user-managementPod 内运行该服务自己的二进制文件(因此需要在实例命名空间中对 Pod 执行exec的权限),不登录任何东西。 - 它总会更换密码。 这里没有可以担保的调用者,所以只移除第二因素会把账号交给持有旧密码的人。dcctl 会生成一个密码并只打印一次。用于脚本,或想自己选择密码时,加上
--password-stdin,使用标准输入的第一行(不打印任何内容;同时还需要--yes)。 - 它会要求你输入实例名称以确认,或接受
--yes。 - 它会结束该账号的所有会话,并移除因素、恢复码和密钥。
- 它会拒绝没有系统角色的身份。 其他人通过管理 API 重置,日志中会记录是哪位运维人员操作的。break-glass 重置记录为
mfa_reset,操作者为break-glass:pod-exec。 - 对于实例的初始超级用户(
superuser@devicechain.local),dcctl 还会更新实例 Secretdci-<instance>-superuser的password键,dcctl sim、各类探测以及再次执行 bootstrap 都会读取它。如果更新不了,它会明确告警,因为重置已经完成,而该 Secret 仍保存旧密码:在修复 Secret 之前,用--admin-password把新密码传给dcctl sim。
锁定,以及为什么补救办法是重置密码
每一次对代码的猜测,都来自已经掌握密码的人,所以一连串错误代码说明密码已被他人知晓。
- 每个错误代码在检查之前先计数。 来自同一来源地址的错误代码达到 5 次后,该地址被锁在账号的验证器之外;所有地址合计达到 20 次后,验证器对所有人锁定。锁定持续一分钟,之后每多一次失败就加倍,最长 15 分钟。
- 被锁定的尝试得到与错误代码完全相同的回答("invalid code"),因此回答不会向猜测者透露任何信息,锁定期间也不会校验任何代码。
- 恢复码在锁定期间仍然有效。 它们带有 80 位随机数据,无法猜测,所以不受验证器锁定影响。这样,持有恢复码的人不会被拒之门外。
- 持有密码的人可以无限期续期锁定,每次到期后输错一个代码即可。设计上没有永久锁定:永久锁定会让任何知道某个地址的人把它的主人永远锁在外面。这与登录退避是同样的取舍。
- 补救办法是重置密码(管理 API 上的
setPassword,或身份页面上的 Set password),而不是重置第二因素。 重置密码会轮换账号的会话纪元,同时使所有未完成的登录质询作废。在密码已被知晓时只重置因素,会把账号交给正在猜测的人。重置因此会拒绝在没有新密码的情况下执行。 - 来源地址在 ingress 日志中。能够控制转发地址头的发送者可以获得新的按地址额度,所以每个账号 20 次的全局上限才是真正的约束。
残余风险在安全模型中说明:对于已经掌握密码并按上限持续猜测的人,每年约 10%。
告警与指标
chart 随附两条告警,仅在启用指标告警时触发:
| 告警 | 严重级别 | 含义 | 处理办法 |
|---|---|---|---|
MfaIdentityLocked | critical | 某个账号的全局锁定生效:检查了二十个错误的验证器代码,每个都发生在密码正确之后。有人掌握了该账号的密码并在猜测代码。只要出现一次就会触发。 | 在审计日志中找到该账号(mfa_locked,其前有 mfa_failed 和 mfa_challenged 记录),并重置它的密码。不要先重置第二因素。来源地址见 ingress 日志。 |
MfaVerificationFailuresSustained | warning | 连续十五分钟,整个实例中错误、重放或被锁定的代码以高于每十秒一次的速度被拒绝。要么是大量用户输错(节点时钟问题,或应用不同步),要么是有人在使用获取到的密码。 | 检查各节点的时钟是否同步,然后查看按账号分组的 mfa_failed 记录以及 ingress 日志。锁定的账号会自行触发 MfaIdentityLocked。 |
它们背后的计数器是 devicechain_usermanagement_mfa_verifications_total{method,result}(method 为 totp 或 recovery;result 为 ok、invalid、replay、locked、expired、budget、rate_limited 或 unavailable)、mfa_challenges_total、mfa_enrollments_total{result}(begun、confirmed、disabled、reset)和 mfa_identity_locks_total。参见指标参考。
审计日志记录什么
登录服务会记录:密码正确且需要代码时的 mfa_challenged;代码经检查被拒绝时的 mfa_failed;触发全局锁定的那次尝试的 mfa_locked;以及 mfa_enrolled、mfa_disabled、mfa_reset(含操作者)、mfa_recovery_code_used、mfa_recovery_codes_regenerated、mfa_enforcement_bypassed 和 tenant_mfa_required_changed。被锁定拒绝的尝试只计数、不写入,因此持有密码的人无法驱动日志写入。任何记录都不会包含代码、密钥或质询。
login 记录的含义变了。 它现在在登录完成时写入,而不是在密码被接受时写入,并说明方式:pwd、pwd+otp 或 pwd+recovery。对设有第二因素的账号,密码这一步对应 mfa_challenged 记录,只有代码被接受后才会出现 login 记录。
故障排查
| 现象 | 可能原因 |
|---|---|
| 所有人的正确代码都被拒绝 | 服务节点的时钟偏差超过约 30 秒。请修复时间同步。 |
| 某一个人的代码被拒绝,其他人正常 | 手机时钟不准(请开启自动时间),或者代码已被使用:一个代码只能用一次,请等下一个。 |
| 即使用新代码也提示 "invalid code" | 账号可能因重复的错误代码而被锁定。被锁定的尝试看起来与错误代码完全一样。请使用恢复码,最多等待 15 分钟,锁定反复出现时请联系运维人员。 |
| "This sign-in timed out" | 质询有效期五分钟、可尝试五次。请重新登录。修改密码也会使其作废。 |
| 某个成员被引导到 Account security(账号安全) | 其租户要求双重身份验证,而他尚未设置。 |
| 设置完成后所有会话都结束了 | 属预期行为:开启后会结束账号的其他会话。 |
| Account security(账号安全) 提示 "Managing two-factor authentication needs a recent password sign-in. Sign in again" | 该页面需要最近一次的密码登录,因为它使用的身份令牌有效期为 15 分钟。请退出登录,用密码重新登录,然后再次打开 Account security(账号安全)。 |