feat: require_mfa setting and user_mfa ciphertext mirror

This commit is contained in:
2026-09-16 08:20:25 +00:00
parent 0e5597fcc0
commit d6ecff6377
3 changed files with 36 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
package backup
import "testing"
// user_mfa holds an encrypted TOTP secret. Absent from this map, verify's live
// probe reports "this database stores no ciphertext yet" and the one gate that
// catches a wrong encryption key becomes a no-op for MFA secrets.
func TestUserMFACiphertextIsMirrored(t *testing.T) {
fields, ok := ciphertextFields["user_mfa"]
if !ok {
t.Fatal("ciphertextFields has no entry for user_mfa")
}
found := false
for _, f := range fields {
if f == "totp_secret_enc" {
found = true
}
}
if !found {
t.Fatalf("user_mfa entry %v does not name totp_secret_enc", fields)
}
}
+2
View File
@@ -128,6 +128,7 @@ func probe(ctx context.Context, opt VerifyOptions, rep *VerifyReport) error {
// secrets - models/secret.go: encrypted_value
// auth_providers - models/auth_provider.go: client_secret_enc
// console_sessions - models/console_session.go: rdp_user_enc, rdp_pass_enc
// user_mfa - models/user_mfa.go: totp_secret_enc
//
// settings is deliberately absent: it holds no ciphertext at all. The ESO read
// token is stored as a SHA-256 hash, which no key opens.
@@ -136,6 +137,7 @@ var ciphertextFields = map[string][]string{
"secrets": {"encrypted_value"},
"auth_providers": {"client_secret_enc"},
"console_sessions": {"rdp_user_enc", "rdp_pass_enc"},
"user_mfa": {"totp_secret_enc"},
}
func findCiphertext(ctx context.Context, db *mongo.Database, coll string) (string, bool, error) {
+12
View File
@@ -35,6 +35,12 @@ type Settings struct {
// upgrade. Nil means enabled.
LocalLoginEnabled *bool `bson:"local_login_enabled,omitempty" json:"local_login_enabled,omitempty"`
// RequireMFA forces every password-authenticated member to hold a second
// factor. A pointer for the same reason LocalLoginEnabled is: absent must
// mean off, and a plain bool read from an old document would lock out an
// entire instance at upgrade.
RequireMFA *bool `bson:"require_mfa,omitempty" json:"require_mfa,omitempty"`
// VulnFindingRetentionDays is a pointer for the same reason
// WorkflowLogRetentionDays is: absent must mean the default, not zero.
// Nil is 90 days, 0 is forever. Only "fixed" findings are ever swept.
@@ -62,6 +68,12 @@ func LocalLoginEnabled(s *Settings) bool {
return *s.LocalLoginEnabled
}
// RequireMFA reads the MFA policy with its absent-means-off default. Every
// caller must go through this rather than dereferencing the field.
func RequireMFA(s *Settings) bool {
return s != nil && s.RequireMFA != nil && *s.RequireMFA
}
// APITokenMaxDays reads the token lifetime cap with its absent-means-uncapped
// default. 0 means no cap. Every caller must go through this rather than
// dereferencing the field.