Implement Phase-3 email-OTP recovery for break-glass mode #13

Closed
opened 2026-07-28 12:21:51 +09:00 by FLYEMOJ1 · 1 comment
FLYEMOJ1 commented 2026-07-28 12:21:51 +09:00 (Migrated from github.com)

问题

break-glass 恢复模式下的管理员身份确认目前只是"这个用户名存在且 role 是 admin"。密码校验随 passwordless 设计一起去掉了,接替它的 email-OTP 还没做。

权限门本身是有的 —— 要跑到这段代码得先是主机上的 root。缺的是问责:谁触发的这次恢复,无法从任何东西上确认,只能从输入的字符串上读。

证据

Identified by Claude Opus 5 (claude-opus-5) while auditing the break-glass path on 2026-07-28.

cmd/felis/breakglass.go:254-256:

// authenticateAdmin resolves a typed admin username for recovery-mode attribution.
// Password verification is gone (passwordless design); Phase 3 replaces this with
// email-OTP recovery. For now it confirms the named admin exists.

函数体(:257-272)确实只做了三件事:trim、UserByUsername、u.Role != "admin" 判断。

验收

恢复模式要求一次 email-OTP 通过之后才继续,且这次恢复在审计里留下可归因的记录。

备注

依赖 SMTP 能真的发出信 —— 也就是说至少要先修掉「Re-apply the felis-api Deployment on existing installs so FELIS_SMTP_PASSWORD exists」。

有个先有鸡还是先有蛋的问题要一起想清楚:break-glass 存在的意义之一就是正常路径挂了的时候还能进去。如果 OTP 走的是同一套 SMTP,那 SMTP 本身挂掉时恢复模式也跟着不可用。可能需要一条明确的降级路径(比如带审计的强制跳过),而不是硬性依赖。

## 问题 break-glass 恢复模式下的管理员身份确认目前只是"这个用户名存在且 role 是 admin"。密码校验随 passwordless 设计一起去掉了,接替它的 email-OTP 还没做。 权限门本身是有的 —— 要跑到这段代码得先是主机上的 root。缺的是**问责**:谁触发的这次恢复,无法从任何东西上确认,只能从输入的字符串上读。 ## 证据 Identified by Claude Opus 5 (claude-opus-5) while auditing the break-glass path on 2026-07-28. `cmd/felis/breakglass.go:254-256`: ```go // authenticateAdmin resolves a typed admin username for recovery-mode attribution. // Password verification is gone (passwordless design); Phase 3 replaces this with // email-OTP recovery. For now it confirms the named admin exists. ``` 函数体(`:257-272`)确实只做了三件事:trim、`UserByUsername`、`u.Role != "admin"` 判断。 ## 验收 恢复模式要求一次 email-OTP 通过之后才继续,且这次恢复在审计里留下可归因的记录。 ## 备注 依赖 SMTP 能真的发出信 —— 也就是说至少要先修掉「Re-apply the felis-api Deployment on existing installs so `FELIS_SMTP_PASSWORD` exists」。 有个先有鸡还是先有蛋的问题要一起想清楚:break-glass 存在的意义之一就是正常路径挂了的时候还能进去。如果 OTP 走的是同一套 SMTP,那 SMTP 本身挂掉时恢复模式也跟着不可用。可能需要一条明确的降级路径(比如带审计的强制跳过),而不是硬性依赖。
Lemon-miaow commented 2026-09-26 09:27:22 +09:00 (Migrated from github.com)

Done in 8bc3997.

Once a staff account exists, felis breakGlass asks which admin or owner is breaking the glass. It then mails a six-digit code (crypto/rand, 10 min, 5 tries, constant-time compare) to that account's verified address, through the same [smtp] relay the watchdog uses: the password_ref env var first, then the felis-smtp Secret, else no AUTH. Only that code makes the run break_glass.recovery with verified: true, verified_by: email_otp, code_sent_to.

Every other ending goes to the typed OVERRIDE, and the screen says why: unknown admin, no verified email, no relay, send failure, expired code, five wrong codes, or the operator typing OVERRIDE at the prompt. The audit row is break_glass.root_override with verified: false, otp_skipped and otp_skip_detail. Adding an Operator account follows the same flow. The mail names the host and OS user, so an admin who did not ask for it learns that someone else has root there.

Tests: cmd/felis/breakglass_otp_test.go plus the updated breakglass_test.go. They cover the code rules, beginRecovery's six skip paths and its datastore error, the relay password source, and the whole console flow through the huh forms. 25 mutants of the new code were all killed (examples: wrong code accepted, expiry off by one, attempt limit, unverified email accepted, skip reason swapped, verified forced true, NotFound turned into an error).

VM (b109, real felis.toml and database):

  • Degraded path, a pty driving the real TUI as root: naming the owner shows Root override — No mail relay can send a recovery code: [smtp] is not configured in felis.toml. Esc, Esc exits with cancelled — no changes made.
  • Happy path, a drill binary with the real database, the real cluster (felis-smtp Secret absent, so no AUTH) and [smtp] pointed at a local sink:
    • the bilingual mail reached the sink;
    • a wrong code gave That code is wrong. 4 attempts left.;
    • the mailed code moved the run to the provision step as recovery, verified_by=email_otp.
  • Zero break_glass audit rows were written by either drill.

Docs: troubleshooting §17 "felis breakGlass: the recovery code and the OVERRIDE".

Done in 8bc3997. Once a staff account exists, `felis breakGlass` asks which admin or owner is breaking the glass. It then mails a six-digit code (crypto/rand, 10 min, 5 tries, constant-time compare) to that account's **verified** address, through the same `[smtp]` relay the watchdog uses: the `password_ref` env var first, then the `felis-smtp` Secret, else no AUTH. Only that code makes the run `break_glass.recovery` with `verified: true, verified_by: email_otp, code_sent_to`. Every other ending goes to the typed OVERRIDE, and the screen says why: unknown admin, no verified email, no relay, send failure, expired code, five wrong codes, or the operator typing OVERRIDE at the prompt. The audit row is `break_glass.root_override` with `verified: false`, `otp_skipped` and `otp_skip_detail`. Adding an Operator account follows the same flow. The mail names the host and OS user, so an admin who did not ask for it learns that someone else has root there. **Tests:** `cmd/felis/breakglass_otp_test.go` plus the updated `breakglass_test.go`. They cover the code rules, `beginRecovery`'s six skip paths and its datastore error, the relay password source, and the whole console flow through the huh forms. 25 mutants of the new code were all killed (examples: wrong code accepted, expiry off by one, attempt limit, unverified email accepted, skip reason swapped, `verified` forced true, NotFound turned into an error). **VM (b109, real felis.toml and database):** - Degraded path, a pty driving the real TUI as root: naming the owner shows `Root override — No mail relay can send a recovery code: [smtp] is not configured in felis.toml.` Esc, Esc exits with `cancelled — no changes made.` - Happy path, a drill binary with the real database, the real cluster (felis-smtp Secret absent, so no AUTH) and `[smtp]` pointed at a local sink: - the bilingual mail reached the sink; - a wrong code gave `That code is wrong. 4 attempts left.`; - the mailed code moved the run to the provision step as `recovery`, `verified_by=email_otp`. - Zero `break_glass` audit rows were written by either drill. Docs: troubleshooting §17 "`felis breakGlass`: the recovery code and the OVERRIDE".
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: FelisMC/Felis#13