Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3b15df1
add timeouts to prevent slowloris attacks
sanikachavan5 Sep 9, 2025
7889552
rename
sanikachavan5 Sep 9, 2025
50dfa2e
fix tests
sanikachavan5 Sep 11, 2025
d9fc2ba
fix test
sanikachavan5 Sep 14, 2025
7428213
fix failing test
sanikachavan5 Sep 14, 2025
1c88f9b
add docs and tests
sanikachavan5 Sep 15, 2025
0d886d3
lint issues
sanikachavan5 Sep 15, 2025
348a265
Added support for IPv6 virtual IP offset calculation and validation. …
nitin-sachdev-29 Sep 15, 2025
9b0cf47
Latest Envoy version update - default v1.34.7 (#22735)
LakshmiNarayananDesikan Sep 15, 2025
8bf06ae
docs: Additional entries for versioned redirects (#22694)
boruszak Sep 16, 2025
ada5f88
added BinAddr field in agent/self API response (#22761)
nitin-sachdev-29 Sep 17, 2025
fe6149f
PKCE and Adding private key JWT support for OIDC (#22732)
mansi991999 Sep 17, 2025
f16fc3d
Submodules Version upgrade (#22776)
LakshmiNarayananDesikan Sep 17, 2025
d276953
[CSL-11760] [Envoy Bootstrap] Defaults to IPv6 for admin-bind and grp…
anilvpatel Sep 18, 2025
df05881
update: default upstream.local_bind_address to ::1 for IPv6 agent bin…
sreeram77 Sep 18, 2025
75ae71a
update: set proxy.local_service_address to ::1 for IPv6 agent bind ad…
sreeram77 Sep 18, 2025
cdd5f65
update: default proxy BindAddress to :: for IPv6 agent bind addr (#22…
sreeram77 Sep 18, 2025
71ca1e2
Consul ENT default version change #22783 (#22784)
LakshmiNarayananDesikan Sep 18, 2025
96727d9
[Bugfix]: suppress lacks token permission while checking dual stack (…
anilvpatel Sep 19, 2025
0e47716
updated redhat image to latest (#22794)
Manishakumari-hc Sep 19, 2025
6ea17e9
Suppress CVEs (#22801)
Manishakumari-hc Sep 22, 2025
db4cc0d
redhat version revert (#22806)
Manishakumari-hc Sep 22, 2025
4b531d8
Suppress CVE-2025-6395 (#22808)
Manishakumari-hc Sep 22, 2025
71273b9
fix path cleaning of proxied urls (#22671)
sanikachavan5 Sep 22, 2025
df3175d
remove usage of dynamic GitHub actions variable (#22725)
sanikachavan5 Sep 22, 2025
ac37acb
Multi Port Service Discovery (#22769)
sriramr98 Sep 22, 2025
5bc00c7
add timeouts to prevent slowloris attacks
sanikachavan5 Sep 9, 2025
f0386fd
Merge branch 'main' into fix-slowloris-http-endpoints
sanikachavan5 Sep 23, 2025
acb6038
Delete .changelog/22625.txt
sanikachavan5 Sep 23, 2025
76315fa
doc changes
sanikachavan5 Sep 23, 2025
294799a
run codegen
sanikachavan5 Sep 23, 2025
e661867
Merge branch 'main' into fix-slowloris-http-endpoints
sanikachavan5 Sep 23, 2025
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
3 changes: 3 additions & 0 deletions .changelog/22739.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:security
security: Configure HTTP server timeouts to prevent Slowloris denial-of-service attacks on agent HTTP endpoints and connect proxy pprof endpoints
```
12 changes: 8 additions & 4 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,14 @@ func (a *Agent) listenHTTP() ([]apiServer, error) {
a.configReloaders = append(a.configReloaders, srv.ReloadConfig)
a.httpHandlers = srv
httpServer := &http.Server{
Addr: l.Addr().String(),
TLSConfig: tlscfg,
Handler: srv.handler(),
MaxHeaderBytes: a.config.HTTPMaxHeaderBytes,
Addr: l.Addr().String(),
TLSConfig: tlscfg,
Handler: srv.handler(),
MaxHeaderBytes: a.config.HTTPMaxHeaderBytes,
ReadHeaderTimeout: a.config.HTTPReadHeaderTimeout,
ReadTimeout: a.config.HTTPReadTimeout,
WriteTimeout: a.config.HTTPWriteTimeout,
IdleTimeout: a.config.HTTPIdleTimeout,
}

if scada.IsCapability(l.Addr()) {
Expand Down
43 changes: 43 additions & 0 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6469,6 +6469,49 @@ func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) {
}
}

func TestAgent_HTTPServerTimeouts(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()

// Test with custom timeout configuration
a := NewTestAgent(t, `
http_config = {
read_timeout = "5s"
read_header_timeout = "2s"
write_timeout = "5s"
idle_timeout = "60s"
}
`)
defer a.Shutdown()

// Verify timeout values are configured correctly
require.Equal(t, 5*time.Second, a.config.HTTPReadTimeout)
require.Equal(t, 2*time.Second, a.config.HTTPReadHeaderTimeout)
require.Equal(t, 5*time.Second, a.config.HTTPWriteTimeout)
require.Equal(t, 60*time.Second, a.config.HTTPIdleTimeout)
}

func TestAgent_HTTPServerDefaultTimeouts(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}

t.Parallel()

// Test with default timeout configuration (no explicit timeouts set)
a := NewTestAgent(t, "")
defer a.Shutdown()

// Verify default timeout values are applied
require.Equal(t, 30*time.Second, a.config.HTTPReadTimeout)
require.Equal(t, 10*time.Second, a.config.HTTPReadHeaderTimeout)
require.Equal(t, 30*time.Second, a.config.HTTPWriteTimeout)
require.Equal(t, 120*time.Second, a.config.HTTPIdleTimeout)
}

func TestAgent_ServiceRegistration(t *testing.T) {
// Since we accept both `port` and `ports` for service registration, we need to ensure that catalog stores it as it gets it

Expand Down
22 changes: 13 additions & 9 deletions agent/config/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -924,15 +924,19 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
DNSCacheMaxAge: b.durationVal("dns_config.cache_max_age", c.DNS.CacheMaxAge),

// HTTP
HTTPPort: httpPort,
HTTPSPort: httpsPort,
HTTPAddrs: httpAddrs,
HTTPSAddrs: httpsAddrs,
HTTPBlockEndpoints: c.HTTPConfig.BlockEndpoints,
HTTPMaxHeaderBytes: intVal(c.HTTPConfig.MaxHeaderBytes),
HTTPResponseHeaders: c.HTTPConfig.ResponseHeaders,
AllowWriteHTTPFrom: b.cidrsVal("allow_write_http_from", c.HTTPConfig.AllowWriteHTTPFrom),
HTTPUseCache: boolValWithDefault(c.HTTPConfig.UseCache, true),
HTTPPort: httpPort,
HTTPSPort: httpsPort,
HTTPAddrs: httpAddrs,
HTTPSAddrs: httpsAddrs,
HTTPBlockEndpoints: c.HTTPConfig.BlockEndpoints,
HTTPMaxHeaderBytes: intVal(c.HTTPConfig.MaxHeaderBytes),
HTTPResponseHeaders: c.HTTPConfig.ResponseHeaders,
AllowWriteHTTPFrom: b.cidrsVal("allow_write_http_from", c.HTTPConfig.AllowWriteHTTPFrom),
HTTPUseCache: boolValWithDefault(c.HTTPConfig.UseCache, true),
HTTPReadTimeout: b.durationValWithDefaultMin("http_config.read_timeout", c.HTTPConfig.ReadTimeout, 30*time.Second, 1*time.Second),
HTTPReadHeaderTimeout: b.durationValWithDefaultMin("http_config.read_header_timeout", c.HTTPConfig.ReadHeaderTimeout, 10*time.Second, 1*time.Second),
HTTPWriteTimeout: b.durationValWithDefaultMin("http_config.write_timeout", c.HTTPConfig.WriteTimeout, 30*time.Second, 1*time.Second),
HTTPIdleTimeout: b.durationValWithDefaultMin("http_config.idle_timeout", c.HTTPConfig.IdleTimeout, 120*time.Second, 10*time.Second),

// Telemetry
Telemetry: lib.TelemetryConfig{
Expand Down
240 changes: 240 additions & 0 deletions agent/config/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,246 @@ func TestBuilder_ServiceVal_MultiError(t *testing.T) {
require.Contains(t, b.err.Error(), "cannot have both socket path")
}

func TestBuilder_DurationVal_EdgeCases(t *testing.T) {
testCases := []struct {
name string
input string
expectError bool
errorMsg string
}{
{
name: "negative duration",
input: "-5s",
expectError: false, // time.ParseDuration allows negative durations
},
{
name: "zero duration",
input: "0s",
expectError: false,
},
{
name: "unparseable string - no unit",
input: "123",
expectError: true,
errorMsg: "time: missing unit in duration",
},
{
name: "unparseable string - invalid unit",
input: "5x",
expectError: true,
errorMsg: "unknown unit",
},
{
name: "unparseable string - empty",
input: "",
expectError: true,
errorMsg: "invalid duration",
},
{
name: "unparseable string - just letters",
input: "abc",
expectError: true,
errorMsg: "invalid duration",
},
{
name: "unparseable string - mixed invalid",
input: "5s10x",
expectError: true,
errorMsg: "unknown unit",
},
{
name: "very large duration",
input: "8760h", // 1 year in hours
expectError: false,
},
{
name: "fractional seconds",
input: "1.5s",
expectError: false,
},
{
name: "complex valid duration",
input: "1h30m45s",
expectError: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := builder{}
result := b.durationVal("test_field", &tc.input)

if tc.expectError {
require.Error(t, b.err)
require.Contains(t, b.err.Error(), tc.errorMsg)
require.Equal(t, time.Duration(0), result)
} else {
require.NoError(t, b.err)
switch tc.input {
case "-5s":
require.Equal(t, -5*time.Second, result)
case "0s":
require.Equal(t, time.Duration(0), result)
case "8760h":
require.Equal(t, 8760*time.Hour, result)
case "1.5s":
require.Equal(t, 1500*time.Millisecond, result)
case "1h30m45s":
expected := time.Hour + 30*time.Minute + 45*time.Second
require.Equal(t, expected, result)
}
}
})
}
}

func TestBuilder_DurationValWithDefaultMin_EdgeCases(t *testing.T) {
testCases := []struct {
name string
input *string
defaultVal time.Duration
minVal time.Duration
expectError bool
errorMsg string
}{
{
name: "nil input uses default",
input: nil,
defaultVal: 10 * time.Second,
minVal: 5 * time.Second,
expectError: false,
},
{
name: "negative duration below minimum",
input: strPtr("-10s"),
defaultVal: 10 * time.Second,
minVal: 0,
expectError: true,
errorMsg: "cannot be less than",
},
{
name: "zero duration below minimum",
input: strPtr("0s"),
defaultVal: 10 * time.Second,
minVal: 5 * time.Second,
expectError: true,
errorMsg: "cannot be less than",
},
{
name: "valid duration above minimum",
input: strPtr("30s"),
defaultVal: 10 * time.Second,
minVal: 5 * time.Second,
expectError: false,
},
{
name: "duration exactly at minimum",
input: strPtr("5s"),
defaultVal: 10 * time.Second,
minVal: 5 * time.Second,
expectError: false,
},
{
name: "unparseable duration with minimum check",
input: strPtr("invalid"),
defaultVal: 10 * time.Second,
minVal: 5 * time.Second,
expectError: true,
errorMsg: "invalid duration",
},
{
name: "very small duration below microsecond minimum",
input: strPtr("1ns"),
defaultVal: 0,
minVal: time.Microsecond,
expectError: true,
errorMsg: "cannot be less than",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := builder{}
result := b.durationValWithDefaultMin("test_field", tc.input, tc.defaultVal, tc.minVal)

if tc.expectError {
require.Error(t, b.err)
require.Contains(t, b.err.Error(), tc.errorMsg)
} else {
require.NoError(t, b.err)
if tc.input == nil {
require.Equal(t, tc.defaultVal, result)
} else if *tc.input == "30s" {
require.Equal(t, 30*time.Second, result)
} else if *tc.input == "5s" {
require.Equal(t, 5*time.Second, result)
}
}
})
}
}

func TestBuilder_DurationValWithDefault_EdgeCases(t *testing.T) {
testCases := []struct {
name string
input *string
defaultVal time.Duration
expectError bool
expected time.Duration
}{
{
name: "nil input returns default",
input: nil,
defaultVal: 15 * time.Minute,
expectError: false,
expected: 15 * time.Minute,
},
{
name: "empty string input",
input: strPtr(""),
defaultVal: 15 * time.Minute,
expectError: true,
expected: time.Duration(0),
},
{
name: "negative duration with default",
input: strPtr("-1h"),
defaultVal: 15 * time.Minute,
expectError: false,
expected: -time.Hour,
},
{
name: "zero duration overrides default",
input: strPtr("0s"),
defaultVal: 15 * time.Minute,
expectError: false,
expected: time.Duration(0),
},
{
name: "valid duration overrides default",
input: strPtr("2h30m"),
defaultVal: 15 * time.Minute,
expectError: false,
expected: 2*time.Hour + 30*time.Minute,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := builder{}
result := b.durationValWithDefault("test_field", tc.input, tc.defaultVal)

if tc.expectError {
require.Error(t, b.err)
require.Equal(t, time.Duration(0), result)
} else {
require.NoError(t, b.err)
require.Equal(t, tc.expected, result)
}
})
}
}

func TestBuilder_ServiceVal_with_Check(t *testing.T) {
b := builder{}
svc := b.serviceVal(&ServiceDefinition{
Expand Down
4 changes: 4 additions & 0 deletions agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,10 @@ type HTTPConfig struct {
ResponseHeaders map[string]string `mapstructure:"response_headers"`
UseCache *bool `mapstructure:"use_cache"`
MaxHeaderBytes *int `mapstructure:"max_header_bytes"`
ReadTimeout *string `mapstructure:"read_timeout"`
ReadHeaderTimeout *string `mapstructure:"read_header_timeout"`
WriteTimeout *string `mapstructure:"write_timeout"`
IdleTimeout *string `mapstructure:"idle_timeout"`
}

type Performance struct {
Expand Down
32 changes: 32 additions & 0 deletions agent/config/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,38 @@ type RuntimeConfig struct {
// If zero, or negative, http.DefaultMaxHeaderBytes is used.
HTTPMaxHeaderBytes int

// HTTPReadTimeout is the maximum duration for reading the entire request,
// including the body. This timeout prevents slow request body attacks.
// A zero or negative value means there will be no timeout.
//
// Default: 30s, Minimum: 1s
// hcl: http_config { read_timeout = "30s" }
HTTPReadTimeout time.Duration

// HTTPReadHeaderTimeout is the amount of time allowed to read request headers.
// The connection's read deadline is reset after reading the headers and the
// Handler can decide what is considered too slow for the body.
// This timeout prevents slowloris attacks on header parsing.
//
// Default: 10s, Minimum: 1s
// hcl: http_config { read_header_timeout = "10s" }
HTTPReadHeaderTimeout time.Duration

// HTTPWriteTimeout is the maximum duration before timing out writes of the response.
// This timeout prevents slow response drain attacks.
// A zero or negative value means there will be no timeout.
//
// Default: 30s, Minimum: 1s
// hcl: http_config { write_timeout = "30s" }
HTTPWriteTimeout time.Duration

// HTTPIdleTimeout is the maximum amount of time to wait for the next request
// when keep-alives are enabled. This timeout prevents connection exhaustion attacks.
//
// Default: 120s, Minimum: 10s
// hcl: http_config { idle_timeout = "120s" }
HTTPIdleTimeout time.Duration

// HTTPSHandshakeTimeout is the time allowed for HTTPS client to complete the
// TLS handshake and send first bytes of the request.
//
Expand Down
Loading
Loading