fix(api): set read/idle timeouts on the felis-api listeners
The three felis-api http.Servers (internal, external, https) were built with only Addr and Handler, leaving ReadHeaderTimeout, IdleTimeout, and ReadTimeout at zero. A zero ReadHeaderTimeout is a Slowloris hole — a client trickling header bytes pins a connection indefinitely — and a zero IdleTimeout lets kept-alive connections accumulate (gosec G112). Route all three listeners through a newAPIServer factory that sets a 10s ReadHeaderTimeout and a 120s IdleTimeout. WriteTimeout and ReadTimeout are left unset on purpose: the external and https faces stream Server-Sent Events (console / build logs) for the lifetime of a client attachment, and a WriteTimeout would sever a healthy long-lived stream. Slowloris is closed by ReadHeaderTimeout, which bounds only the header phase.
This commit is contained in:
2 files changed
+58
-3
No files matched your search
+30
-3
@@ -198,15 +198,15 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain)
|
externalHandler := panel.Handler(a.ExternalHandler(), cfg.Server.RootDomain)
|
||||||
internalSrv := &http.Server{Addr: *internalAddr, Handler: a.InternalHandler()}
|
internalSrv := newAPIServer(*internalAddr, a.InternalHandler())
|
||||||
externalSrv := &http.Server{Addr: cfg.Server.Listen, Handler: externalHandler}
|
externalSrv := newAPIServer(cfg.Server.Listen, externalHandler)
|
||||||
|
|
||||||
errc := make(chan error, 3)
|
errc := make(chan error, 3)
|
||||||
go func() { errc <- internalSrv.ListenAndServe() }()
|
go func() { errc <- internalSrv.ListenAndServe() }()
|
||||||
go func() { errc <- externalSrv.ListenAndServe() }()
|
go func() { errc <- externalSrv.ListenAndServe() }()
|
||||||
var httpsSrv *http.Server
|
var httpsSrv *http.Server
|
||||||
if *httpsAddr != "" {
|
if *httpsAddr != "" {
|
||||||
httpsSrv = &http.Server{Addr: *httpsAddr, Handler: externalHandler}
|
httpsSrv = newAPIServer(*httpsAddr, externalHandler)
|
||||||
go func() { errc <- httpsSrv.ListenAndServeTLS(*tlsCert, *tlsKey) }()
|
go func() { errc <- httpsSrv.ListenAndServeTLS(*tlsCert, *tlsKey) }()
|
||||||
}
|
}
|
||||||
if httpsSrv != nil {
|
if httpsSrv != nil {
|
||||||
@@ -239,6 +239,33 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// apiReadHeaderTimeout caps how long a client may take to send its request
|
||||||
|
// headers, defeating a Slowloris that trickles a header line forever to pin a
|
||||||
|
// connection open. It bounds only the header phase, so it is safe on every face —
|
||||||
|
// including the SSE streaming one, whose response, not its request, is long-lived.
|
||||||
|
apiReadHeaderTimeout = 10 * time.Second
|
||||||
|
// apiIdleTimeout caps how long a kept-alive connection may sit idle between
|
||||||
|
// requests before the server closes it, bounding idle-connection exhaustion.
|
||||||
|
apiIdleTimeout = 120 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// newAPIServer builds an http.Server with hardened header/idle timeouts (gosec
|
||||||
|
// G112) shared by all three felis-api listeners (internal, external, https).
|
||||||
|
// WriteTimeout and ReadTimeout are deliberately LEFT UNSET: the external and https
|
||||||
|
// faces stream Server-Sent Events (console / build logs, spec §8) for the lifetime
|
||||||
|
// of a client's attachment, and a WriteTimeout would sever a healthy long-lived
|
||||||
|
// stream mid-flight. Slowloris is closed by ReadHeaderTimeout, which bounds only the
|
||||||
|
// header phase and never touches the response.
|
||||||
|
func newAPIServer(addr string, handler http.Handler) *http.Server {
|
||||||
|
return &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: handler,
|
||||||
|
ReadHeaderTimeout: apiReadHeaderTimeout,
|
||||||
|
IdleTimeout: apiIdleTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// buildConfig projects felis.toml onto the build subsystem config (spec §16,
|
// buildConfig projects felis.toml onto the build subsystem config (spec §16,
|
||||||
// §24). Unset fields fall back to the build package's hardened defaults
|
// §24). Unset fields fall back to the build package's hardened defaults
|
||||||
// (felis-build namespace + weak SA, 30m deadline, resource limits).
|
// (felis-build namespace + weak SA, 30m deadline, resource limits).
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNewAPIServerSetsHardenedTimeouts pins the gosec-G112 hardening on every
|
||||||
|
// felis-api listener: the shared factory must bound the header and idle phases
|
||||||
|
// (Slowloris + idle-connection exhaustion) while leaving WriteTimeout UNSET, because
|
||||||
|
// the external and https faces stream Server-Sent Events for the life of a client's
|
||||||
|
// console/build-log attachment and a WriteTimeout would sever a healthy long stream.
|
||||||
|
func TestNewAPIServerSetsHardenedTimeouts(t *testing.T) {
|
||||||
|
srv := newAPIServer(":0", http.NewServeMux())
|
||||||
|
|
||||||
|
if srv.ReadHeaderTimeout <= 0 {
|
||||||
|
t.Errorf("ReadHeaderTimeout = %v, want a positive Slowloris bound", srv.ReadHeaderTimeout)
|
||||||
|
}
|
||||||
|
if srv.IdleTimeout <= 0 {
|
||||||
|
t.Errorf("IdleTimeout = %v, want a positive idle-connection bound", srv.IdleTimeout)
|
||||||
|
}
|
||||||
|
if srv.WriteTimeout != 0 {
|
||||||
|
t.Errorf("WriteTimeout = %v, want 0 (unset) so long-lived SSE streams are not severed", srv.WriteTimeout)
|
||||||
|
}
|
||||||
|
if srv.ReadTimeout != 0 {
|
||||||
|
t.Errorf("ReadTimeout = %v, want 0 (unset) so a slow SSE attach is not capped", srv.ReadTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user