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"`
|
DisableLockoutRaw interface{} `hcl:"disable_lockout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSupportedUserLockoutsAuthMethods() []string {
|
||||||
|
return []string{"userpass", "approle", "ldap"}
|
||||||
|
}
|
||||||
|
|
||||||
func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
|
func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
|
||||||
var err error
|
var err error
|
||||||
result.UserLockouts = make([]*UserLockout, 0, len(list.Items))
|
result.UserLockouts = make([]*UserLockout, 0, len(list.Items))
|
||||||
|
|
|
@ -525,6 +525,9 @@ type Core struct {
|
||||||
// and login counter, last failed login time as value
|
// and login counter, last failed login time as value
|
||||||
userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo
|
userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo
|
||||||
|
|
||||||
|
// userFailedLoginInfoLock controls access to the userFailedLoginInfoMap
|
||||||
|
userFailedLoginInfoLock sync.RWMutex
|
||||||
|
|
||||||
enableMlock bool
|
enableMlock bool
|
||||||
|
|
||||||
// This can be used to trigger operations to stop running when Vault is
|
// 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)
|
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
|
// ListMounts will provide a slice containing a deep copy each mount entry
|
||||||
func (c *Core) ListMounts() ([]*MountEntry, error) {
|
func (c *Core) ListMounts() ([]*MountEntry, error) {
|
||||||
c.mountsLock.RLock()
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||||
"github.com/hashicorp/go-sockaddr"
|
"github.com/hashicorp/go-sockaddr"
|
||||||
"github.com/hashicorp/go-uuid"
|
"github.com/hashicorp/go-uuid"
|
||||||
|
"github.com/hashicorp/vault/command/server"
|
||||||
"github.com/hashicorp/vault/helper/identity"
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/identity/mfa"
|
"github.com/hashicorp/vault/helper/identity/mfa"
|
||||||
"github.com/hashicorp/vault/helper/metricsutil"
|
"github.com/hashicorp/vault/helper/metricsutil"
|
||||||
|
@ -37,6 +39,8 @@ import (
|
||||||
const (
|
const (
|
||||||
replTimeout = 1 * time.Second
|
replTimeout = 1 * time.Second
|
||||||
EnvVaultDisableLocalAuthMountEntities = "VAULT_DISABLE_LOCAL_AUTH_MOUNT_ENTITIES"
|
EnvVaultDisableLocalAuthMountEntities = "VAULT_DISABLE_LOCAL_AUTH_MOUNT_ENTITIES"
|
||||||
|
// base path to store locked users
|
||||||
|
coreLockedUsersPath = "core/login/lockedUsers/"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -1325,8 +1329,35 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
|
||||||
return nil, nil, ErrInternalError
|
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
|
// Route the request
|
||||||
resp, routeErr := c.doRouting(ctx, req)
|
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 resp != nil {
|
||||||
// If wrapping is used, use the shortest between the request and response
|
// If wrapping is used, use the shortest between the request and response
|
||||||
var wrapTTL time.Duration
|
var wrapTTL time.Duration
|
||||||
|
@ -1596,6 +1627,23 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
|
||||||
resp = respTokenCreate
|
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 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
|
// if not, we will then check if the API is locked for the requesting
|
||||||
// namespace, to avoid leaking locked namespaces to unauthenticated clients.
|
// 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
|
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) {
|
func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) {
|
||||||
mfaAny := &logical.MFAConstraintAny{
|
mfaAny := &logical.MFAConstraintAny{
|
||||||
Any: []*logical.MFAMethodID{},
|
Any: []*logical.MFAMethodID{},
|
||||||
|
@ -1810,23 +2125,49 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st
|
||||||
if te.ExternalID != "" {
|
if te.ExternalID != "" {
|
||||||
auth.ClientToken = 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserFailedLoginInfo gets the failed login information for a user based on alias name and mountAccessor
|
// 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 {
|
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
|
// 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 {
|
func (c *Core) UpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
|
||||||
c.userFailedLoginInfo[userKey] = &failedLoginInfo
|
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
|
// check if the update worked
|
||||||
failedLoginResp := c.GetUserFailedLoginInfo(ctx, userKey)
|
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 fmt.Errorf("failed to update entry in userFailedLoginInfo map")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -50,12 +50,12 @@ func getAuthRegisterFunc(c *Core) (RegisterAuthFunc, error) {
|
||||||
return c.RegisterAuth, nil
|
return c.RegisterAuth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) *FailedLoginInfo {
|
func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) (*FailedLoginInfo, error) {
|
||||||
return c.GetUserFailedLoginInfo(ctx, userInfo)
|
return c.GetUserFailedLoginInfo(ctx, userInfo), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Core) updateUserFailedLoginInfo(ctx context.Context, userInfo FailedLoginUser, failedLoginInfo FailedLoginInfo) error {
|
func updateUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
|
||||||
return c.UpdateUserFailedLoginInfo(ctx, userInfo, failedLoginInfo)
|
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) {
|
func possiblyForwardAliasCreation(ctx context.Context, c *Core, inErr error, auth *logical.Auth, entity *identity.Entity) (*identity.Entity, bool, error) {
|
||||||
|
|
Loading…
Reference in New Issue