diff --git a/changelog/25098.txt b/changelog/25098.txt new file mode 100644 index 000000000000..ab487d63148b --- /dev/null +++ b/changelog/25098.txt @@ -0,0 +1,4 @@ +```release-note:improvement +limits: Add a listener configuration option `disable_request_limiter` to allow +disabling the request limiter per-listener. +``` diff --git a/command/agent_test.go b/command/agent_test.go index 521070736d98..0bb8ca38af41 100644 --- a/command/agent_test.go +++ b/command/agent_test.go @@ -350,11 +350,13 @@ listener "tcp" { address = "%s" tls_disable = true require_request_header = false + disable_request_limiter = false } listener "tcp" { address = "%s" tls_disable = true require_request_header = true + disable_request_limiter = true } ` listenAddr1 := generateListenerAddress(t) diff --git a/command/server.go b/command/server.go index fa0c01aead96..383e4aca66b4 100644 --- a/command/server.go +++ b/command/server.go @@ -901,6 +901,8 @@ func (c *ServerCommand) InitListeners(config *server.Config, disableClustering b } props["max_request_duration"] = lnConfig.MaxRequestDuration.String() + props["disable_request_limiter"] = strconv.FormatBool(lnConfig.DisableRequestLimiter) + if lnConfig.ChrootNamespace != "" { props["chroot_namespace"] = lnConfig.ChrootNamespace } diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index 5bfbcafa414e..8da927e2beea 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -611,6 +611,7 @@ func testLoadConfigFile_json(t *testing.T) { Type: "tcp", Address: "127.0.0.1:443", CustomResponseHeaders: DefaultCustomHeaders, + DisableRequestLimiter: false, }, }, @@ -789,8 +790,9 @@ func testConfig_Sanitized(t *testing.T) { "listeners": []interface{}{ map[string]interface{}{ "config": map[string]interface{}{ - "address": "127.0.0.1:443", - "chroot_namespace": "admin/", + "address": "127.0.0.1:443", + "chroot_namespace": "admin/", + "disable_request_limiter": false, }, "type": configutil.TCP, }, @@ -889,6 +891,7 @@ listener "tcp" { redact_addresses = true redact_cluster_name = true redact_version = true + disable_request_limiter = true } listener "unix" { address = "/var/run/vault.sock" @@ -951,6 +954,7 @@ listener "unix" { RedactAddresses: true, RedactClusterName: true, RedactVersion: true, + DisableRequestLimiter: true, }, { Type: "unix", diff --git a/command/server/test-fixtures/config3.hcl b/command/server/test-fixtures/config3.hcl index dcc2afedf54c..587698b35e9e 100644 --- a/command/server/test-fixtures/config3.hcl +++ b/command/server/test-fixtures/config3.hcl @@ -13,6 +13,7 @@ cluster_addr = "top_level_cluster_addr" listener "tcp" { address = "127.0.0.1:443" chroot_namespace="admin/" + disable_request_limiter = false } backend "consul" { diff --git a/http/handler.go b/http/handler.go index c6efbe5d5154..fd920394fa7d 100644 --- a/http/handler.go +++ b/http/handler.go @@ -263,6 +263,10 @@ func handler(props *vault.HandlerProperties) http.Handler { wrappedHandler = disableReplicationStatusEndpointWrapping(wrappedHandler) } + if props.ListenerConfig != nil && props.ListenerConfig.DisableRequestLimiter { + wrappedHandler = wrapRequestLimiterHandler(wrappedHandler, props) + } + return wrappedHandler } @@ -910,6 +914,15 @@ func forwardRequest(core *vault.Core, w http.ResponseWriter, r *http.Request) { } func acquireLimiterListener(core *vault.Core, rawReq *http.Request, r *logical.Request) (*limits.RequestListener, bool) { + var disable bool + disableRequestLimiter := rawReq.Context().Value(logical.CtxKeyDisableRequestLimiter{}) + if disableRequestLimiter != nil { + disable = disableRequestLimiter.(bool) + } + if disable { + return &limits.RequestListener{}, true + } + lim := &limits.RequestLimiter{} if r.PathLimited { lim = core.GetRequestLimiter(limits.SpecialPathLimiter) diff --git a/http/util.go b/http/util.go index 4de8f8132646..9293120cc029 100644 --- a/http/util.go +++ b/http/util.go @@ -43,6 +43,19 @@ func wrapMaxRequestSizeHandler(handler http.Handler, props *vault.HandlerPropert }) } +func wrapRequestLimiterHandler(handler http.Handler, props *vault.HandlerProperties) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + request := r.WithContext( + context.WithValue( + r.Context(), + logical.CtxKeyDisableRequestLimiter{}, + props.ListenerConfig.DisableRequestLimiter, + ), + ) + handler.ServeHTTP(w, request) + }) +} + func rateLimitQuotaWrapping(handler http.Handler, core *vault.Core) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ns, err := namespace.FromContext(r.Context()) diff --git a/internalshared/configutil/listener.go b/internalshared/configutil/listener.go index de9c6096cdde..2c526eca9bcc 100644 --- a/internalshared/configutil/listener.go +++ b/internalshared/configutil/listener.go @@ -143,6 +143,10 @@ type Listener struct { // DisableReplicationStatusEndpoint disables the unauthenticated replication status endpoints DisableReplicationStatusEndpointsRaw interface{} `hcl:"disable_replication_status_endpoints"` DisableReplicationStatusEndpoints bool `hcl:"-"` + + // DisableRequestLimiter allows per-listener disabling of the Request Limiter. + DisableRequestLimiterRaw any `hcl:"disable_request_limiter"` + DisableRequestLimiter bool `hcl:"-"` } // AgentAPI allows users to select which parts of the Agent API they want enabled. @@ -257,6 +261,7 @@ func parseListener(item *ast.ObjectItem) (*Listener, error) { l.parseChrootNamespaceSettings, l.parseRedactionSettings, l.parseDisableReplicationStatusEndpointSettings, + l.parseDisableRequestLimiter, } { err := parser() if err != nil { @@ -370,6 +375,17 @@ func (l *Listener) parseDisableReplicationStatusEndpointSettings() error { return nil } +// parseDisableRequestLimiter attempts to parse the raw disable_request_limiter +// setting. The receiving Listener's DisableRequestLimiter field will be set +// with the successfully parsed value or return an error +func (l *Listener) parseDisableRequestLimiter() error { + if err := parseAndClearBool(&l.DisableRequestLimiterRaw, &l.DisableRequestLimiter); err != nil { + return fmt.Errorf("invalid value for disable_request_limiter: %w", err) + } + + return nil +} + // parseChrootNamespace attempts to parse the raw listener chroot namespace settings. // The state of the listener will be modified, raw data will be cleared upon // successful parsing. @@ -446,6 +462,10 @@ func (l *Listener) parseRequestSettings() error { return fmt.Errorf("invalid value for require_request_header: %w", err) } + if err := parseAndClearBool(&l.DisableRequestLimiterRaw, &l.DisableRequestLimiter); err != nil { + return fmt.Errorf("invalid value for disable_request_limiter: %w", err) + } + return nil } diff --git a/internalshared/configutil/listener_test.go b/internalshared/configutil/listener_test.go index 6205ccf773f7..bfd922faa89b 100644 --- a/internalshared/configutil/listener_test.go +++ b/internalshared/configutil/listener_test.go @@ -181,14 +181,16 @@ func TestListener_parseRequestSettings(t *testing.T) { t.Parallel() tests := map[string]struct { - rawMaxRequestSize any - expectedMaxRequestSize int64 - rawMaxRequestDuration any - expectedDuration time.Duration - rawRequireRequestHeader any - expectedRequireRequestHeader bool - isErrorExpected bool - errorMessage string + rawMaxRequestSize any + expectedMaxRequestSize int64 + rawMaxRequestDuration any + expectedDuration time.Duration + rawRequireRequestHeader any + expectedRequireRequestHeader bool + rawDisableRequestLimiter any + expectedDisableRequestLimiter bool + isErrorExpected bool + errorMessage string }{ "nil": { isErrorExpected: false, @@ -224,6 +226,17 @@ func TestListener_parseRequestSettings(t *testing.T) { expectedRequireRequestHeader: true, isErrorExpected: false, }, + "disable-request-limiter-bad": { + rawDisableRequestLimiter: "badvalue", + expectedDisableRequestLimiter: false, + isErrorExpected: true, + errorMessage: "invalid value for disable_request_limiter", + }, + "disable-request-limiter-good": { + rawDisableRequestLimiter: "true", + expectedDisableRequestLimiter: true, + isErrorExpected: false, + }, } for name, tc := range tests { @@ -234,9 +247,10 @@ func TestListener_parseRequestSettings(t *testing.T) { // Configure listener with raw values l := &Listener{ - MaxRequestSizeRaw: tc.rawMaxRequestSize, - MaxRequestDurationRaw: tc.rawMaxRequestDuration, - RequireRequestHeaderRaw: tc.rawRequireRequestHeader, + MaxRequestSizeRaw: tc.rawMaxRequestSize, + MaxRequestDurationRaw: tc.rawMaxRequestDuration, + RequireRequestHeaderRaw: tc.rawRequireRequestHeader, + DisableRequestLimiterRaw: tc.rawDisableRequestLimiter, } err := l.parseRequestSettings() @@ -251,11 +265,13 @@ func TestListener_parseRequestSettings(t *testing.T) { require.Equal(t, tc.expectedMaxRequestSize, l.MaxRequestSize) require.Equal(t, tc.expectedDuration, l.MaxRequestDuration) require.Equal(t, tc.expectedRequireRequestHeader, l.RequireRequestHeader) + require.Equal(t, tc.expectedDisableRequestLimiter, l.DisableRequestLimiter) // Ensure the state was modified for the raw values. require.Nil(t, l.MaxRequestSizeRaw) require.Nil(t, l.MaxRequestDurationRaw) require.Nil(t, l.RequireRequestHeaderRaw) + require.Nil(t, l.DisableRequestLimiterRaw) } }) } diff --git a/sdk/logical/request.go b/sdk/logical/request.go index 0faa6a59fae9..a291795648ad 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -543,3 +543,9 @@ func ContextOriginalBodyValue(ctx context.Context) (io.ReadCloser, bool) { func CreateContextOriginalBody(parent context.Context, body io.ReadCloser) context.Context { return context.WithValue(parent, ctxKeyOriginalBody{}, body) } + +type CtxKeyDisableRequestLimiter struct{} + +func (c CtxKeyDisableRequestLimiter) String() string { + return "disable_request_limiter" +}