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

* config file changes

* lockout config changes

* auth tune r/w and auth tune

* removing changes at enable

* removing q.Q

* go mod tidy

* removing comments

* changing struct name for config file

* fixing mount tune

* adding test file for user lockout

* fixing comments and add changelog

* addressing comments

* fixing mount table updates

* updating consts in auth_tune

* small fixes

* adding hcl parse test

* fixing config compare

* fixing github comments

* optimize userlockouts.go

* fixing test

* minor changes

* adding comments

* adding sort to flaky test

* fix flaky test
This commit is contained in:
akshya96
2022-11-01 11:02:07 -07:00
committed by GitHub
parent 80c2dec816
commit 746b089472
14 changed files with 624 additions and 38 deletions

View File

@@ -267,7 +267,7 @@ type MountConfigInput struct {
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
PluginVersion string `json:"plugin_version,omitempty"`
UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
@@ -299,11 +299,25 @@ type MountConfigOutput struct {
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"`
// Deprecated: This field will always be blank for newer server responses.
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type UserLockoutConfigInput struct {
LockoutThreshold string `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
LockoutDuration string `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
LockoutCounterResetDuration string `json:"lockout_counter_reset_duration,omitempty" structs:"lockout_counter_reset_duration" mapstructure:"lockout_counter_reset_duration"`
DisableLockout *bool `json:"lockout_disable,omitempty" structs:"lockout_disable" mapstructure:"lockout_disable"`
}
type UserLockoutConfigOutput struct {
LockoutThreshold uint `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
LockoutDuration int `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
LockoutCounterReset int `json:"lockout_counter_reset,omitempty" structs:"lockout_counter_reset" mapstructure:"lockout_counter_reset"`
DisableLockout *bool `json:"disable_lockout,omitempty" structs:"disable_lockout" mapstructure:"disable_lockout"`
}
type MountMigrationOutput struct {
MigrationID string `mapstructure:"migration_id"`
}

3
changelog/17338.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:feature
core: Add user lockout field to config and configuring this for auth mount using auth tune to prevent brute forcing in auth methods
```

View File

@@ -32,6 +32,10 @@ type AuthTuneCommand struct {
flagTokenType string
flagVersion int
flagPluginVersion string
flagUserLockoutThreshold uint
flagUserLockoutDuration time.Duration
flagUserLockoutCounterResetDuration time.Duration
flagUserLockoutDisable bool
}
func (c *AuthTuneCommand) Synopsis() string {
@@ -145,6 +149,41 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
Usage: "Select the version of the auth method to run. Not supported by all auth methods.",
})
f.UintVar(&UintVar{
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, " +
"or a previously configured value for the auth method.",
})
f.DurationVar(&DurationVar{
Name: flagNameUserLockoutDuration,
Target: &c.flagUserLockoutDuration,
Completion: complete.PredictAnything,
Usage: "The user lockout duration for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured user lockout duration, " +
"or a previously configured value for the auth method.",
})
f.DurationVar(&DurationVar{
Name: flagNameUserLockoutCounterResetDuration,
Target: &c.flagUserLockoutCounterResetDuration,
Completion: complete.PredictAnything,
Usage: "The user lockout counter reset duration for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured user lockout counter reset duration, " +
"or a previously configured value for the auth method.",
})
f.BoolVar(&BoolVar{
Name: flagNameUserLockoutDisable,
Target: &c.flagUserLockoutDisable,
Default: false,
Usage: "Disable user lockout for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured user lockout disable, " +
"or a previously configured value for the auth method.",
})
f.StringVar(&StringVar{
Name: flagNamePluginVersion,
Target: &c.flagPluginVersion,
@@ -230,6 +269,24 @@ func (c *AuthTuneCommand) Run(args []string) int {
if fl.Name == flagNameTokenType {
mountConfigInput.TokenType = c.flagTokenType
}
switch fl.Name {
case flagNameUserLockoutThreshold, flagNameUserLockoutDuration, flagNameUserLockoutCounterResetDuration, flagNameUserLockoutDisable:
if mountConfigInput.UserLockoutConfig == nil {
mountConfigInput.UserLockoutConfig = &api.UserLockoutConfigInput{}
}
}
if fl.Name == flagNameUserLockoutThreshold {
mountConfigInput.UserLockoutConfig.LockoutThreshold = strconv.FormatUint(uint64(c.flagUserLockoutThreshold), 10)
}
if fl.Name == flagNameUserLockoutDuration {
mountConfigInput.UserLockoutConfig.LockoutDuration = ttlToAPI(c.flagUserLockoutDuration)
}
if fl.Name == flagNameUserLockoutCounterResetDuration {
mountConfigInput.UserLockoutConfig.LockoutCounterResetDuration = ttlToAPI(c.flagUserLockoutCounterResetDuration)
}
if fl.Name == flagNameUserLockoutDisable {
mountConfigInput.UserLockoutConfig.DisableLockout = &c.flagUserLockoutDisable
}
if fl.Name == flagNamePluginVersion {
mountConfigInput.PluginVersion = c.flagPluginVersion

View File

@@ -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"
// flagNameDisableRedirects is used to prevent the client from honoring a single redirect as a response to a request
flagNameDisableRedirects = "disable-redirects"
)

View File

@@ -36,6 +36,10 @@ func TestParseListeners(t *testing.T) {
testParseListeners(t)
}
func TestParseUserLockouts(t *testing.T) {
testParseUserLockouts(t)
}
func TestParseSockaddrTemplate(t *testing.T) {
testParseSockaddrTemplate(t)
}

View File

@@ -3,6 +3,7 @@ package server
import (
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
@@ -892,6 +893,67 @@ listener "tcp" {
}
}
func testParseUserLockouts(t *testing.T) {
obj, _ := hcl.Parse(strings.TrimSpace(`
user_lockout "all" {
lockout_duration = "40m"
lockout_counter_reset = "45m"
disable_lockout = "false"
}
user_lockout "userpass" {
lockout_threshold = "100"
lockout_duration = "20m"
}
user_lockout "ldap" {
disable_lockout = "true"
}`))
config := Config{
SharedConfig: &configutil.SharedConfig{},
}
list, _ := obj.Node.(*ast.ObjectList)
objList := list.Filter("user_lockout")
configutil.ParseUserLockouts(config.SharedConfig, objList)
sort.Slice(config.SharedConfig.UserLockouts[:], func(i, j int) bool {
return config.SharedConfig.UserLockouts[i].Type < config.SharedConfig.UserLockouts[j].Type
})
expected := &Config{
SharedConfig: &configutil.SharedConfig{
UserLockouts: []*configutil.UserLockout{
{
Type: "all",
LockoutThreshold: 5,
LockoutDuration: 2400000000000,
LockoutCounterReset: 2700000000000,
DisableLockout: false,
},
{
Type: "userpass",
LockoutThreshold: 100,
LockoutDuration: 1200000000000,
LockoutCounterReset: 2700000000000,
DisableLockout: false,
},
{
Type: "ldap",
LockoutThreshold: 5,
LockoutDuration: 2400000000000,
LockoutCounterReset: 2700000000000,
DisableLockout: true,
},
},
},
}
sort.Slice(expected.SharedConfig.UserLockouts[:], func(i, j int) bool {
return expected.SharedConfig.UserLockouts[i].Type < expected.SharedConfig.UserLockouts[j].Type
})
config.Prune()
require.Equal(t, config, *expected)
}
func testParseSockaddrTemplate(t *testing.T) {
config, err := ParseConfig(`
api_addr = <<EOF

View File

@@ -21,6 +21,8 @@ type SharedConfig struct {
Listeners []*Listener `hcl:"-"`
UserLockouts []*UserLockout `hcl:"-"`
Seals []*KMS `hcl:"-"`
Entropy *Entropy `hcl:"-"`
@@ -134,6 +136,13 @@ func ParseConfig(d string) (*SharedConfig, error) {
}
}
if o := list.Filter("user_lockout"); len(o.Items) > 0 {
result.found("user_lockout", "UserLockout")
if err := ParseUserLockouts(&result, o); err != nil {
return nil, fmt.Errorf("error parsing 'user_lockout': %w", err)
}
}
if o := list.Filter("telemetry"); len(o.Items) > 0 {
result.found("telemetry", "Telemetry")
if err := parseTelemetry(&result, o); err != nil {
@@ -194,6 +203,22 @@ func (c *SharedConfig) Sanitized() map[string]interface{} {
result["listeners"] = sanitizedListeners
}
// Sanitize user lockout stanza
if len(c.UserLockouts) != 0 {
var sanitizedUserLockouts []interface{}
for _, userlockout := range c.UserLockouts {
cleanUserLockout := map[string]interface{}{
"type": userlockout.Type,
"lockout_threshold": userlockout.LockoutThreshold,
"lockout_duration": userlockout.LockoutDuration,
"lockout_counter_reset": userlockout.LockoutCounterReset,
"disable_lockout": userlockout.DisableLockout,
}
sanitizedUserLockouts = append(sanitizedUserLockouts, cleanUserLockout)
}
result["user_lockout_configs"] = sanitizedUserLockouts
}
// Sanitize seals stanza
if len(c.Seals) != 0 {
var sanitizedSeals []interface{}

View File

@@ -14,6 +14,13 @@ func (c *SharedConfig) Merge(c2 *SharedConfig) *SharedConfig {
result.Listeners = append(result.Listeners, l)
}
for _, userlockout := range c.UserLockouts {
result.UserLockouts = append(result.UserLockouts, userlockout)
}
for _, userlockout := range c2.UserLockouts {
result.UserLockouts = append(result.UserLockouts, userlockout)
}
result.HCPLinkConf = c.HCPLinkConf
if c2.HCPLinkConf != nil {
result.HCPLinkConf = c2.HCPLinkConf

View File

@@ -0,0 +1,186 @@
package configutil
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
)
const (
UserLockoutThresholdDefault = 5
UserLockoutDurationDefault = 15 * time.Minute
UserLockoutCounterResetDefault = 15 * time.Minute
DisableUserLockoutDefault = false
)
type UserLockout struct {
Type string
LockoutThreshold uint64 `hcl:"-"`
LockoutThresholdRaw interface{} `hcl:"lockout_threshold"`
LockoutDuration time.Duration `hcl:"-"`
LockoutDurationRaw interface{} `hcl:"lockout_duration"`
LockoutCounterReset time.Duration `hcl:"-"`
LockoutCounterResetRaw interface{} `hcl:"lockout_counter_reset"`
DisableLockout bool `hcl:"-"`
DisableLockoutRaw interface{} `hcl:"disable_lockout"`
}
func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
var err error
result.UserLockouts = make([]*UserLockout, 0, len(list.Items))
userLockoutsMap := make(map[string]*UserLockout)
for i, item := range list.Items {
var userLockoutConfig UserLockout
if err := hcl.DecodeObject(&userLockoutConfig, item.Val); err != nil {
return multierror.Prefix(err, fmt.Sprintf("userLockouts.%d:", i))
}
// Base values
{
switch {
case userLockoutConfig.Type != "":
case len(item.Keys) == 1:
userLockoutConfig.Type = strings.ToLower(item.Keys[0].Token.Value().(string))
default:
return multierror.Prefix(errors.New("auth type for user lockout must be specified, if it applies to all auth methods specify \"all\" "), fmt.Sprintf("user_lockouts.%d:", i))
}
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":
result.found(userLockoutConfig.Type, userLockoutConfig.Type)
default:
return multierror.Prefix(fmt.Errorf("unsupported auth type %q", userLockoutConfig.Type), fmt.Sprintf("user_lockouts.%d:", i))
}
}
// Lockout Parameters
// Not setting raw entries to nil here as soon as they are parsed
// as they are used to set the missing user lockout configuration values later.
{
if userLockoutConfig.LockoutThresholdRaw != nil {
userLockoutThresholdString := fmt.Sprintf("%v", userLockoutConfig.LockoutThresholdRaw)
if userLockoutConfig.LockoutThreshold, err = strconv.ParseUint(userLockoutThresholdString, 10, 64); err != nil {
return multierror.Prefix(fmt.Errorf("error parsing lockout_threshold: %w", err), fmt.Sprintf("user_lockouts.%d", i))
}
}
if userLockoutConfig.LockoutDurationRaw != nil {
if userLockoutConfig.LockoutDuration, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutDurationRaw); err != nil {
return multierror.Prefix(fmt.Errorf("error parsing lockout_duration: %w", err), fmt.Sprintf("user_lockouts.%d", i))
}
if userLockoutConfig.LockoutDuration < 0 {
return multierror.Prefix(errors.New("lockout_duration cannot be negative"), fmt.Sprintf("user_lockouts.%d", i))
}
}
if userLockoutConfig.LockoutCounterResetRaw != nil {
if userLockoutConfig.LockoutCounterReset, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutCounterResetRaw); err != nil {
return multierror.Prefix(fmt.Errorf("error parsing lockout_counter_reset: %w", err), fmt.Sprintf("user_lockouts.%d", i))
}
if userLockoutConfig.LockoutCounterReset < 0 {
return multierror.Prefix(errors.New("lockout_counter_reset cannot be negative"), fmt.Sprintf("user_lockouts.%d", i))
}
}
if userLockoutConfig.DisableLockoutRaw != nil {
if userLockoutConfig.DisableLockout, err = parseutil.ParseBool(userLockoutConfig.DisableLockoutRaw); err != nil {
return multierror.Prefix(fmt.Errorf("invalid value for disable_lockout: %w", err), fmt.Sprintf("user_lockouts.%d", i))
}
}
}
userLockoutsMap[userLockoutConfig.Type] = &userLockoutConfig
}
// Use raw entries to set values for user lockout configurations fields
// that were not configured using config file.
// The raw entries would mean that the entry was configured by the user using the config file.
// If any of these fields are not configured using the config file (missing fields),
// we set values for these fields with defaults
// The issue with not being able to use non-raw entries is because of fields lockout threshold
// and disable lockout. We cannot differentiate using non-raw entries if the user configured these fields
// with values (0 and false) or if the the user did not configure these values in config file at all.
// The raw fields are set to nil after setting missing values in setNilValuesForRawUserLockoutFields function
userLockoutsMap = setMissingUserLockoutValuesInMap(userLockoutsMap)
for _, userLockoutValues := range userLockoutsMap {
result.UserLockouts = append(result.UserLockouts, userLockoutValues)
}
return nil
}
// setUserLockoutValueAllInMap sets default user lockout values for key "all" (all auth methods)
// for user lockout fields that are not configured using config file
func setUserLockoutValueAllInMap(userLockoutAll *UserLockout) *UserLockout {
if userLockoutAll.Type == "" {
userLockoutAll.Type = "all"
}
if userLockoutAll.LockoutThresholdRaw == nil {
userLockoutAll.LockoutThreshold = UserLockoutThresholdDefault
}
if userLockoutAll.LockoutDurationRaw == nil {
userLockoutAll.LockoutDuration = UserLockoutDurationDefault
}
if userLockoutAll.LockoutCounterResetRaw == nil {
userLockoutAll.LockoutCounterReset = UserLockoutCounterResetDefault
}
if userLockoutAll.DisableLockoutRaw == nil {
userLockoutAll.DisableLockout = DisableUserLockoutDefault
}
return setNilValuesForRawUserLockoutFields(userLockoutAll)
}
// 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 {
// set values for "all" key with default values for "all" user lockout fields that are not configured
// the "all" key values will be used as default values for other auth methods
userLockoutAll, ok := userLockoutsMap["all"]
switch ok {
case true:
userLockoutsMap["all"] = setUserLockoutValueAllInMap(userLockoutAll)
default:
userLockoutsMap["all"] = setUserLockoutValueAllInMap(&UserLockout{})
}
for _, userLockoutAuth := range userLockoutsMap {
if userLockoutAuth.Type == "all" {
continue
}
// set missing values
if userLockoutAuth.LockoutThresholdRaw == nil {
userLockoutAuth.LockoutThreshold = userLockoutsMap["all"].LockoutThreshold
}
if userLockoutAuth.LockoutDurationRaw == nil {
userLockoutAuth.LockoutDuration = userLockoutsMap["all"].LockoutDuration
}
if userLockoutAuth.LockoutCounterResetRaw == nil {
userLockoutAuth.LockoutCounterReset = userLockoutsMap["all"].LockoutCounterReset
}
if userLockoutAuth.DisableLockoutRaw == nil {
userLockoutAuth.DisableLockout = userLockoutsMap["all"].DisableLockout
}
userLockoutAuth = setNilValuesForRawUserLockoutFields(userLockoutAuth)
userLockoutsMap[userLockoutAuth.Type] = userLockoutAuth
}
return userLockoutsMap
}
// setNilValuesForRawUserLockoutFields sets nil values for user lockout Raw fields
func setNilValuesForRawUserLockoutFields(userLockout *UserLockout) *UserLockout {
userLockout.LockoutThresholdRaw = nil
userLockout.LockoutDurationRaw = nil
userLockout.LockoutCounterResetRaw = nil
userLockout.DisableLockoutRaw = nil
return userLockout
}

View File

@@ -0,0 +1,69 @@
package configutil
import (
"reflect"
"testing"
"time"
)
func TestParseUserLockout(t *testing.T) {
t.Parallel()
t.Run("Missing user lockout block in config file", func(t *testing.T) {
t.Parallel()
inputConfig := make(map[string]*UserLockout)
expectedConfig := make(map[string]*UserLockout)
expectedConfigall := &UserLockout{}
expectedConfigall.Type = "all"
expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault
expectedConfigall.LockoutDuration = UserLockoutDurationDefault
expectedConfigall.LockoutCounterReset = UserLockoutCounterResetDefault
expectedConfigall.DisableLockout = DisableUserLockoutDefault
expectedConfig["all"] = expectedConfigall
outputConfig := setMissingUserLockoutValuesInMap(inputConfig)
if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) {
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"])
}
})
t.Run("setting default lockout counter reset and lockout duration for userpass in config ", func(t *testing.T) {
t.Parallel()
// input user lockout in config file
inputConfig := make(map[string]*UserLockout)
configAll := &UserLockout{}
configAll.Type = "all"
configAll.LockoutCounterReset = 20 * time.Minute
configAll.LockoutCounterResetRaw = "1200000000000"
inputConfig["all"] = configAll
configUserpass := &UserLockout{}
configUserpass.Type = "userpass"
configUserpass.LockoutDuration = 10 * time.Minute
configUserpass.LockoutDurationRaw = "600000000000"
inputConfig["userpass"] = configUserpass
expectedConfig := make(map[string]*UserLockout)
expectedConfigall := &UserLockout{}
expectedConfigUserpass := &UserLockout{}
// expected default values
expectedConfigall.Type = "all"
expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault
expectedConfigall.LockoutDuration = UserLockoutDurationDefault
expectedConfigall.LockoutCounterReset = 20 * time.Minute
expectedConfigall.DisableLockout = DisableUserLockoutDefault
// expected values for userpass
expectedConfigUserpass.Type = "userpass"
expectedConfigUserpass.LockoutThreshold = UserLockoutThresholdDefault
expectedConfigUserpass.LockoutDuration = 10 * time.Minute
expectedConfigUserpass.LockoutCounterReset = 20 * time.Minute
expectedConfigUserpass.DisableLockout = DisableUserLockoutDefault
expectedConfig["all"] = expectedConfigall
expectedConfig["userpass"] = expectedConfigUserpass
outputConfig := setMissingUserLockoutValuesInMap(inputConfig)
if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) {
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"])
}
if !reflect.DeepEqual(expectedConfig["userpass"], outputConfig["userpass"]) {
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["userpass"], outputConfig["userpass"])
}
})
}

View File

@@ -34,4 +34,6 @@ const (
ReplicationResolverALPN = "replication_resolver_v1"
VaultEnableFilePermissionsCheckEnv = "VAULT_ENABLE_FILE_PERMISSIONS_CHECK"
VaultDisableUserLockout = "VAULT_DISABLE_USER_LOCKOUT"
)

View File

@@ -944,6 +944,15 @@ 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 != nil {
userLockoutConfig := map[string]interface{}{
"user_lockout_counter_reset_duration": int64(entry.Config.UserLockoutConfig.LockoutCounterReset.Seconds()),
"user_lockout_threshold": entry.Config.UserLockoutConfig.LockoutThreshold,
"user_lockout_duration": int64(entry.Config.UserLockoutConfig.LockoutDuration.Seconds()),
"user_lockout_disable": entry.Config.UserLockoutConfig.DisableLockout,
}
entryConfig["user_lockout_config"] = userLockoutConfig
}
// Add deprecation status only if it exists
builtinType := b.Core.builtinTypeFromMountEntry(ctx, entry)
@@ -1604,6 +1613,13 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) (
resp.Data["allowed_managed_keys"] = rawVal.([]string)
}
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())
resp.Data["user_lockout_disable"] = mountEntry.Config.UserLockoutConfig.DisableLockout
}
if len(mountEntry.Options) > 0 {
resp.Data["options"] = mountEntry.Options
}
@@ -1723,6 +1739,111 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
}
}
// user-lockout config
{
var apiuserLockoutConfig APIUserLockoutConfig
userLockoutConfigMap := data.Get("user_lockout_config").(map[string]interface{})
var err error
if userLockoutConfigMap != nil && len(userLockoutConfigMap) != 0 {
err := mapstructure.Decode(userLockoutConfigMap, &apiuserLockoutConfig)
if err != nil {
return logical.ErrorResponse(
"unable to convert given user lockout config information"),
logical.ErrInvalidRequest
}
// Supported auth methods for user lockout configuration: ldap, approle, userpass
switch strings.ToLower(mountEntry.Type) {
case "ldap", "approle", "userpass":
default:
return logical.ErrorResponse("tuning of user lockout configuration for auth type %q not allowed", mountEntry.Type),
logical.ErrInvalidRequest
}
}
if len(userLockoutConfigMap) > 0 && mountEntry.Config.UserLockoutConfig == nil {
mountEntry.Config.UserLockoutConfig = &UserLockoutConfig{}
}
var oldUserLockoutThreshold uint64
var newUserLockoutDuration, oldUserLockoutDuration time.Duration
var newUserLockoutCounterReset, oldUserLockoutCounterReset time.Duration
var oldUserLockoutDisable bool
if apiuserLockoutConfig.LockoutThreshold != "" {
userLockoutThreshold, err := strconv.ParseUint(apiuserLockoutConfig.LockoutThreshold, 10, 64)
if err != nil {
return nil, fmt.Errorf("unable to parse user lockout threshold: %w", err)
}
oldUserLockoutThreshold = mountEntry.Config.UserLockoutConfig.LockoutThreshold
mountEntry.Config.UserLockoutConfig.LockoutThreshold = userLockoutThreshold
}
if apiuserLockoutConfig.LockoutDuration != "" {
oldUserLockoutDuration = mountEntry.Config.UserLockoutConfig.LockoutDuration
switch apiuserLockoutConfig.LockoutDuration {
case "":
newUserLockoutDuration = oldUserLockoutDuration
case "system":
newUserLockoutDuration = time.Duration(0)
default:
tmpUserLockoutDuration, err := parseutil.ParseDurationSecond(apiuserLockoutConfig.LockoutDuration)
if err != nil {
return handleError(err)
}
newUserLockoutDuration = tmpUserLockoutDuration
}
mountEntry.Config.UserLockoutConfig.LockoutDuration = newUserLockoutDuration
}
if apiuserLockoutConfig.LockoutCounterResetDuration != "" {
oldUserLockoutCounterReset = mountEntry.Config.UserLockoutConfig.LockoutCounterReset
switch apiuserLockoutConfig.LockoutCounterResetDuration {
case "":
newUserLockoutCounterReset = oldUserLockoutCounterReset
case "system":
newUserLockoutCounterReset = time.Duration(0)
default:
tmpUserLockoutCounterReset, err := parseutil.ParseDurationSecond(apiuserLockoutConfig.LockoutCounterResetDuration)
if err != nil {
return handleError(err)
}
newUserLockoutCounterReset = tmpUserLockoutCounterReset
}
mountEntry.Config.UserLockoutConfig.LockoutCounterReset = newUserLockoutCounterReset
}
if apiuserLockoutConfig.DisableLockout != nil {
oldUserLockoutDisable = mountEntry.Config.UserLockoutConfig.DisableLockout
userLockoutDisable := apiuserLockoutConfig.DisableLockout
mountEntry.Config.UserLockoutConfig.DisableLockout = *userLockoutDisable
}
// Update the mount table
if len(userLockoutConfigMap) > 0 {
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.LockoutCounterReset = oldUserLockoutCounterReset
mountEntry.Config.UserLockoutConfig.LockoutThreshold = oldUserLockoutThreshold
mountEntry.Config.UserLockoutConfig.LockoutDuration = oldUserLockoutDuration
mountEntry.Config.UserLockoutConfig.DisableLockout = oldUserLockoutDisable
return handleError(err)
}
if b.Core.logger.IsInfo() {
b.Core.logger.Info("tuning of user_lockout_config successful", "path", path)
}
}
}
if rawVal, ok := data.GetOk("description"); ok {
description := rawVal.(string)
@@ -5021,6 +5142,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",
`

View File

@@ -1542,6 +1542,10 @@ func (b *SystemBackend) authPaths() []*framework.Path {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["token_type"][0]),
},
"user_lockout_config": {
Type: framework.TypeMap,
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
},
"plugin_version": {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
@@ -1929,6 +1933,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
},
"user_lockout_config": {
Type: framework.TypeMap,
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{

View File

@@ -351,6 +351,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"`
// PluginName is the name of the plugin registered in the catalog.
//
@@ -358,6 +359,20 @@ type MountConfig struct {
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
}
type UserLockoutConfig struct {
LockoutThreshold uint64 `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
LockoutDuration time.Duration `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
LockoutCounterReset time.Duration `json:"lockout_counter_reset,omitempty" structs:"lockout_counter_reset" mapstructure:"lockout_counter_reset"`
DisableLockout bool `json:"disable_lockout,omitempty" structs:"disable_lockout" mapstructure:"disable_lockout"`
}
type APIUserLockoutConfig struct {
LockoutThreshold string `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
LockoutDuration string `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
LockoutCounterResetDuration string `json:"lockout_counter_reset_duration,omitempty" structs:"lockout_counter_reset_duration" mapstructure:"lockout_counter_reset_duration"`
DisableLockout *bool `json:"lockout_disable,omitempty" structs:"lockout_disable" mapstructure:"lockout_disable"`
}
// APIMountConfig is an embedded struct of api.MountConfigInput
type APIMountConfig struct {
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
@@ -370,6 +385,7 @@ type APIMountConfig struct {
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
TokenType string `json:"token_type" 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"`
PluginVersion string `json:"plugin_version,omitempty" mapstructure:"plugin_version"`
// PluginName is the name of the plugin registered in the catalog.
@@ -1275,8 +1291,8 @@ func (c *Core) runMountUpdates(ctx context.Context, needPersist bool) error {
entry.NamespaceID = namespace.RootNamespaceID
needPersist = true
}
}
}
// Done if we have restored the mount table and we don't need
// to persist
if !needPersist {