mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Implement user lockout log (#23140)
* implement user lockout logger * formatting * make user lockout log interval configurable * create func to get locked user count, and fix potential deadlock * fix test * fix test * add changelog
This commit is contained in:
3
changelog/23140.txt
Normal file
3
changelog/23140.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
core: emit logs when user(s) are locked out and when all lockouts have been cleared
|
||||||
|
```
|
||||||
@@ -3069,6 +3069,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical.
|
|||||||
DisableSSCTokens: config.DisableSSCTokens,
|
DisableSSCTokens: config.DisableSSCTokens,
|
||||||
Experiments: config.Experiments,
|
Experiments: config.Experiments,
|
||||||
AdministrativeNamespacePath: config.AdministrativeNamespacePath,
|
AdministrativeNamespacePath: config.AdministrativeNamespacePath,
|
||||||
|
UserLockoutLogInterval: config.UserLockoutLogInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.flagDev {
|
if c.flagDev {
|
||||||
|
|||||||
@@ -780,6 +780,7 @@ func testConfig_Sanitized(t *testing.T) {
|
|||||||
"enable_response_header_hostname": false,
|
"enable_response_header_hostname": false,
|
||||||
"enable_response_header_raft_node_id": false,
|
"enable_response_header_raft_node_id": false,
|
||||||
"log_requests_level": "basic",
|
"log_requests_level": "basic",
|
||||||
|
"user_lockout_log_interval": 0 * time.Second,
|
||||||
"ha_storage": map[string]interface{}{
|
"ha_storage": map[string]interface{}{
|
||||||
"cluster_addr": "top_level_cluster_addr",
|
"cluster_addr": "top_level_cluster_addr",
|
||||||
"disable_clustering": true,
|
"disable_clustering": true,
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ func TestSysConfigState_Sanitized(t *testing.T) {
|
|||||||
"enable_response_header_hostname": false,
|
"enable_response_header_hostname": false,
|
||||||
"enable_response_header_raft_node_id": false,
|
"enable_response_header_raft_node_id": false,
|
||||||
"log_requests_level": "",
|
"log_requests_level": "",
|
||||||
|
"user_lockout_log_interval": json.Number("0"),
|
||||||
"listeners": []interface{}{
|
"listeners": []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"config": nil,
|
"config": nil,
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ type SharedConfig struct {
|
|||||||
|
|
||||||
Listeners []*Listener `hcl:"-"`
|
Listeners []*Listener `hcl:"-"`
|
||||||
|
|
||||||
UserLockouts []*UserLockout `hcl:"-"`
|
UserLockouts []*UserLockout `hcl:"-"`
|
||||||
|
UserLockoutLogInterval time.Duration `hcl:"-"`
|
||||||
|
UserLockoutLogIntervalRaw interface{} `hcl:"user_lockout_log_interval"`
|
||||||
|
|
||||||
Seals []*KMS `hcl:"-"`
|
Seals []*KMS `hcl:"-"`
|
||||||
Entropy *Entropy `hcl:"-"`
|
Entropy *Entropy `hcl:"-"`
|
||||||
@@ -87,6 +89,14 @@ func ParseConfig(d string) (*SharedConfig, error) {
|
|||||||
result.DisableMlockRaw = nil
|
result.DisableMlockRaw = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result.UserLockoutLogIntervalRaw != nil {
|
||||||
|
if result.UserLockoutLogInterval, err = parseutil.ParseDurationSecond(result.UserLockoutLogIntervalRaw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.FoundKeys = append(result.FoundKeys, "UserLockoutLogInterval")
|
||||||
|
result.UserLockoutLogIntervalRaw = nil
|
||||||
|
}
|
||||||
|
|
||||||
list, ok := obj.Node.(*ast.ObjectList)
|
list, ok := obj.Node.(*ast.ObjectList)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
||||||
@@ -184,6 +194,7 @@ func (c *SharedConfig) Sanitized() map[string]interface{} {
|
|||||||
"pid_file": c.PidFile,
|
"pid_file": c.PidFile,
|
||||||
"cluster_name": c.ClusterName,
|
"cluster_name": c.ClusterName,
|
||||||
"administrative_namespace_path": c.AdministrativeNamespacePath,
|
"administrative_namespace_path": c.AdministrativeNamespacePath,
|
||||||
|
"user_lockout_log_interval": c.UserLockoutLogInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional log related settings
|
// Optional log related settings
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ func (c *SharedConfig) Merge(c2 *SharedConfig) *SharedConfig {
|
|||||||
result.DefaultMaxRequestDuration = c2.DefaultMaxRequestDuration
|
result.DefaultMaxRequestDuration = c2.DefaultMaxRequestDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.UserLockoutLogInterval = c.UserLockoutLogInterval
|
||||||
|
if c2.UserLockoutLogInterval > result.UserLockoutLogInterval {
|
||||||
|
result.UserLockoutLogInterval = c2.UserLockoutLogInterval
|
||||||
|
}
|
||||||
|
|
||||||
result.LogLevel = c.LogLevel
|
result.LogLevel = c.LogLevel
|
||||||
if c2.LogLevel != "" {
|
if c2.LogLevel != "" {
|
||||||
result.LogLevel = c2.LogLevel
|
result.LogLevel = c2.LogLevel
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ const (
|
|||||||
// MfaAuthResponse when the value is not specified in the server config
|
// MfaAuthResponse when the value is not specified in the server config
|
||||||
defaultMFAAuthResponseTTL = 300 * time.Second
|
defaultMFAAuthResponseTTL = 300 * time.Second
|
||||||
|
|
||||||
|
// defaultUserLockoutLogInterval is the default duration that Vault will
|
||||||
|
// emit a log informing that a user lockout is in effect when the value
|
||||||
|
// is not specified in the server config
|
||||||
|
defaultUserLockoutLogInterval = 1 * time.Minute
|
||||||
|
|
||||||
// defaultMaxTOTPValidateAttempts is the default value for the number
|
// defaultMaxTOTPValidateAttempts is the default value for the number
|
||||||
// of failed attempts to validate a request subject to TOTP MFA. If the
|
// of failed attempts to validate a request subject to TOTP MFA. If the
|
||||||
// number of failed totp passcode validations exceeds this max value, the
|
// number of failed totp passcode validations exceeds this max value, the
|
||||||
@@ -673,6 +678,9 @@ type Core struct {
|
|||||||
|
|
||||||
updateLockedUserEntriesCancel context.CancelFunc
|
updateLockedUserEntriesCancel context.CancelFunc
|
||||||
|
|
||||||
|
lockoutLoggerCancel context.CancelFunc
|
||||||
|
userLockoutLogInterval time.Duration
|
||||||
|
|
||||||
// number of workers to use for lease revocation in the expiration manager
|
// number of workers to use for lease revocation in the expiration manager
|
||||||
numExpirationWorkers int
|
numExpirationWorkers int
|
||||||
|
|
||||||
@@ -888,6 +896,8 @@ type CoreConfig struct {
|
|||||||
AdministrativeNamespacePath string
|
AdministrativeNamespacePath string
|
||||||
|
|
||||||
NumRollbackWorkers int
|
NumRollbackWorkers int
|
||||||
|
|
||||||
|
UserLockoutLogInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubloggerHook implements the SubloggerAdder interface. This implementation
|
// SubloggerHook implements the SubloggerAdder interface. This implementation
|
||||||
@@ -935,6 +945,10 @@ func CreateCore(conf *CoreConfig) (*Core, error) {
|
|||||||
return nil, fmt.Errorf("cannot have DefaultLeaseTTL larger than MaxLeaseTTL")
|
return nil, fmt.Errorf("cannot have DefaultLeaseTTL larger than MaxLeaseTTL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.UserLockoutLogInterval == 0 {
|
||||||
|
conf.UserLockoutLogInterval = defaultUserLockoutLogInterval
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the advertise addr if its given to us
|
// Validate the advertise addr if its given to us
|
||||||
if conf.RedirectAddr != "" {
|
if conf.RedirectAddr != "" {
|
||||||
u, err := url.Parse(conf.RedirectAddr)
|
u, err := url.Parse(conf.RedirectAddr)
|
||||||
@@ -1059,6 +1073,7 @@ func CreateCore(conf *CoreConfig) (*Core, error) {
|
|||||||
disableSSCTokens: conf.DisableSSCTokens,
|
disableSSCTokens: conf.DisableSSCTokens,
|
||||||
effectiveSDKVersion: effectiveSDKVersion,
|
effectiveSDKVersion: effectiveSDKVersion,
|
||||||
userFailedLoginInfo: make(map[FailedLoginUser]*FailedLoginInfo),
|
userFailedLoginInfo: make(map[FailedLoginUser]*FailedLoginInfo),
|
||||||
|
userLockoutLogInterval: conf.UserLockoutLogInterval,
|
||||||
experiments: conf.Experiments,
|
experiments: conf.Experiments,
|
||||||
pendingRemovalMountsAllowed: conf.PendingRemovalMountsAllowed,
|
pendingRemovalMountsAllowed: conf.PendingRemovalMountsAllowed,
|
||||||
expirationRevokeRetryBase: conf.ExpirationRevokeRetryBase,
|
expirationRevokeRetryBase: conf.ExpirationRevokeRetryBase,
|
||||||
@@ -3615,6 +3630,51 @@ func (c *Core) setupCachedMFAResponseAuth() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Core) startLockoutLogger() {
|
||||||
|
// Are we already running a logger
|
||||||
|
if c.lockoutLoggerCancel != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancelFunc := context.WithCancel(c.activeContext)
|
||||||
|
c.lockoutLoggerCancel = cancelFunc
|
||||||
|
|
||||||
|
// Perform first check for lockout entries
|
||||||
|
lockedUserCount := c.getUserFailedLoginCount(ctx)
|
||||||
|
|
||||||
|
if lockedUserCount > 0 {
|
||||||
|
c.Logger().Warn("user lockout(s) in effect")
|
||||||
|
} else {
|
||||||
|
// We shouldn't end up here
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start lockout watcher
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(c.userLockoutLogInterval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Check for lockout entries
|
||||||
|
lockedUserCount := c.getUserFailedLoginCount(ctx)
|
||||||
|
|
||||||
|
if lockedUserCount > 0 {
|
||||||
|
c.Logger().Warn("user lockout(s) in effect")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.Logger().Info("user lockout(s) cleared")
|
||||||
|
ticker.Stop()
|
||||||
|
c.lockoutLoggerCancel = nil
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
c.lockoutLoggerCancel = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// updateLockedUserEntries runs every 15 mins to remove stale user entries from storage
|
// updateLockedUserEntries runs every 15 mins to remove stale user entries from storage
|
||||||
// it also updates the userFailedLoginInfo map with correct information for locked users if incorrect
|
// it also updates the userFailedLoginInfo map with correct information for locked users if incorrect
|
||||||
func (c *Core) updateLockedUserEntries() {
|
func (c *Core) updateLockedUserEntries() {
|
||||||
@@ -3643,7 +3703,13 @@ func (c *Core) updateLockedUserEntries() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return
|
}
|
||||||
|
|
||||||
|
func (c *Core) getUserFailedLoginCount(ctx context.Context) int {
|
||||||
|
c.userFailedLoginInfoLock.Lock()
|
||||||
|
defer c.userFailedLoginInfoLock.Unlock()
|
||||||
|
|
||||||
|
return len(c.userFailedLoginInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runLockedUserEntryUpdates runs updates for locked user storage entries and userFailedLoginInfo map
|
// runLockedUserEntryUpdates runs updates for locked user storage entries and userFailedLoginInfo map
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ func unlockUser(ctx context.Context, core *Core, mountAccessor string, aliasName
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we have no more locked users and cancel any running lockout logger
|
||||||
|
core.userFailedLoginInfoLock.RLock()
|
||||||
|
numLockedUsers := len(core.userFailedLoginInfo)
|
||||||
|
core.userFailedLoginInfoLock.RUnlock()
|
||||||
|
|
||||||
|
if numLockedUsers == 0 {
|
||||||
|
core.Logger().Info("user lockout(s) cleared")
|
||||||
|
core.lockoutLoggerCancel()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1483,6 +1483,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if isloginUserLocked {
|
if isloginUserLocked {
|
||||||
|
c.startLockoutLogger()
|
||||||
return nil, nil, logical.ErrPermissionDenied
|
return nil, nil, logical.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2298,6 +2299,8 @@ func (c *Core) LocalGetUserFailedLoginInfo(ctx context.Context, userKey FailedLo
|
|||||||
// LocalUpdateUserFailedLoginInfo updates the failed login information for a user based on alias name and mountAccessor
|
// LocalUpdateUserFailedLoginInfo updates the failed login information for a user based on alias name and mountAccessor
|
||||||
func (c *Core) LocalUpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
|
func (c *Core) LocalUpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
|
||||||
c.userFailedLoginInfoLock.Lock()
|
c.userFailedLoginInfoLock.Lock()
|
||||||
|
defer c.userFailedLoginInfoLock.Unlock()
|
||||||
|
|
||||||
switch deleteEntry {
|
switch deleteEntry {
|
||||||
case false:
|
case false:
|
||||||
// update entry in the map
|
// update entry in the map
|
||||||
@@ -2340,7 +2343,6 @@ func (c *Core) LocalUpdateUserFailedLoginInfo(ctx context.Context, userKey Faile
|
|||||||
// delete the entry from the map, if no key exists it is no-op
|
// delete the entry from the map, if no key exists it is no-op
|
||||||
delete(c.userFailedLoginInfo, userKey)
|
delete(c.userFailedLoginInfo, userKey)
|
||||||
}
|
}
|
||||||
c.userFailedLoginInfoLock.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user