From 0cc063e3391b7d251ed21a16570dd6a6a845e175 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Thu, 24 Oct 2024 15:47:17 +0000 Subject: [PATCH] backport of commit 415d26099536a6cdc173f87c23af1c7f231eda2c --- api/sys_mounts.go | 61 ++++++++++--------- changelog/28752.txt | 3 + command/auth_enable.go | 48 +++++++++------ command/auth_enable_test.go | 4 ++ command/auth_tune.go | 11 ++++ command/auth_tune_test.go | 4 ++ command/commands.go | 2 + command/secrets_enable.go | 52 ++++++++++------ command/secrets_enable_test.go | 4 ++ command/secrets_tune.go | 39 +++++++----- command/secrets_tune_test.go | 4 ++ vault/logical_system.go | 41 ++++++++++++- vault/logical_system_paths.go | 16 +++++ vault/mount.go | 56 +++++++++-------- vault/request_handling.go | 39 ++++++------ vault/router.go | 19 +++--- website/content/docs/commands/auth/enable.mdx | 4 ++ website/content/docs/commands/auth/tune.mdx | 4 ++ .../content/docs/commands/secrets/enable.mdx | 4 ++ .../content/docs/commands/secrets/tune.mdx | 4 ++ website/content/docs/secrets/pki/cmpv2.mdx | 9 +-- 21 files changed, 290 insertions(+), 138 deletions(-) create mode 100644 changelog/28752.txt diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 64529986af6a..7775c67f5908 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -290,23 +290,23 @@ 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"` - UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"` - DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` - IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` - + 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"` + DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` + IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + TrimRequestTrailingSlashes *bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // Deprecated: This field will always be blank for newer server responses. PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"` } @@ -328,19 +328,20 @@ 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"` - UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"` - DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` - IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + 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"` + DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` + IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // Deprecated: This field will always be blank for newer server responses. PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"` diff --git a/changelog/28752.txt b/changelog/28752.txt new file mode 100644 index 000000000000..9cc548ecc0c1 --- /dev/null +++ b/changelog/28752.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core: Add a mount tuneable that trims trailing slashes of request paths during POST. Needed to support CMPv2 in PKI. +``` diff --git a/command/auth_enable.go b/command/auth_enable.go index dcea5141fcf0..2bdc84fad4fb 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -23,24 +23,25 @@ var ( type AuthEnableCommand struct { *BaseCommand - flagDescription string - flagPath string - flagDefaultLeaseTTL time.Duration - flagMaxLeaseTTL time.Duration - flagAuditNonHMACRequestKeys []string - flagAuditNonHMACResponseKeys []string - flagListingVisibility string - flagPluginName string - flagPassthroughRequestHeaders []string - flagAllowedResponseHeaders []string - flagOptions map[string]string - flagLocal bool - flagSealWrap bool - flagExternalEntropyAccess bool - flagTokenType string - flagVersion int - flagPluginVersion string - flagIdentityTokenKey string + flagDescription string + flagPath string + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration + flagAuditNonHMACRequestKeys []string + flagAuditNonHMACResponseKeys []string + flagListingVisibility string + flagPluginName string + flagPassthroughRequestHeaders []string + flagAllowedResponseHeaders []string + flagOptions map[string]string + flagLocal bool + flagSealWrap bool + flagExternalEntropyAccess bool + flagTokenType string + flagVersion int + flagPluginVersion string + flagIdentityTokenKey string + flagTrimRequestTrailingSlashes BoolPtr } func (c *AuthEnableCommand) Synopsis() string { @@ -217,6 +218,12 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Usage: "Select the key used to sign plugin identity tokens.", }) + f.BoolPtrVar(&BoolPtrVar{ + Name: flagNameTrimRequestTrailingSlashes, + Target: &c.flagTrimRequestTrailingSlashes, + Usage: "Whether to trim trailing slashes for incoming requests to this mount", + }) + return set } @@ -324,6 +331,11 @@ func (c *AuthEnableCommand) Run(args []string) int { if fl.Name == flagNameIdentityTokenKey { authOpts.Config.IdentityTokenKey = c.flagIdentityTokenKey } + + if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() { + val := c.flagTrimRequestTrailingSlashes.Get() + authOpts.Config.TrimRequestTrailingSlashes = &val + } }) if err := client.Sys().EnableAuthWithOptions(authPath, authOpts); err != nil { diff --git a/command/auth_enable_test.go b/command/auth_enable_test.go index 3467c9b00657..59d610c358c0 100644 --- a/command/auth_enable_test.go +++ b/command/auth_enable_test.go @@ -100,6 +100,7 @@ func TestAuthEnableCommand_Run(t *testing.T) { "-allowed-response-headers", "authorization", "-listing-visibility", "unauth", "-identity-token-key", "default", + "-trim-request-trailing-slashes=true", "userpass", }) if exp := 0; code != exp { @@ -127,6 +128,9 @@ func TestAuthEnableCommand_Run(t *testing.T) { if exp := "The best kind of test"; authInfo.Description != exp { t.Errorf("expected %q to be %q", authInfo.Description, exp) } + if !authInfo.Config.TrimRequestTrailingSlashes { + t.Errorf("expected trim_request_trailing_slashes to be enabled") + } if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, authInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) } diff --git a/command/auth_tune.go b/command/auth_tune.go index 56c2d25fae96..243f7012fa22 100644 --- a/command/auth_tune.go +++ b/command/auth_tune.go @@ -40,6 +40,7 @@ type AuthTuneCommand struct { flagUserLockoutCounterResetDuration time.Duration flagUserLockoutDisable bool flagIdentityTokenKey string + flagTrimRequestTrailingSlashes BoolPtr } func (c *AuthTuneCommand) Synopsis() string { @@ -195,6 +196,11 @@ func (c *AuthTuneCommand) Flags() *FlagSets { Usage: "Select the semantic version of the plugin to run. The new version must be registered in " + "the plugin catalog, and will not start running until the plugin is reloaded.", }) + f.BoolPtrVar(&BoolPtrVar{ + Name: flagNameTrimRequestTrailingSlashes, + Target: &c.flagTrimRequestTrailingSlashes, + Usage: "Whether to trim trailing slashes for incoming requests to this mount", + }) f.StringVar(&StringVar{ Name: flagNameIdentityTokenKey, @@ -306,6 +312,11 @@ func (c *AuthTuneCommand) Run(args []string) int { if fl.Name == flagNameIdentityTokenKey { mountConfigInput.IdentityTokenKey = c.flagIdentityTokenKey } + + if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() { + val := c.flagTrimRequestTrailingSlashes.Get() + mountConfigInput.TrimRequestTrailingSlashes = &val + } }) // Append /auth (since that's where auths live) and a trailing slash to diff --git a/command/auth_tune_test.go b/command/auth_tune_test.go index c9b7923d83de..6fc238fab047 100644 --- a/command/auth_tune_test.go +++ b/command/auth_tune_test.go @@ -120,6 +120,7 @@ func TestAuthTuneCommand_Run(t *testing.T) { "-listing-visibility", "unauth", "-plugin-version", version, "-identity-token-key", "default", + "-trim-request-trailing-slashes=true", "my-auth/", }) if exp := 0; code != exp { @@ -156,6 +157,9 @@ func TestAuthTuneCommand_Run(t *testing.T) { if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) } + if !mountInfo.Config.TrimRequestTrailingSlashes { + t.Errorf("expected trim_request_trailing_slashes to be enabled") + } if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) } diff --git a/command/commands.go b/command/commands.go index a4253dd192d0..daa40ead48a9 100644 --- a/command/commands.go +++ b/command/commands.go @@ -97,6 +97,8 @@ const ( flagNamePluginVersion = "plugin-version" // flagNameIdentityTokenKey selects the key used to sign plugin identity tokens flagNameIdentityTokenKey = "identity-token-key" + // flagNameTrimRequestTrailingSlashes selects the key used to determine whether to trim trailing slashes + flagNameTrimRequestTrailingSlashes = "trim-request-trailing-slashes" // 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 diff --git a/command/secrets_enable.go b/command/secrets_enable.go index a73a5e49ef87..9a0bd140ea92 100644 --- a/command/secrets_enable.go +++ b/command/secrets_enable.go @@ -23,26 +23,27 @@ var ( type SecretsEnableCommand struct { *BaseCommand - flagDescription string - flagPath string - flagDefaultLeaseTTL time.Duration - flagMaxLeaseTTL time.Duration - flagAuditNonHMACRequestKeys []string - flagAuditNonHMACResponseKeys []string - flagListingVisibility string - flagPassthroughRequestHeaders []string - flagAllowedResponseHeaders []string - flagForceNoCache bool - flagPluginName string - flagPluginVersion string - flagOptions map[string]string - flagLocal bool - flagSealWrap bool - flagExternalEntropyAccess bool - flagVersion int - flagAllowedManagedKeys []string - flagDelegatedAuthAccessors []string - flagIdentityTokenKey string + flagDescription string + flagPath string + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration + flagAuditNonHMACRequestKeys []string + flagAuditNonHMACResponseKeys []string + flagListingVisibility string + flagPassthroughRequestHeaders []string + flagAllowedResponseHeaders []string + flagForceNoCache bool + flagPluginName string + flagPluginVersion string + flagOptions map[string]string + flagLocal bool + flagSealWrap bool + flagExternalEntropyAccess bool + flagVersion int + flagAllowedManagedKeys []string + flagDelegatedAuthAccessors []string + flagIdentityTokenKey string + flagTrimRequestTrailingSlashes BoolPtr } func (c *SecretsEnableCommand) Synopsis() string { @@ -245,6 +246,12 @@ func (c *SecretsEnableCommand) Flags() *FlagSets { Usage: "Select the key used to sign plugin identity tokens.", }) + f.BoolPtrVar(&BoolPtrVar{ + Name: flagNameTrimRequestTrailingSlashes, + Target: &c.flagTrimRequestTrailingSlashes, + Usage: "Whether to trim trailing slashes for incoming requests to this mount", + }) + return set } @@ -359,6 +366,11 @@ func (c *SecretsEnableCommand) Run(args []string) int { if fl.Name == flagNameIdentityTokenKey { mountInput.Config.IdentityTokenKey = c.flagIdentityTokenKey } + + if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() { + val := c.flagTrimRequestTrailingSlashes.Get() + mountInput.Config.TrimRequestTrailingSlashes = &val + } }) if err := client.Sys().Mount(mountPath, mountInput); err != nil { diff --git a/command/secrets_enable_test.go b/command/secrets_enable_test.go index 3efc171a7be1..de4b9ae89b2c 100644 --- a/command/secrets_enable_test.go +++ b/command/secrets_enable_test.go @@ -120,6 +120,7 @@ func TestSecretsEnableCommand_Run(t *testing.T) { "-allowed-managed-keys", "key1,key2", "-identity-token-key", "default", "-delegated-auth-accessors", "authAcc1,authAcc2", + "-trim-request-trailing-slashes=true", "-force-no-cache", "pki", }) @@ -157,6 +158,9 @@ func TestSecretsEnableCommand_Run(t *testing.T) { if exp := true; mountInfo.Config.ForceNoCache != exp { t.Errorf("expected %t to be %t", mountInfo.Config.ForceNoCache, exp) } + if !mountInfo.Config.TrimRequestTrailingSlashes { + t.Errorf("expected trim_request_trailing_slashes to be enabled") + } if diff := deep.Equal([]string{"authorization,authentication", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { t.Errorf("Failed to find expected values in PassthroughRequestHeaders. Difference is: %v", diff) } diff --git a/command/secrets_tune.go b/command/secrets_tune.go index b853aec2711b..195f1303f582 100644 --- a/command/secrets_tune.go +++ b/command/secrets_tune.go @@ -23,20 +23,21 @@ var ( type SecretsTuneCommand struct { *BaseCommand - flagAuditNonHMACRequestKeys []string - flagAuditNonHMACResponseKeys []string - flagDefaultLeaseTTL time.Duration - flagDescription string - flagListingVisibility string - flagMaxLeaseTTL time.Duration - flagPassthroughRequestHeaders []string - flagAllowedResponseHeaders []string - flagOptions map[string]string - flagVersion int - flagPluginVersion string - flagAllowedManagedKeys []string - flagDelegatedAuthAccessors []string - flagIdentityTokenKey string + flagAuditNonHMACRequestKeys []string + flagAuditNonHMACResponseKeys []string + flagDefaultLeaseTTL time.Duration + flagDescription string + flagListingVisibility string + flagMaxLeaseTTL time.Duration + flagPassthroughRequestHeaders []string + flagAllowedResponseHeaders []string + flagOptions map[string]string + flagVersion int + flagPluginVersion string + flagAllowedManagedKeys []string + flagDelegatedAuthAccessors []string + flagIdentityTokenKey string + flagTrimRequestTrailingSlashes BoolPtr } func (c *SecretsTuneCommand) Synopsis() string { @@ -175,6 +176,12 @@ func (c *SecretsTuneCommand) Flags() *FlagSets { Usage: "Select the key used to sign plugin identity tokens.", }) + f.BoolPtrVar(&BoolPtrVar{ + Name: flagNameTrimRequestTrailingSlashes, + Target: &c.flagTrimRequestTrailingSlashes, + Usage: "Whether to trim trailing slashes for incoming requests to this mount", + }) + return set } @@ -267,6 +274,10 @@ func (c *SecretsTuneCommand) Run(args []string) int { if fl.Name == flagNameIdentityTokenKey { mountConfigInput.IdentityTokenKey = c.flagIdentityTokenKey } + if fl.Name == flagNameTrimRequestTrailingSlashes && c.flagTrimRequestTrailingSlashes.IsSet() { + val := c.flagTrimRequestTrailingSlashes.Get() + mountConfigInput.TrimRequestTrailingSlashes = &val + } }) if err := client.Sys().TuneMount(mountPath, mountConfigInput); err != nil { diff --git a/command/secrets_tune_test.go b/command/secrets_tune_test.go index b2d932779fd8..2d30e6b7a7e3 100644 --- a/command/secrets_tune_test.go +++ b/command/secrets_tune_test.go @@ -196,6 +196,7 @@ func TestSecretsTuneCommand_Run(t *testing.T) { "-listing-visibility", "unauth", "-plugin-version", version, "-delegated-auth-accessors", "authAcc1,authAcc2", + "-trim-request-trailing-slashes=true", "mount_tune_integration/", }) if exp := 0; code != exp { @@ -232,6 +233,9 @@ func TestSecretsTuneCommand_Run(t *testing.T) { if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) } + if !mountInfo.Config.TrimRequestTrailingSlashes { + t.Errorf("expected trim_request_trailing_slashes to be enabled") + } if diff := deep.Equal([]string{"authorization", "www-authentication"}, mountInfo.Config.PassthroughRequestHeaders); len(diff) > 0 { t.Errorf("Failed to find expected values for PassthroughRequestHeaders. Difference is: %v", diff) } diff --git a/vault/logical_system.go b/vault/logical_system.go index f57c239d8bb8..49a1e3186212 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -1389,7 +1389,9 @@ func (b *SystemBackend) mountInfo(ctx context.Context, entry *MountEntry, legacy entryConfig["max_lease_ttl"] = coreMaxTTL } } - + if entry.Config.TrimRequestTrailingSlashes { + entryConfig["trim_request_trailing_slashes"] = true + } if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string) } @@ -1613,6 +1615,10 @@ func (b *SystemBackend) handleMount(ctx context.Context, req *logical.Request, d config.ForceNoCache = true } + if apiConfig.TrimRequestTrailingSlashes { + config.TrimRequestTrailingSlashes = true + } + if err := checkListingVisibility(apiConfig.ListingVisibility); err != nil { return logical.ErrorResponse(fmt.Sprintf("invalid listing_visibility %s", apiConfig.ListingVisibility)), nil } @@ -2149,6 +2155,10 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) ( resp.Data["identity_token_key"] = rawVal.(string) } + if mountEntry.Config.TrimRequestTrailingSlashes { + resp.Data["trim_request_trailing_slashes"] = mountEntry.Config.TrimRequestTrailingSlashes + } + 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 @@ -2753,6 +2763,30 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, } } + if rawVal, ok := data.GetOk("trim_request_trailing_slashes"); ok { + trimRequestTrailingSlashes := rawVal.(bool) + + oldVal := mountEntry.Config.TrimRequestTrailingSlashes + mountEntry.Config.TrimRequestTrailingSlashes = trimRequestTrailingSlashes + + // Update the mount table + 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.TrimRequestTrailingSlashes = oldVal + return handleError(err) + } + + if b.Core.logger.IsInfo() { + b.Core.logger.Info("mount tuning of trim_request_trailing_slashes successful", "path", path) + } + } + var err error var resp *logical.Response var options map[string]string @@ -3269,6 +3303,7 @@ func (b *SystemBackend) handleEnableAuth(ctx context.Context, req *logical.Reque return logical.ErrorResponse(fmt.Sprintf("invalid listing_visibility %s", apiConfig.ListingVisibility)), nil } config.ListingVisibility = apiConfig.ListingVisibility + config.TrimRequestTrailingSlashes = apiConfig.TrimRequestTrailingSlashes if len(apiConfig.AuditNonHMACRequestKeys) > 0 { config.AuditNonHMACRequestKeys = apiConfig.AuditNonHMACRequestKeys @@ -7156,4 +7191,8 @@ This path responds to the following HTTP methods. `The label representing a path-prefix within the /.well-known/ path`, "", }, + "trim_request_trailing_slashes": { + `Whether to trim a trailing slash on incoming requests to this mount`, + "", + }, } diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 8eb98e427168..649ac9a790cb 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -3826,6 +3826,10 @@ func (b *SystemBackend) authPaths() []*framework.Path { Description: strings.TrimSpace(sysHelp["identity_token_key"][0]), Required: false, }, + "trim_request_trailing_slashes": { + Type: framework.TypeBool, + Required: false, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ @@ -3916,6 +3920,10 @@ func (b *SystemBackend) authPaths() []*framework.Path { Type: framework.TypeString, Required: false, }, + "trim_request_trailing_slashes": { + Type: framework.TypeBool, + Required: false, + }, }, }}, }, @@ -4686,6 +4694,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path { Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["identity_token_key"][0]), }, + "trim_request_trailing_slashes": { + Type: framework.TypeBool, + Description: strings.TrimSpace(sysHelp["trim_request_trailing_slashes"][0]), + }, }, Operations: map[logical.Operation]framework.OperationHandler{ @@ -4788,6 +4800,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path { Type: framework.TypeString, Required: false, }, + "trim_request_trailing_slashes": { + Type: framework.TypeBool, + Required: false, + }, }, }}, }, diff --git a/vault/mount.go b/vault/mount.go index 9561a680e12a..e3d356ce4b6a 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -349,19 +349,20 @@ type MountEntry struct { // MountConfig is used to hold settable options type MountConfig struct { - DefaultLeaseTTL time.Duration `json:"default_lease_ttl,omitempty" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default - MaxLeaseTTL time.Duration `json:"max_lease_ttl,omitempty" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default - ForceNoCache bool `json:"force_no_cache,omitempty" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` - PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` - 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"` - DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` - IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + DefaultLeaseTTL time.Duration `json:"default_lease_ttl,omitempty" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` // Override for global default + MaxLeaseTTL time.Duration `json:"max_lease_ttl,omitempty" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` // Override for global default + ForceNoCache bool `json:"force_no_cache,omitempty" structs:"force_no_cache" mapstructure:"force_no_cache"` // Override for global default + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` + 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"` + DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` + IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // If requests to this mount should have trailing slashes trimmed // PluginName is the name of the plugin registered in the catalog. // @@ -389,20 +390,21 @@ type APIUserLockoutConfig struct { // 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"` - MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` - ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` - AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` - AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` - ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` - PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` - 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"` - DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` - IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"` + MaxLeaseTTL string `json:"max_lease_ttl" structs:"max_lease_ttl" mapstructure:"max_lease_ttl"` + ForceNoCache bool `json:"force_no_cache" structs:"force_no_cache" mapstructure:"force_no_cache"` + AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" structs:"audit_non_hmac_request_keys" mapstructure:"audit_non_hmac_request_keys"` + AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" structs:"audit_non_hmac_response_keys" mapstructure:"audit_non_hmac_response_keys"` + ListingVisibility ListingVisibilityType `json:"listing_visibility,omitempty" structs:"listing_visibility" mapstructure:"listing_visibility"` + PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" structs:"passthrough_request_headers" mapstructure:"passthrough_request_headers"` + 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"` + DelegatedAuthAccessors []string `json:"delegated_auth_accessors,omitempty" mapstructure:"delegated_auth_accessors"` + IdentityTokenKey string `json:"identity_token_key,omitempty" mapstructure:"identity_token_key"` + TrimRequestTrailingSlashes bool `json:"trim_request_trailing_slashes,omitempty" mapstructure:"trim_request_trailing_slashes"` // If requests to this mount should have trailing slashes trimmed // PluginName is the name of the plugin registered in the catalog. // diff --git a/vault/request_handling.go b/vault/request_handling.go index bae646094384..e8244f06af0b 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -612,27 +612,11 @@ func (c *Core) switchedLockHandleRequest(httpCtx context.Context, req *logical.R } func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request) (resp *logical.Response, err error) { - // Allowing writing to a path ending in / makes it extremely difficult to - // understand user intent for the filesystem-like backends (kv, - // cubbyhole) -- did they want a key named foo/ or did they want to write - // to a directory foo/ with no (or forgotten) key, or...? It also affects - // lookup, because paths ending in / are considered prefixes by some - // backends. Basically, it's all just terrible, so don't allow it. - if strings.HasSuffix(req.Path, "/") && - (req.Operation == logical.UpdateOperation || - req.Operation == logical.CreateOperation || - req.Operation == logical.PatchOperation) { - return logical.ErrorResponse("cannot write to a path ending in '/'"), nil - } waitGroup, err := waitForReplicationState(ctx, c, req) if err != nil { return nil, err } - // MountPoint will not always be set at this point, so we ensure the req contains it - // as it is depended on by some functionality (e.g. quotas) - req.MountPoint = c.router.MatchingMount(ctx, req.Path) - // Decrement the wait group when our request is done if waitGroup != nil { defer waitGroup.Done() @@ -642,6 +626,28 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request return nil, logical.ErrMissingRequiredState } + // Ensure the req contains a MountPoint as it is depended on by some + // functionality (e.g. quotas) + var entry *MountEntry + req.MountPoint, entry = c.router.MatchingMountAndEntry(ctx, req.Path) + + // Allowing writing to a path ending in / makes it extremely difficult to + // understand user intent for the filesystem-like backends (kv, + // cubbyhole) -- did they want a key named foo/ or did they want to write + // to a directory foo/ with no (or forgotten) key, or...? It also affects + // lookup, because paths ending in / are considered prefixes by some + // backends. Basically, it's all just terrible, so don't allow it. + if strings.HasSuffix(req.Path, "/") && + (req.Operation == logical.UpdateOperation || + req.Operation == logical.CreateOperation || + req.Operation == logical.PatchOperation) { + if entry == nil || !entry.Config.TrimRequestTrailingSlashes { + return logical.ErrorResponse("cannot write to a path ending in '/'"), nil + } else { + req.Path = strings.TrimSuffix(req.Path, "/") + } + } + err = c.PopulateTokenEntry(ctx, req) if err != nil { if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { @@ -892,7 +898,6 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request var nonHMACReqDataKeys []string var nonHMACRespDataKeys []string - entry := c.router.MatchingMountEntry(ctx, req.Path) if entry != nil { // Get and set ignored HMAC'd value. Reset those back to empty afterwards. if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok { diff --git a/vault/router.go b/vault/router.go index 2618101502ec..688707410fe6 100644 --- a/vault/router.go +++ b/vault/router.go @@ -373,23 +373,28 @@ func (r *Router) MatchingMountByAccessor(mountAccessor string) *MountEntry { // MatchingMount returns the mount prefix that would be used for a path func (r *Router) MatchingMount(ctx context.Context, path string) string { r.l.RLock() - mount := r.matchingMountInternal(ctx, path) + mount, _ := r.matchingMountInternal(ctx, path) r.l.RUnlock() return mount } -func (r *Router) matchingMountInternal(ctx context.Context, path string) string { +// MatchingMountAndEntry returns the MountEntry used for a path and it's router path +func (r *Router) MatchingMountAndEntry(ctx context.Context, path string) (string, *MountEntry) { + return r.matchingMountInternal(ctx, path) +} + +func (r *Router) matchingMountInternal(ctx context.Context, path string) (string, *MountEntry) { ns, err := namespace.FromContext(ctx) if err != nil { - return "" + return "", nil } path = ns.Path + path - mount, _, ok := r.root.LongestPrefix(path) + mount, raw, ok := r.root.LongestPrefix(path) if !ok { - return "" + return "", nil } - return mount + return mount, raw.(*routeEntry).mountEntry } // matchingPrefixInternal returns a mount prefix that a path may be a part of @@ -416,7 +421,7 @@ func (r *Router) matchingPrefixInternal(ctx context.Context, path string) string func (r *Router) MountConflict(ctx context.Context, path string) string { r.l.RLock() defer r.l.RUnlock() - if exactMatch := r.matchingMountInternal(ctx, path); exactMatch != "" { + if exactMatch, _ := r.matchingMountInternal(ctx, path); exactMatch != "" { return exactMatch } if prefixMatch := r.matchingPrefixInternal(ctx, path); prefixMatch != "" { diff --git a/website/content/docs/commands/auth/enable.mdx b/website/content/docs/commands/auth/enable.mdx index f19c418d080b..ccf24c98398a 100644 --- a/website/content/docs/commands/auth/enable.mdx +++ b/website/content/docs/commands/auth/enable.mdx @@ -89,6 +89,10 @@ flags](/vault/docs/commands) included on all commands. - `-token-type` `(string: "")` - Specifies the type of tokens that should be returned by the auth method. +- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to + this mount with trailing slashes will have those slashes trimmed. + Necessary for some standards based APIs handled by Vault. + - `-plugin-version` `(string: "")` - Configures the semantic version of the plugin to use. If unspecified, implies the built-in or any matching unversioned plugin that may have been registered. diff --git a/website/content/docs/commands/auth/tune.mdx b/website/content/docs/commands/auth/tune.mdx index 950868f35af1..248d27147441 100644 --- a/website/content/docs/commands/auth/tune.mdx +++ b/website/content/docs/commands/auth/tune.mdx @@ -168,6 +168,10 @@ flags](/vault/docs/commands) included on all commands. - `-token-type` `(string: "")` - Specifies the type of tokens that should be returned by the auth method. +- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to + this mount with trailing slashes will have those slashes trimmed. + Necessary for some standards based APIs handled by Vault. + - `-plugin-version` `(string: "")` - Configures the semantic version of the plugin to use. The new version will not start running until the mount is [reloaded](/vault/docs/commands/plugin/reload). diff --git a/website/content/docs/commands/secrets/enable.mdx b/website/content/docs/commands/secrets/enable.mdx index d26ab12bcd1b..445018b72f33 100644 --- a/website/content/docs/commands/secrets/enable.mdx +++ b/website/content/docs/commands/secrets/enable.mdx @@ -111,6 +111,10 @@ flags](/vault/docs/commands) included on all commands. backend can delegate authentication to. To allow multiple accessors, provide the `delegated-auth-accessors` multiple times, each time with 1 accessor. +- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to + this mount with trailing slashes will have those slashes trimmed. + Necessary for some standards based APIs handled by Vault. + - `-plugin-version` `(string: "")` - Configures the semantic version of the plugin to use. If unspecified, implies the built-in or any matching unversioned plugin that may have been registered. diff --git a/website/content/docs/commands/secrets/tune.mdx b/website/content/docs/commands/secrets/tune.mdx index 0bb31549f314..fc95cd426b33 100644 --- a/website/content/docs/commands/secrets/tune.mdx +++ b/website/content/docs/commands/secrets/tune.mdx @@ -97,6 +97,10 @@ flags](/vault/docs/commands) included on all commands. backend can delegate authentication to. To allow multiple accessors, provide the `delegated-auth-accessors` multiple times, each time with 1 accessor. +- `-trim-request-trailing-slashes` `(bool: false)` - If true, requests to + this mount with trailing slashes will have those slashes trimmed. + Necessary for some standards based APIs handled by Vault. + - `-plugin-version` `(string: "")` - Configures the semantic version of the plugin to use. The new version will not start running until the mount is [reloaded](/vault/docs/commands/plugin/reload). diff --git a/website/content/docs/secrets/pki/cmpv2.mdx b/website/content/docs/secrets/pki/cmpv2.mdx index 457d44528212..14ea0874b6a5 100644 --- a/website/content/docs/secrets/pki/cmpv2.mdx +++ b/website/content/docs/secrets/pki/cmpv2.mdx @@ -87,10 +87,10 @@ To get an authentication mount's accessor field, the following command can be us $ vault read -field=accessor sys/auth/auth/cert ``` -For CMP to work within certain clients, a few response headers need to be explicitly allowed -along with configuring the list of accessors the mount can delegate authentication towards. -The following will grant the required response headers, you will need to replace the values for the `delegated-auth-accessors` -to match your values. +For CMP to work within certain clients, a few response headers need to be explicitly +allowed, trailing slashes must be trimmed, and the list of accessors the mount can delegate authentication towards +must be configured. The following will grant the required response headers, you will need to replace the values for +the `delegated-auth-accessors` to match your values. ```shell-session $ vault secrets tune \ @@ -98,6 +98,7 @@ $ vault secrets tune \ -allowed-response-headers="Content-Length" \ -allowed-response-headers="WWW-Authenticate" \ -delegated-auth-accessors="auth_cert_4088ac2d" \ + -trim-request-trailing-slashes="true" \ pki ```