From 1801f09c6aa03b39c72ba97bc7fe48415c860bce Mon Sep 17 00:00:00 2001 From: akshya96 <87045294+akshya96@users.noreply.github.com> Date: Tue, 6 Dec 2022 17:22:46 -0800 Subject: [PATCH] Vault 8307 user lockout workflow oss (#17951) * adding oss file changes * check disabled and read values from config * isUserLocked, getUserLockout Configurations, check user lock before login and return error * remove stale entry from storage during read * added failed login process workflow * success workflow updated * user lockouts external tests * changing update to support delete * provide access to alias look ahead function * adding path alias lookahead * adding tests * added changelog * added comments * adding changes from ent branch * adding lock to UpdateUserFailedLoginInfo * fix return default bug --- changelog/17951.txt | 3 + internalshared/configutil/userlockout.go | 4 + vault/core.go | 36 ++ .../identity/userlockouts_test.go | 326 ++++++++++++++++ vault/request_handling.go | 353 +++++++++++++++++- vault/request_handling_util.go | 8 +- 6 files changed, 720 insertions(+), 10 deletions(-) create mode 100644 changelog/17951.txt create mode 100644 vault/external_tests/identity/userlockouts_test.go diff --git a/changelog/17951.txt b/changelog/17951.txt new file mode 100644 index 000000000..06dd7a4a5 --- /dev/null +++ b/changelog/17951.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core: added changes for user lockout workflow. +``` \ No newline at end of file diff --git a/internalshared/configutil/userlockout.go b/internalshared/configutil/userlockout.go index f2be4461b..ccf51b23b 100644 --- a/internalshared/configutil/userlockout.go +++ b/internalshared/configutil/userlockout.go @@ -32,6 +32,10 @@ type UserLockout struct { DisableLockoutRaw interface{} `hcl:"disable_lockout"` } +func GetSupportedUserLockoutsAuthMethods() []string { + return []string{"userpass", "approle", "ldap"} +} + func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error { var err error result.UserLockouts = make([]*UserLockout, 0, len(list.Items)) diff --git a/vault/core.go b/vault/core.go index 3fb008bc7..b3808096c 100644 --- a/vault/core.go +++ b/vault/core.go @@ -525,6 +525,9 @@ type Core struct { // and login counter, last failed login time as value userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo + // userFailedLoginInfoLock controls access to the userFailedLoginInfoMap + userFailedLoginInfoLock sync.RWMutex + enableMlock bool // This can be used to trigger operations to stop running when Vault is @@ -3455,6 +3458,39 @@ func (c *Core) DetermineRoleFromLoginRequest(mountPoint string, data map[string] return resp.Data["role"].(string) } +// aliasNameFromLoginRequest will determine the aliasName from the login Request +func (c *Core) aliasNameFromLoginRequest(ctx context.Context, req *logical.Request) (string, error) { + c.authLock.RLock() + defer c.authLock.RUnlock() + ns, err := namespace.FromContext(ctx) + if err != nil { + return "", err + } + + // ns path is added while checking matching backend + mountPath := strings.TrimPrefix(req.MountPoint, ns.Path) + + matchingBackend := c.router.MatchingBackend(ctx, mountPath) + if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential { + // pathLoginAliasLookAhead operation does not apply to this request + return "", nil + } + + path := strings.ReplaceAll(req.Path, mountPath, "") + + resp, err := matchingBackend.HandleRequest(ctx, &logical.Request{ + MountPoint: req.MountPoint, + Path: path, + Operation: logical.AliasLookaheadOperation, + Data: req.Data, + Storage: c.router.MatchingStorageByAPIPath(ctx, req.Path), + }) + if err != nil || resp.Auth.Alias == nil { + return "", nil + } + return resp.Auth.Alias.Name, nil +} + // ListMounts will provide a slice containing a deep copy each mount entry func (c *Core) ListMounts() ([]*MountEntry, error) { c.mountsLock.RLock() diff --git a/vault/external_tests/identity/userlockouts_test.go b/vault/external_tests/identity/userlockouts_test.go new file mode 100644 index 000000000..d686ab92a --- /dev/null +++ b/vault/external_tests/identity/userlockouts_test.go @@ -0,0 +1,326 @@ +package identity + +import ( + "strings" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/userpass" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +// TestIdentityStore_UserLockoutTest tests that the user gets locked after +// more than 1 failed login request than the number specified for +// lockout threshold field in user lockout configuration. It also +// tests that the user gets unlocked after the duration specified +// for lockout duration field has passed +func TestIdentityStore_UserLockoutTest(t *testing.T) { + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + active := cluster.Cores[0].Client + standby := cluster.Cores[1].Client + + err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + // tune auth mount + userlockoutConfig := &api.UserLockoutConfigInput{ + LockoutThreshold: "3", + LockoutDuration: "5s", + LockoutCounterResetDuration: "5s", + } + err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{ + UserLockoutConfig: userlockoutConfig, + }) + if err != nil { + t.Fatal(err) + } + + // create a user for userpass + _, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal(err) + } + + // login failure count 1 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 2 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 3 + active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login : permission denied as user locked out + _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "training", + }) + if err == nil { + t.Fatal("expected login to fail as user locked out") + } + if !strings.Contains(err.Error(), logical.ErrPermissionDenied.Error()) { + t.Fatalf("expected to see permission denied error as user locked out, got %v", err) + } + + time.Sleep(5 * time.Second) + + // login with right password and wait for user to get unlocked + _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal("expected login to succeed as user is unlocked") + } +} + +// TestIdentityStore_UserFailedLoginMapResetOnSuccess tests that +// the user lockout feature is reset for a user after one successfull attempt +// after multiple failed login attempts (within lockout threshold) +func TestIdentityStore_UserFailedLoginMapResetOnSuccess(t *testing.T) { + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + // tune auth mount + userlockoutConfig := &api.UserLockoutConfigInput{ + LockoutThreshold: "3", + LockoutDuration: "5s", + LockoutCounterResetDuration: "5s", + } + err = client.Sys().TuneMount("auth/userpass", api.MountConfigInput{ + UserLockoutConfig: userlockoutConfig, + }) + if err != nil { + t.Fatal(err) + } + + // create a user for userpass + _, err = client.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal(err) + } + + // login failure count 1 + client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 2 + client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login with right credentials - successful login + // entry for this user is removed from userFailedLoginInfo map + _, err = client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal(err) + } + + // login failure count 3, is now count 1 after successful login + client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 4, is now count 2 after successful login + // error should not be permission denied as user not locked out + _, err = client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + if err == nil { + t.Fatal("expected login to fail due to wrong credentials") + } + if !strings.Contains(err.Error(), "invalid username or password") { + t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err) + } +} + +// TestIdentityStore_DisableUserLockoutTest tests that user login will +// fail when supplied with wrong credentials. If the user is locked, +// it returns permission denied. In this case, it returns invalid user +// credentials error as the user lockout feature is disabled and the +// user did not get locked after multiple failed login attempts +func TestIdentityStore_DisableUserLockoutTest(t *testing.T) { + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + active := cluster.Cores[0].Client + standby := cluster.Cores[1].Client + + err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + // tune auth mount + disableLockout := true + userlockoutConfig := &api.UserLockoutConfigInput{ + LockoutThreshold: "3", + DisableLockout: &disableLockout, + } + err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{ + UserLockoutConfig: userlockoutConfig, + }) + if err != nil { + t.Fatal(err) + } + + // create a userpass user + _, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal(err) + } + + // login failure count 1 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 2 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 3 + active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // login failure count 4 + _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + if err == nil { + t.Fatal("expected login to fail due to wrong credentials") + } + if !strings.Contains(err.Error(), "invalid username or password") { + t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err) + } +} + +// TestIdentityStore_LockoutCounterResetTest tests that the user lockout counter +// for a user is reset after no failed login attempts for a duration +// as specified for lockout counter reset field in user lockout configuration +func TestIdentityStore_LockoutCounterResetTest(t *testing.T) { + coreConfig := &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + active := cluster.Cores[0].Client + standby := cluster.Cores[1].Client + + err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + // tune auth mount + userlockoutConfig := &api.UserLockoutConfigInput{ + LockoutThreshold: "3", + LockoutCounterResetDuration: "5s", + } + err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{ + UserLockoutConfig: userlockoutConfig, + }) + if err != nil { + t.Fatal(err) + } + + // create a user for userpass + _, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{ + "password": "training", + }) + if err != nil { + t.Fatal(err) + } + + // login failure count 1 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + // login failure count 2 + standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + + // set sleep timer to reset login counter + time.Sleep(5 * time.Second) + + // login failure 3, count should be reset, this will be treated as failed count 1 + active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + // login failure 4, this will be treated as failed count 2 + _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{ + "password": "wrongPassword", + }) + if err == nil { + t.Fatal("expected login to fail due to wrong credentials") + } + if !strings.Contains(err.Error(), "invalid username or password") { + t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err) + } +} diff --git a/vault/request_handling.go b/vault/request_handling.go index 4a5bb5da4..d0f9e4281 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "strconv" "strings" "time" @@ -17,6 +18,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/command/server" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/identity/mfa" "github.com/hashicorp/vault/helper/metricsutil" @@ -37,6 +39,8 @@ import ( const ( replTimeout = 1 * time.Second EnvVaultDisableLocalAuthMountEntities = "VAULT_DISABLE_LOCAL_AUTH_MOUNT_ENTITIES" + // base path to store locked users + coreLockedUsersPath = "core/login/lockedUsers/" ) var ( @@ -1325,8 +1329,35 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re return nil, nil, ErrInternalError } + // check if user lockout feature is disabled + isUserLockoutDisabled, err := c.isUserLockoutDisabled(entry) + if err != nil { + return nil, nil, err + } + + // if user lockout feature is not disabled, check if the user is locked + if !isUserLockoutDisabled { + isloginUserLocked, err := c.isUserLocked(ctx, entry, req) + if err != nil { + return nil, nil, err + } + if isloginUserLocked { + return nil, nil, logical.ErrPermissionDenied + } + } + // Route the request resp, routeErr := c.doRouting(ctx, req) + + // if routeErr has invalid credentials error, update the userFailedLoginMap + if routeErr != nil && routeErr == logical.ErrInvalidCredentials { + err := c.failedUserLoginProcess(ctx, entry, req) + if err != nil { + return nil, nil, err + } + return nil, nil, resp.Error() + } + if resp != nil { // If wrapping is used, use the shortest between the request and response var wrapTTL time.Duration @@ -1596,6 +1627,23 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re resp = respTokenCreate } + // Successful login, remove any entry from userFailedLoginInfo map + // if it exists. This is done for batch tokens (for oss & ent) + // For service tokens on oss it is taken care by core RegisterAuth function. + // For service tokens on ent it is taken care by registerAuth RPC calls. + // This update is done as part of registerAuth of RPC calls from standby + // to active node. This is added there to reduce RPC calls + if !isUserLockoutDisabled && (auth.TokenType == logical.TokenTypeBatch) { + loginUserInfoKey := FailedLoginUser{ + aliasName: auth.Alias.Name, + mountAccessor: auth.Alias.MountAccessor, + } + err = updateUserFailedLoginInfo(ctx, c, loginUserInfoKey, nil, true) + if err != nil { + return nil, nil, err + } + } + // if we were already going to return some error from this login, do that. // if not, we will then check if the API is locked for the requesting // namespace, to avoid leaking locked namespaces to unauthenticated clients. @@ -1717,6 +1765,273 @@ func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, re return leaseGenerated, resp, nil } +// failedUserLoginProcess updates the userFailedLoginMap with login count and last failed +// login time for users with failed login attempt +// If the user gets locked for current login attempt, it updates the storage entry too +func (c *Core) failedUserLoginProcess(ctx context.Context, mountEntry *MountEntry, req *logical.Request) error { + // get the user lockout configuration for the user + userLockoutConfiguration := c.getUserLockoutConfiguration(mountEntry) + + // determine the key for userFailedLoginInfo map + loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req) + if err != nil { + return err + } + + // get entry from userFailedLoginInfo map for the key + userFailedLoginInfo, err := getUserFailedLoginInfo(ctx, c, loginUserInfoKey) + if err != nil { + return err + } + + // update the last failed login time with current time + failedLoginInfo := FailedLoginInfo{ + lastFailedLoginTime: int(time.Now().Unix()), + } + + // set the failed login count value for the entry in userFailedLoginInfo map + switch userFailedLoginInfo { + case nil: // entry does not exist in userfailedLoginMap + failedLoginInfo.count = 1 + default: + failedLoginInfo.count = userFailedLoginInfo.count + 1 + + // if counter reset, set the count value to 1 as this gets counted as new entry + lastFailedLoginTime := time.Unix(int64(userFailedLoginInfo.lastFailedLoginTime), 0) + counterResetDuration := userLockoutConfiguration.LockoutCounterReset + if time.Now().After(lastFailedLoginTime.Add(counterResetDuration)) { + failedLoginInfo.count = 1 + } + } + + // update the userFailedLoginInfo map with the updated/new entry + err = updateUserFailedLoginInfo(ctx, c, loginUserInfoKey, &failedLoginInfo, false) + if err != nil { + return err + } + + // if failed login count has reached threshold, create a storage entry as the user got locked + if failedLoginInfo.count >= uint(userLockoutConfiguration.LockoutThreshold) { + // user locked + ns, err := namespace.FromContext(ctx) + if err != nil { + return fmt.Errorf("could not parse namespace from http context: %w", err) + } + storageUserLockoutPath := fmt.Sprintf(coreLockedUsersPath+"%s/%s/%s", ns.ID, loginUserInfoKey.mountAccessor, loginUserInfoKey.aliasName) + compressedBytes, err := jsonutil.EncodeJSONAndCompress(failedLoginInfo.lastFailedLoginTime, nil) + if err != nil { + c.logger.Error("failed to encode or compress failed login user entry", "error", err) + return err + } + + // Create an entry + entry := &logical.StorageEntry{ + Key: storageUserLockoutPath, + Value: compressedBytes, + } + + // Write to the physical backend + if err := c.barrier.Put(ctx, entry); err != nil { + c.logger.Error("failed to persist failed login user entry", "error", err) + return err + } + + } + return nil +} + +// getLoginUserInfoKey gets failedUserLoginInfo map key for login user +func (c *Core) getLoginUserInfoKey(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (FailedLoginUser, error) { + userInfo := FailedLoginUser{} + aliasName, err := c.aliasNameFromLoginRequest(ctx, req) + if err != nil { + return userInfo, err + } + if aliasName == "" { + return userInfo, errors.New("failed to determine alias name from login request") + } + + userInfo.aliasName = aliasName + userInfo.mountAccessor = mountEntry.Accessor + return userInfo, nil +} + +// isUserLockoutDisabled checks if user lockout feature to prevent brute forcing is disabled +// Auth types userpass, ldap and approle support this feature +// precedence: environment var setting >> auth tune setting >> config file setting >> default (enabled) +func (c *Core) isUserLockoutDisabled(mountEntry *MountEntry) (bool, error) { + if !strutil.StrListContains(configutil.GetSupportedUserLockoutsAuthMethods(), mountEntry.Type) { + return true, nil + } + + // check environment variable + var disableUserLockout bool + if disableUserLockoutEnv := os.Getenv(consts.VaultDisableUserLockout); disableUserLockoutEnv != "" { + var err error + disableUserLockout, err = strconv.ParseBool(disableUserLockoutEnv) + if err != nil { + return false, errors.New("Error parsing the environment variable VAULT_DISABLE_USER_LOCKOUT") + } + } + if disableUserLockout { + return true, nil + } + + // read auth tune for mount entry + userLockoutConfigFromMount := mountEntry.Config.UserLockoutConfig + if userLockoutConfigFromMount != nil && userLockoutConfigFromMount.DisableLockout { + return true, nil + } + + // read config for auth type from config file + userLockoutConfiguration := c.getUserLockoutFromConfig(mountEntry.Type) + if userLockoutConfiguration.DisableLockout { + return true, nil + } + + // default + return false, nil +} + +// isUserLocked determines if the login user is locked +func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (locked bool, err error) { + // get userFailedLoginInfo map key for login user + loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req) + if err != nil { + return false, err + } + + // get entry from userFailedLoginInfo map for the key + userFailedLoginInfo, err := getUserFailedLoginInfo(ctx, c, loginUserInfoKey) + if err != nil { + return false, err + } + userLockoutConfiguration := c.getUserLockoutConfiguration(mountEntry) + + switch userFailedLoginInfo { + case nil: + // entry not found in userFailedLoginInfo map, check storage to re-verify + ns, err := namespace.FromContext(ctx) + if err != nil { + return false, fmt.Errorf("could not parse namespace from http context: %w", err) + } + storageUserLockoutPath := fmt.Sprintf(coreLockedUsersPath+"%s/%s/%s", ns.ID, loginUserInfoKey.mountAccessor, loginUserInfoKey.aliasName) + existingEntry, err := c.barrier.Get(ctx, storageUserLockoutPath) + if err != nil { + return false, err + } + var lastLoginTime int + if existingEntry == nil { + // no storage entry found, user is not locked + return false, nil + } + + err = jsonutil.DecodeJSON(existingEntry.Value, &lastLoginTime) + if err != nil { + return false, err + } + + // if time passed from last login time is within lockout duration, the user is locked + if time.Now().Unix()-int64(lastLoginTime) < int64(userLockoutConfiguration.LockoutDuration.Seconds()) { + // user locked + return true, nil + } else { + // user is not locked. Entry is stale, remove this from storage + if err := c.barrier.Delete(ctx, storageUserLockoutPath); err != nil { + c.logger.Error("failed to cleanup storage entry for user", "path", storageUserLockoutPath, "error", err) + } + } + + default: + // entry found in userFailedLoginInfo map, check if the user is locked + isCountOverLockoutThreshold := userFailedLoginInfo.count >= uint(userLockoutConfiguration.LockoutThreshold) + isWithinLockoutDuration := time.Now().Unix()-int64(userFailedLoginInfo.lastFailedLoginTime) < int64(userLockoutConfiguration.LockoutDuration.Seconds()) + + if isCountOverLockoutThreshold && isWithinLockoutDuration { + // user locked + return true, nil + } + } + return false, nil +} + +// getUserLockoutConfiguration gets the user lockout configuration for a mount entry +// it checks the config file and auth tune values +// precedence: auth tune >> config file values for auth type >> config file values for all type +// >> default user lockout values +// getUserLockoutFromConfig call in this function takes care of config file precedence +func (c *Core) getUserLockoutConfiguration(mountEntry *MountEntry) (userLockoutConfig UserLockoutConfig) { + // get user configuration values from config file + userLockoutConfig = c.getUserLockoutFromConfig(mountEntry.Type) + + authTuneUserLockoutConfig := mountEntry.Config.UserLockoutConfig + // if user lockout is not configured using auth tune, return values from config file + if authTuneUserLockoutConfig == nil { + return userLockoutConfig + } + // replace values in return with config file configuration + // for fields that are not configured using auth tune + if authTuneUserLockoutConfig.LockoutThreshold != 0 { + userLockoutConfig.LockoutThreshold = authTuneUserLockoutConfig.LockoutThreshold + } + if authTuneUserLockoutConfig.LockoutDuration != 0 { + userLockoutConfig.LockoutDuration = authTuneUserLockoutConfig.LockoutDuration + } + if authTuneUserLockoutConfig.LockoutCounterReset != 0 { + userLockoutConfig.LockoutCounterReset = authTuneUserLockoutConfig.LockoutCounterReset + } + if authTuneUserLockoutConfig.DisableLockout { + userLockoutConfig.DisableLockout = authTuneUserLockoutConfig.DisableLockout + } + return userLockoutConfig +} + +// getUserLockoutFromConfig gets the userlockout configuration for given mount type from config file +// it reads the user lockout configuration from server config +// it has values for "all" type and any mountType that is configured using config file +// "all" type values are updated in shared config with default values i.e; if "all" type is +// not configured in config file, it is updated in shared config with default configuration +// If "all" type is configured in config file, any missing fields are updated with default values +// similarly missing values for a given mount type in config file are updated with "all" type +// default values +// If user_lockout configuration is not configured using config file at all, defaults are returned +func (c *Core) getUserLockoutFromConfig(mountType string) UserLockoutConfig { + defaultUserLockoutConfig := UserLockoutConfig{ + LockoutThreshold: configutil.UserLockoutThresholdDefault, + LockoutDuration: configutil.UserLockoutDurationDefault, + LockoutCounterReset: configutil.UserLockoutCounterResetDefault, + DisableLockout: configutil.DisableUserLockoutDefault, + } + conf := c.rawConfig.Load() + if conf == nil { + return defaultUserLockoutConfig + } + userlockouts := conf.(*server.Config).UserLockouts + if userlockouts == nil { + return defaultUserLockoutConfig + } + for _, userLockoutConfig := range userlockouts { + switch userLockoutConfig.Type { + case "all": + defaultUserLockoutConfig = UserLockoutConfig{ + LockoutThreshold: userLockoutConfig.LockoutThreshold, + LockoutDuration: userLockoutConfig.LockoutDuration, + LockoutCounterReset: userLockoutConfig.LockoutCounterReset, + DisableLockout: userLockoutConfig.DisableLockout, + } + case mountType: + return UserLockoutConfig{ + LockoutThreshold: userLockoutConfig.LockoutThreshold, + LockoutDuration: userLockoutConfig.LockoutDuration, + LockoutCounterReset: userLockoutConfig.LockoutCounterReset, + DisableLockout: userLockoutConfig.DisableLockout, + } + + } + } + return defaultUserLockoutConfig +} + func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) { mfaAny := &logical.MFAConstraintAny{ Any: []*logical.MFAMethodID{}, @@ -1810,23 +2125,49 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st if te.ExternalID != "" { auth.ClientToken = te.ExternalID } + // Successful login, remove any entry from userFailedLoginInfo map + // if it exists. This is done for service tokens (for oss) here. + // For ent it is taken care by registerAuth RPC calls. + if auth.Alias != nil { + loginUserInfoKey := FailedLoginUser{ + aliasName: auth.Alias.Name, + mountAccessor: auth.Alias.MountAccessor, + } + err = c.UpdateUserFailedLoginInfo(ctx, loginUserInfoKey, nil, true) + if err != nil { + return err + } + } } - return nil } // GetUserFailedLoginInfo gets the failed login information for a user based on alias name and mountAccessor func (c *Core) GetUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser) *FailedLoginInfo { - return c.userFailedLoginInfo[userKey] + c.userFailedLoginInfoLock.Lock() + value, exists := c.userFailedLoginInfo[userKey] + c.userFailedLoginInfoLock.Unlock() + if exists { + return value + } + return nil } // UpdateUserFailedLoginInfo updates the failed login information for a user based on alias name and mountAccessor -func (c *Core) UpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo FailedLoginInfo) error { - c.userFailedLoginInfo[userKey] = &failedLoginInfo - +func (c *Core) UpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error { + c.userFailedLoginInfoLock.Lock() + switch deleteEntry { + case false: + // create or update entry in the map + c.userFailedLoginInfo[userKey] = failedLoginInfo + default: + // delete the entry from the map + delete(c.userFailedLoginInfo, userKey) + } + c.userFailedLoginInfoLock.Unlock() // check if the update worked failedLoginResp := c.GetUserFailedLoginInfo(ctx, userKey) - if failedLoginResp == nil { + if (failedLoginResp == nil && !deleteEntry) || (failedLoginResp != nil && deleteEntry) { return fmt.Errorf("failed to update entry in userFailedLoginInfo map") } return nil diff --git a/vault/request_handling_util.go b/vault/request_handling_util.go index 0c86363c2..c4a86b63c 100644 --- a/vault/request_handling_util.go +++ b/vault/request_handling_util.go @@ -50,12 +50,12 @@ func getAuthRegisterFunc(c *Core) (RegisterAuthFunc, error) { return c.RegisterAuth, nil } -func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) *FailedLoginInfo { - return c.GetUserFailedLoginInfo(ctx, userInfo) +func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) (*FailedLoginInfo, error) { + return c.GetUserFailedLoginInfo(ctx, userInfo), nil } -func (c *Core) updateUserFailedLoginInfo(ctx context.Context, userInfo FailedLoginUser, failedLoginInfo FailedLoginInfo) error { - return c.UpdateUserFailedLoginInfo(ctx, userInfo, failedLoginInfo) +func updateUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error { + return c.UpdateUserFailedLoginInfo(ctx, userInfo, failedLoginInfo, deleteEntry) } func possiblyForwardAliasCreation(ctx context.Context, c *Core, inErr error, auth *logical.Auth, entity *identity.Entity) (*identity.Entity, bool, error) {