HashiCorp Vault improper configuration of multi factor authentication
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
1.10.0 → fixed in 1.10.3
Details
HashiCorp Vault and Vault Enterprise from 1.10.0 to 1.10.2 did not correctly configure and enforce MFA on login after server restarts. This affects the Login MFA feature introduced in Vault and Vault Enterprise 1.10.0 and does not affect the separate Enterprise MFA feature set. Fixed in 1.10.3.
The fix
loading MFA configs upont restart (#15261) (#15308)
changelog/15261.txt+3 −0
@@ -0,0 +1,3 @@+```release-note:bug+auth: load login MFA configuration upon restart+```
vault/core.go+36 −7
@@ -35,6 +35,7 @@ import ("github.com/hashicorp/vault/api""github.com/hashicorp/vault/audit""github.com/hashicorp/vault/command/server"+"github.com/hashicorp/vault/helper/identity/mfa""github.com/hashicorp/vault/helper/metricsutil""github.com/hashicorp/vault/helper/namespace""github.com/hashicorp/vault/physical/raft"@@ -2113,7 +2114,6 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, cif err := c.setupQuotas(ctx, false); err != nil {return err}-c.setupCachedMFAResponseAuth()if err := c.setupHeaderHMACKey(ctx, false); err != nil {return err@@ -2135,9 +2135,14 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, cif err := c.loadIdentityStoreArtifacts(ctx); err != nil {return err}-if err := loadMFAConfigs(ctx, c); err != nil {+if err := loadPolicyMFAConfigs(ctx, c); err != nil {return err}+c.setupCachedMFAResponseAuth()+if err := c.loadLoginMFAConfigs(ctx); err != nil {+return err+}+if err := c.setupAuditedHeadersConfig(ctx); err != nil {return err}@@ -2299,10 +2304,6 @@ func (c *Core) preSeal() error {result = multierror.Append(result, fmt.Errorf("error stopping expiration: %w", err))}c.stopActivityLog()-// Clear any cached auth response-c.mfaResponseAuthQueueLock.Lock()-c.mfaResponseAuthQueue = nil-c.mfaResponseAuthQueueLock.Unlock()if err := c.teardownCredentials(context.Background()); err != nil {result = multierror.Append(result, fmt.Errorf("error tearing down credentials: %w", err))@@ -2330,10 +2331,13 @@ func (c *Core) preSeal() error {seal.StopHealthCheck()}-c.loginMFABackend.usedCodes = nilif c.systemBackend != nil && c.systemBackend.mfaBackend != nil {c.systemBackend.mfaBackend.usedCodes = nil}+if err := c.teardownLoginMFA(); err != nil {+result = multierror.Append(result, fmt.Errorf("error tearing down login MFA, error: %w", err))+}+preSealPhysical(c)c.logger.Info("pre-seal teardown complete")@@ -3047,6 +3051,31 @@ type LicenseState struct {Terminated bool}+func (c *Core) loadLoginMFAConfigs(ctx context.Context) error {+eConfigs := make([]*mfa.MFAEnforcementConfig, 0)+allNamespaces := c.collectNamespaces()+for _, ns := range allNamespaces {+err := c.loginMFABackend.loadMFAMethodConfigs(ctx, ns)+if err != nil {+return fmt.Errorf("error loading MFA method Config, namespaceid %s, error: %w", ns.ID, err)+}++loadedConfigs, err := c.loginMFABackend.loadMFAEnforcementConfigs(ctx, ns)+if err != nil {+return fmt.Errorf("error loading MFA enforcement Config, namespaceid %s, error: %w", ns.ID, err)+}++eConfigs = append(eConfigs, loadedConfigs...)+}++for _, conf := range eConfigs {+if err := c.loginMFABackend.loginMFAMethodExistenceCheck(conf); err != nil {+c.loginMFABackend.mfaLogger.Error("failed to find all MFA methods that exist in MFA enforcement configs", "configID", conf.ID, "namespaceID", conf.NamespaceID, "error", err.Error())+}+}+return nil+}+type MFACachedAuthResponse struct {CachedAuth *logical.AuthRequestPath string
vault/core_util.go+1 −1
@@ -115,7 +115,7 @@ func postUnsealPhysical(c *Core) error {return nil}-func loadMFAConfigs(context.Context, *Core) error { return nil }+func loadPolicyMFAConfigs(context.Context, *Core) error { return nil }func shouldStartClusterListener(*Core) bool { return true }
vault/login_mfa.go+161 −30
@@ -122,6 +122,18 @@ func NewLoginMFABackend(core *Core, logger hclog.Logger) *LoginMFABackend {}func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema) *MFABackend {+db, _ := SetupMFAMemDB(schemaFuncs)+return &MFABackend{+Core: core,+mfaLock: &sync.RWMutex{},+db: db,+mfaLogger: logger.Named("mfa"),+namespacer: core,+methodTable: prefix,+}+}++func SetupMFAMemDB(schemaFuncs []func() *memdb.TableSchema) (*memdb.MemDB, error) {mfaSchemas := &memdb.DBSchema{Tables: make(map[string]*memdb.TableSchema),}@@ -134,15 +146,24 @@ func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs [mfaSchemas.Tables[schema.Name] = schema}-db, _ := memdb.NewMemDB(mfaSchemas)-return &MFABackend{-Core: core,-mfaLock: &sync.RWMutex{},-db: db,-mfaLogger: logger.Named("mfa"),-namespacer: core,-methodTable: prefix,+db, err := memdb.NewMemDB(mfaSchemas)+if err != nil {+return nil, err}+return db, nil+}++func (b *LoginMFABackend) ResetLoginMFAMemDB() error {+var err error++db, err := SetupMFAMemDB(loginMFASchemaFuncs())+if err != nil {+return err+}++b.db = db++return nil}func (i *IdentityStore) handleMFAMethodListTOTP(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {@@ -474,6 +495,103 @@ func (i *IdentityStore) handleLoginMFAAdminDestroyUpdate(ctx context.Context, rereturn nil, nil}+// loadMFAMethodConfigs loads MFA method configs for login MFA+func (b *LoginMFABackend) loadMFAMethodConfigs(ctx context.Context, ns *namespace.Namespace) error {+b.mfaLogger.Trace("loading login MFA configurations")+barrierView, err := b.Core.barrierViewForNamespace(ns.ID)+if err != nil {+return fmt.Errorf("error getting namespace view, namespaceid %s, error %w", ns.ID, err)+}+existing, err := barrierView.List(ctx, loginMFAConfigPrefix)+if err != nil {+return fmt.Errorf("failed to list MFA configurations for namespace path %s and prefix %s: %w", ns.Path, loginMFAConfigPrefix, err)+}+b.mfaLogger.Trace("methods collected", "num_existing", len(existing))++for _, key := range existing {+b.mfaLogger.Trace("loading method", "method", key)++// Read the config from storage+mConfig, err := b.getMFAConfig(ctx, loginMFAConfigPrefix+key, barrierView)+if err != nil {+return err+}++if mConfig == nil {+b.mfaLogger.Trace("failed to find the config related to a method", "namespace", ns.Path, "prefix", loginMFAConfigPrefix, "method", key)+continue+}++// Load the config in MemDB+err = b.MemDBUpsertMFAConfig(ctx, mConfig)+if err != nil {+return fmt.Errorf("failed to load configuration ID %s prefix %s in MemDB: %w", mConfig.ID, loginMFAConfigPrefix, err)+}+}++b.mfaLogger.Trace("configurations restored", "namespace", ns.Path, "prefix", loginMFAConfigPrefix)++return nil+}++// loadMFAEnforcementConfigs loads MFA method configs for login MFA+func (b *LoginMFABackend) loadMFAEnforcementConfigs(ctx context.Context, ns *namespace.Namespace) ([]*mfa.MFAEnforcementConfig, error) {+b.mfaLogger.Trace("loading login MFA enforcement configurations")+barrierView, err := b.Core.barrierViewForNamespace(ns.ID)+if err != nil {+return nil, fmt.Errorf("error getting namespace view, namespaceid %s, error %w", ns.ID, err)+}+existing, err := barrierView.List(ctx, mfaLoginEnforcementPrefix)+if err != nil {+return nil, fmt.Errorf("failed to list MFA enforcement configurations for namespace %s with prefix %s: %w", ns.Path, mfaLoginEnforcementPrefix, err)+}+b.mfaLogger.Trace("enforcements configs collected", "num_existing", len(existing))++eConfigs := make([]*mfa.MFAEnforcementConfig, 0)+for _, key := range existing {+b.mfaLogger.Trace("loading enforcement", "config", key)++// Read the config from storage+mConfig, err := b.getMFALoginEnforcementConfig(ctx, mfaLoginEnforcementPrefix+key, barrierView)+if err != nil {+return nil, err+}++if mConfig == nil {+b.mfaLogger.Trace("failed to find an enforcement config", "namespace", ns.Path, "prefix", mfaLoginEnforcementPrefix, "config", key)+continue+}++// Load the config in MemDB+err = b.MemDBUpsertMFALoginEnforcementConfig(ctx, mConfig)+if err != nil {+return nil, fmt.Errorf("failed to load enforcement configuration ID %s with prefix %s in MemDB: %w", mConfig.ID, mfaLoginEnforcementPrefix, err)+}++eConfigs = append(eConfigs, mConfig)+}++b.mfaLogger.Trace("enforcement configurations restored", "namespace", ns.Path, "prefix", mfaLoginEnforcementPrefix)++return eConfigs, nil+}++func (b *LoginMFABackend) loginMFAMethodExistenceCheck(eConfig *mfa.MFAEnforcementConfig) error {+var aggErr *multierror.Error+for _, confID := range eConfig.MFAMethodIDs {+config, memErr := b.MemDBMFAConfigByID(confID)+if memErr != nil {+aggErr = multierror.Append(aggErr, memErr)+return aggErr.ErrorOrNil()+}+if config == nil {+aggErr = multierror.Append(aggErr, fmt.Errorf("found an MFA method ID in enforcement config, but failed to find the MFA method config method ID %s", confID))+}+}++return aggErr.ErrorOrNil()+}+func (b *LoginMFABackend) handleMFALoginValidate(ctx context.Context, req *logical.Request, d *framework.FieldData) (retResp *logical.Response, retErr error) {// mfaReqID is the ID of the login requestmfaReqID := d.Get("mfa_request_id").(string)@@ -551,6 +669,22 @@ func (b *LoginMFABackend) handleMFALoginValidate(ctx context.Context, req *logicreturn resp, nil}+func (c *Core) teardownLoginMFA() error {+if !c.IsDRSecondary() {+// Clear any cached auth response+c.mfaResponseAuthQueueLock.Lock()+c.mfaResponseAuthQueue = nil+c.mfaResponseAuthQueueLock.Unlock()++c.loginMFABackend.usedCodes = nil++if err := c.loginMFABackend.ResetLoginMFAMemDB(); err != nil {+return err+}+}+return nil+}+// LoginMFACreateToken creates a token after the login MFA is validated.// It also applies the lease quotas on the original login request path.func (c *Core) LoginMFACreateToken(ctx context.Context, reqPath string, cachedAuth *logical.Auth) (*logical.Response, error) {@@ -2529,6 +2663,25 @@ func (b *MFABackend) getMFAConfig(ctx context.Context, path string, barrierViewreturn &mConfig, nil}+func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, path string, barrierView *BarrierView) (*mfa.MFAEnforcementConfig, error) {+entry, err := barrierView.Get(ctx, path)+if err != nil {+return nil, err+}++if entry == nil {+return nil, nil+}++var mConfig mfa.MFAEnforcementConfig+err = proto.Unmarshal(entry.Value, &mConfig)+if err != nil {+return nil, err+}++return &mConfig, nil+}+func (b *LoginMFABackend) putMFALoginEnforcementConfig(ctx context.Context, eConfig *mfa.MFAEnforcementConfig) error {entryIndex := mfaLoginEnforcementPrefix + eConfig.IDmarshaledEntry, err := proto.Marshal(eConfig)@@ -2547,28 +2700,6 @@ func (b *LoginMFABackend) putMFALoginEnforcementConfig(ctx context.Context, eCon})}-func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, key, namespaceId string) (*mfa.MFAEnforcementConfig, error) {-barrierView, err := b.Core.barrierViewForNamespace(namespaceId)-if err != nil {-return nil, err-}-entry, err := barrierView.Get(ctx, mfaLoginEnforcementPrefix+key)-if err != nil {-return nil, err-}-if entry == nil {-return nil, nil-}--var eConfig mfa.MFAEnforcementConfig-err = proto.Unmarshal(entry.Value, &eConfig)-if err != nil {-return nil, err-}--return &eConfig, nil-}-var mfaHelp = map[string][2]string{"methods-list": {"Lists all the available MFA methods by their name.",
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2022-30689
- WEBhttps://github.com/hashicorp/vault/commit/15baea5fa3e71c837c33b8bcbd8f06e0fbbc110d
- WEBhttps://discuss.hashicorp.com
- WEBhttps://github.com/hashicorp/vault
- WEBhttps://security.gentoo.org/glsa/202207-01
- WEBhttps://security.netapp.com/advisory/ntap-20220629-0006