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
This commit is contained in:
parent
94a349c406
commit
1801f09c6a
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
core: added changes for user lockout workflow.
|
||||
```
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue