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 all commits
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
64 changes: 39 additions & 25 deletions api/sys_mounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,20 +254,20 @@ type MountInput struct {
}

type MountConfigInput struct {
Options map[string]string `json:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
Description *string `json:"description,omitempty" mapstructure:"description"`
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
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"`
PluginVersion string `json:"plugin_version,omitempty"`

Options map[string]string `json:"options" mapstructure:"options"`
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
Description *string `json:"description,omitempty" mapstructure:"description"`
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
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"`
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"`
}
Expand All @@ -289,21 +289,35 @@ type MountOutput struct {
}

type MountConfigOutput struct {
DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
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"`

DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
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"`
}
Expand Down
3 changes: 3 additions & 0 deletions changelog/17338.txt
Original file line number Diff line number Diff line change
@@ -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
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?

```
81 changes: 69 additions & 12 deletions command/auth_tune.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ var (
type AuthTuneCommand struct {
*BaseCommand

flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagDefaultLeaseTTL time.Duration
flagDescription string
flagListingVisibility string
flagMaxLeaseTTL time.Duration
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagTokenType string
flagVersion int
flagPluginVersion string
flagAuditNonHMACRequestKeys []string
flagAuditNonHMACResponseKeys []string
flagDefaultLeaseTTL time.Duration
flagDescription string
flagListingVisibility string
flagMaxLeaseTTL time.Duration
flagPassthroughRequestHeaders []string
flagAllowedResponseHeaders []string
flagOptions map[string]string
flagTokenType string
flagVersion int
flagPluginVersion string
flagUserLockoutThreshold uint
flagUserLockoutDuration time.Duration
flagUserLockoutCounterResetDuration time.Duration
flagUserLockoutDisable bool
}

func (c *AuthTuneCommand) Synopsis() string {
Expand Down Expand Up @@ -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, " +
akshya96 marked this conversation as resolved.
Show resolved Hide resolved
"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,
Expand Down Expand Up @@ -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
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"
// flagNameDisableRedirects is used to prevent the client from honoring a single redirect as a response to a request
flagNameDisableRedirects = "disable-redirects"
)
Expand Down
4 changes: 4 additions & 0 deletions command/server/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
62 changes: 62 additions & 0 deletions command/server/config_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions internalshared/configutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type SharedConfig struct {

Listeners []*Listener `hcl:"-"`

UserLockouts []*UserLockout `hcl:"-"`
akshya96 marked this conversation as resolved.
Show resolved Hide resolved

Seals []*KMS `hcl:"-"`
Entropy *Entropy `hcl:"-"`

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
hghaf099 marked this conversation as resolved.
Show resolved Hide resolved
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{}
Expand Down
7 changes: 7 additions & 0 deletions internalshared/configutil/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading