Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault 8305 Prevent Brute Forcing in Auth methods : Setting user lockout configuration #17338

Merged
merged 27 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
addressing comments
  • Loading branch information
akshya96 committed Oct 4, 2022
commit 0616553764c6bec3b77e60516398cf6acd46e0d3
2 changes: 1 addition & 1 deletion changelog/17338.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
```release-note:feature
core: Add user lockout field to config and configuring this for auth mount using auth tune
core: Add user lockout field to config and configuring this for auth mount using auth tune to prevent brute forcing in auth methods
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akshya96 - I left a comment on the enterprise PR as well - for new features, they only need one changelog entry per new feature. If you want to make separate changelog entries per PR, those entries shouldn't be in the "features" section (and you may or may not need to make separate additional changelog entries).

For new features, they have a different format for changelog entries. Could you please open a new PR (or use the enterprise PR) to consolidate the changelog entries for this feature and change the formatting?

```
8 changes: 4 additions & 4 deletions command/auth_tune.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,15 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
})

f.IntVar(&IntVar{
Name: "user-lockout-threshold",
Name: flagNameUserLockoutThreshold,
Target: &c.flagUserLockoutThreshold,
Usage: "The threshold for user lockout for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured user lockout threshold, " +
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
"or a previously configured value for the auth method.",
})

f.DurationVar(&DurationVar{
Name: "user-lockout-duration",
Name: flagNameUserLockoutDuration,
Target: &c.flagUserLockoutDuration,
Completion: complete.PredictAnything,
Usage: "The user lockout duration for this auth method. If unspecified, this " +
Expand All @@ -167,7 +167,7 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
})

f.DurationVar(&DurationVar{
Name: "user-lockout-counter-reset-duration",
Name: flagNameUserLockoutCounterResetDuration,
Target: &c.flagUserLockoutCounterResetDuration,
Completion: complete.PredictAnything,
Usage: "The user lockout counter reset duration for this auth method. If unspecified, this " +
Expand All @@ -176,7 +176,7 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
})

