feat(cli): attribute break-glass recovery to the SysAdmin who runs it
Root is machine authority, not a human identity, so `felis breakGlass` now also records WHICH SysAdmin broke the glass. Even under `sudo felis breakGlass` an account and password are entered in the TUI; the root gate is necessary but no longer sufficient for accountability. The console resolves one of three modes up front and audits the difference: - bootstrap (no staff account exists yet): the typed credential mints the first Owner; the act is attributed to the OS user ($SUDO_USER, else root) and recorded verified:false. - recovery (an admin already exists): the operator authenticates as an existing admin via bcrypt; the verified identity is the accountable actor and the row is recorded verified:true. - root override (the typed credential did not verify): a deliberate OVERRIDE token proceeds under local-root authority, attributed to the OS user and recorded verified:false. Break-glass never refuses - recovering when no admin password can be produced is its whole job. Attribution is best-effort, not proof (whoever runs this is root and can edit Postgres directly); the audit row is honest about which it is. - internal/api: AuditEntry gains an optional jsonb Payload (nil maps to SQL NULL, so existing callers are unaffected); PGRepo.Audit writes it and a new PGRepo.AdminExists drives the bootstrap-vs-recovery switch. - the accountability row is written the instant the credential changes, before local auth is enabled, so a failed toggle write can never leave a reset credential with no "who did it" record. - local_auth_enabled is now one exported api.LocalAuthEnabledKey shared by the break-glass writer and the per-request reader, replacing two drifting copies of the literal. - break-glass password entry reuses the panel's 8-72-byte rule so a credential set here is never later rejected by web change-password. Covered by Go unit tests over a fake owner store: auth match/non-match, the three audit modes and their payloads, that a dead audit sink does not fail the recovery, that the audit precedes the toggle write, and a headless drive of the TUI state machine asserting no credential reaches provisioning without a verified admin or an explicit OVERRIDE.
This commit is contained in:
6 files changed
+1079
-210
No files matched your search
@@ -28,12 +28,15 @@ const (
|
||||
sessionCookieName = "felis_session"
|
||||
// sessionTTL bounds a local-password session. Staff re-authenticate after it.
|
||||
sessionTTL = 12 * time.Hour
|
||||
// localAuthEnabledKey gates whether local-password sessions are honored. It is
|
||||
// flipped on by `felis breakGlass` direct-to-Postgres at first-run and read
|
||||
// live per-request, so enabling local auth needs no pod roll.
|
||||
localAuthEnabledKey = "local_auth_enabled"
|
||||
)
|
||||
|
||||
// LocalAuthEnabledKey is the platform_settings key that gates whether
|
||||
// local-password sessions are honored. It is flipped on by `felis breakGlass`
|
||||
// direct-to-Postgres at first-run and read live per-request, so enabling local
|
||||
// auth needs no pod roll. Exported so the break-glass writer and this
|
||||
// per-request reader share one source of truth instead of drifting copies.
|
||||
const LocalAuthEnabledKey = "local_auth_enabled"
|
||||
|
||||
// newSessionToken returns a fresh opaque session value (256 bits, URL-safe). It
|
||||
// is the cookie value; only its hash is persisted.
|
||||
func newSessionToken() (string, error) {
|
||||
@@ -161,7 +164,7 @@ func (s SessionAuth) Authenticate(r *http.Request) (*Principal, error) {
|
||||
// (minting one) consult it, so the two never disagree about whether local auth is
|
||||
// live.
|
||||
func localAuthEnabled(ctx context.Context, repo Repo) bool {
|
||||
raw, err := repo.GetSetting(ctx, localAuthEnabledKey)
|
||||
raw, err := repo.GetSetting(ctx, LocalAuthEnabledKey)
|
||||
if err != nil {
|
||||
return false // ErrNotFound (never enabled) or a transient read error → closed
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user