f.BoolVar(&BoolVar{
Name: "user-lockout-disable",
Name: flagNameUserLockoutDisable,
Target: &c.flagUserLockoutDisable,
Default: false,
Usage: "Disable user lockout for this auth method. If unspecified, this " +
Expand Down
8 changes: 8 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ const (
flagNameAllowedManagedKeys = "allowed-managed-keys"
// flagNamePluginVersion selects what version of a plugin should be used.
flagNamePluginVersion = "plugin-version"
// flagNameUserLockoutThreshold is the flag name used for tuning the auth mount lockout threshold parameter
flagNameUserLockoutThreshold = "user-lockout-threshold"
// flagNameUserLockoutDuration is the flag name used for tuning the auth mount lockout duration parameter
flagNameUserLockoutDuration = "user-lockout-duration"
// flagNameUserLockoutCounterResetDuration is the flag name used for tuning the auth mount lockout counter reset parameter
flagNameUserLockoutCounterResetDuration = "user-lockout-counter-reset-duration"
// flagNameUserLockoutDisable is the flag name used for tuning the auth mount disable lockout parameter
flagNameUserLockoutDisable = "user-lockout-disable"
)

var (
Expand Down
8 changes: 6 additions & 2 deletions internalshared/configutil/userlockout.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
}

userLockoutConfig.Type = strings.ToLower(userLockoutConfig.Type)
// Supported auth methods for user lockout configuration: ldap, approle, userpass
// "all" is used to apply the configuration to all supported auth methods
switch userLockoutConfig.Type {
case "all", "ldap", "approle", "userpass":
ccapurso marked this conversation as resolved.
Show resolved Hide resolved
result.found(userLockoutConfig.Type, userLockoutConfig.Type)
Expand Down Expand Up @@ -103,7 +105,8 @@ func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
return nil
}

// setDefaultUserLockoutValuesInMap sets default user lockout values for key "all" (all auth methods) for user lockout fields that are not configured using config file
// setDefaultUserLockoutValuesInMap sets default user lockout values for key "all" (all auth methods)
// for user lockout fields that are not configured using config file
func setDefaultUserLockoutValuesInMap(userLockoutsMap map[string]*UserLockout) map[string]*UserLockout {
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
if userLockoutAll, ok := userLockoutsMap["all"]; !ok {
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
var tmpUserLockoutConfig UserLockout
Expand Down Expand Up @@ -132,7 +135,8 @@ func setDefaultUserLockoutValuesInMap(userLockoutsMap map[string]*UserLockout) m
return userLockoutsMap
}

// setDefaultUserLockoutValuesInMap sets missing user lockout fields for auth methods with default values (from key "all") that are not configured using config file
// setDefaultUserLockoutValuesInMap sets missing user lockout fields for auth methods
// with default values (from key "all") that are not configured using config file
func setMissingUserLockoutValuesInMap(userLockoutsMap map[string]*UserLockout) map[string]*UserLockout {
userLockoutsMap = setDefaultUserLockoutValuesInMap(userLockoutsMap)
for _, userLockoutAuth := range userLockoutsMap {
Expand Down
84 changes: 47 additions & 37 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@ func (b *SystemBackend) mountInfo(ctx context.Context, entry *MountEntry) map[st
if entry.Table == credentialTableType {
entryConfig["token_type"] = entry.Config.TokenType.String()
}
if (entry.Config.UserLockoutConfig != UserLockoutConfig{}) {
if entry.Config.UserLockoutConfig != nil {
userLockoutConfig := map[string]interface{}{
"user_lockout_counter_reset_duration": int64(entry.Config.UserLockoutConfig.LockoutCounterReset.Seconds()),
"user_lockout_threshold": entry.Config.UserLockoutConfig.LockoutThreshold,
Expand Down Expand Up @@ -1607,7 +1607,7 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) (
resp.Data["allowed_managed_keys"] = rawVal.([]string)
}

if (mountEntry.Config.UserLockoutConfig != UserLockoutConfig{}) {
if mountEntry.Config.UserLockoutConfig != nil {
resp.Data["user_lockout_counter_reset_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutCounterReset.Seconds())
resp.Data["user_lockout_threshold"] = mountEntry.Config.UserLockoutConfig.LockoutThreshold
resp.Data["user_lockout_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutDuration.Seconds())
Expand Down Expand Up @@ -1740,12 +1740,6 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
userLockoutConfigMap := data.Get("user_lockout_config").(map[string]interface{})
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
var err error
// Augmenting userLockoutConfigMap for some config options to treat them as comma separated entries
err = expandStringValsWithCommas(userLockoutConfigMap)
if err != nil {
return logical.ErrorResponse(
"unable to parse given auth user lockout config information"),
logical.ErrInvalidRequest
}
if userLockoutConfigMap != nil && len(userLockoutConfigMap) != 0 {
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
err := mapstructure.Decode(userLockoutConfigMap, &apiuserLockoutConfig)
if err != nil {
Expand All @@ -1754,6 +1748,8 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
logical.ErrInvalidRequest
}

// Supported auth methods for user lockout configuration: ldap, approle, userpass
// "all" is used to apply the configuration to all supported auth methods
switch strings.ToLower(mountEntry.Type) {
case "all", "ldap", "approle", "userpass":
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
default:
Expand All @@ -1763,21 +1759,25 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
}
}

if mountEntry.Config.UserLockoutConfig == nil {
mountEntry.Config.UserLockoutConfig = &UserLockoutConfig{}
}

if _, ok := userLockoutConfigMap["lockout_threshold"]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It seems a bit strange that we are checking on userLockoutConfigMap, but then using apiuserLockoutConfig. Why not use the latter to check if the specific entry is not nil or something?

Copy link
Contributor Author

@akshya96 akshya96 Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was because I had disable lockout as a bool value. Therefore, checking apiuserLockoutConfig will not work as expected and I used userLockoutConfigMap to check lockout disable field. I used it for checking other user lockout fields to maintain consistency in checking. I now modified the apiuserLockoutConfig struct to use *bool to check for nil values. I will change checking userLockoutConfigMap to checking apiuserLockoutConfig

var newUserLockoutThreshold int64
userLockoutThreshold, err := parseutil.ParseInt(apiuserLockoutConfig.LockoutThreshold)
if err != nil {
return nil, fmt.Errorf("unable to parse user lockout threshold: %w", err)
}

oldUserLockoutThreshold := mountEntry.Config.UserLockoutConfig.LockoutThreshold

switch {
case userLockoutThreshold < 0:
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
newUserLockoutThreshold = oldUserLockoutThreshold
mountEntry.Config.UserLockoutConfig.LockoutThreshold = oldUserLockoutThreshold
default:
newUserLockoutThreshold = userLockoutThreshold
mountEntry.Config.UserLockoutConfig.LockoutThreshold = userLockoutThreshold
}
mountEntry.Config.UserLockoutConfig.LockoutThreshold = newUserLockoutThreshold

// Update the mount table
switch {
case strings.HasPrefix(path, "auth/"):
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -1795,9 +1795,11 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,

}

{
var newUserLockoutDuration time.Duration
oldUserLockoutDuration := mountEntry.Config.UserLockoutConfig.LockoutDuration
if _, ok := userLockoutConfigMap["lockout_duration"]; ok {
var newUserLockoutDuration, oldUserLockoutDuration time.Duration

oldUserLockoutDuration = mountEntry.Config.UserLockoutConfig.LockoutDuration

switch apiuserLockoutConfig.LockoutDuration {

case "":
Expand Down Expand Up @@ -1830,9 +1832,12 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
}

}
{
var newUserLockoutCounterReset time.Duration
oldUserLockoutCounterReset := mountEntry.Config.UserLockoutConfig.LockoutCounterReset

if _, ok := userLockoutConfigMap["lockout_counter_reset_duration"]; ok {
var newUserLockoutCounterReset, oldUserLockoutCounterReset time.Duration

oldUserLockoutCounterReset = mountEntry.Config.UserLockoutConfig.LockoutCounterReset

switch apiuserLockoutConfig.LockoutCounterResetDuration {
case "":
newUserLockoutCounterReset = oldUserLockoutCounterReset
Expand Down Expand Up @@ -1862,27 +1867,28 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
b.Core.logger.Info("tuning of user-lockout-counter-reset successful", "path", path, "user-lockout-counter-reset", apiuserLockoutConfig.LockoutCounterResetDuration)
}
}
{
if rawVal, ok := userLockoutConfigMap["lockout_disable"]; ok {
userLockoutDisable := rawVal.(bool)
oldUserLockoutDisable := mountEntry.Config.UserLockoutConfig.DisableLockout
mountEntry.Config.UserLockoutConfig.DisableLockout = userLockoutDisable
var err error
switch {
case strings.HasPrefix(path, "auth/"):
err = b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local)
default:
err = b.Core.persistMounts(ctx, b.Core.mounts, &mountEntry.Local)
}
if err != nil {
mountEntry.Config.UserLockoutConfig.DisableLockout = oldUserLockoutDisable
return handleError(err)
}
if b.Core.logger.IsInfo() {
b.Core.logger.Info("tuning of user-lockout-disable successful", "path", path, "user-lockout-disable", userLockoutDisable)
}

if rawVal, ok := userLockoutConfigMap["lockout_disable"]; ok {
userLockoutDisable := rawVal.(bool)
var oldUserLockoutDisable bool

oldUserLockoutDisable = mountEntry.Config.UserLockoutConfig.DisableLockout
mountEntry.Config.UserLockoutConfig.DisableLockout = userLockoutDisable
var err error
switch {
case strings.HasPrefix(path, "auth/"):
err = b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local)
default:
err = b.Core.persistMounts(ctx, b.Core.mounts, &mountEntry.Local)
}
if err != nil {
mountEntry.Config.UserLockoutConfig.DisableLockout = oldUserLockoutDisable
return handleError(err)
}
if b.Core.logger.IsInfo() {
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
b.Core.logger.Info("tuning of user-lockout-disable successful", "path", path, "user-lockout-disable", userLockoutDisable)
}

}

}
Expand Down Expand Up @@ -5216,6 +5222,10 @@ in the plugin catalog.`,
`The options to pass into the backend. Should be a json object with string keys and values.`,
},

"tune_user_lockout_config": {
`The user lockout configuration to pass into the backend. Should be a json object with string keys and values.`,
},

"remount": {
"Move the mount point of an already-mounted backend, within or across namespaces",
`
Expand Down
4 changes: 2 additions & 2 deletions vault/logical_system_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -1544,7 +1544,7 @@ func (b *SystemBackend) authPaths() []*framework.Path {
},
"user_lockout_config": {
Type: framework.TypeMap,
Description: strings.TrimSpace(sysHelp["user_lockout_config"][0]),
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
},
"plugin_version": {
Type: framework.TypeString,
Expand Down Expand Up @@ -1935,7 +1935,7 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
},
"user_lockout_config": {
Type: framework.TypeMap,
Description: strings.TrimSpace(sysHelp["user_lockout_config"][0]),
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
},
},

Expand Down
2 changes: 1 addition & 1 deletion vault/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ type MountConfig struct {
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType logical.TokenType `json:"token_type,omitempty" structs:"token_type" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`

// PluginName is the name of the plugin registered in the catalog.
//
Expand Down