Skip to content

harnessd: HTTP timeouts + MaxBytesReader middleware (audit #12+#14) #652

Description

@dennisonbertram

Intent

Harden the harnessd HTTP server against slowloris/body-bomb DoS by setting full timeouts and MaxHeaderBytes, wrapping non-streaming handlers in http.TimeoutHandler, and adding http.MaxBytesReader to every request body.

Plan reference

Source finding

harnessd http.Server sets only ReadHeaderTimeout (cmd/harnessd/runtime_container.go:159-163). No ReadTimeout, IdleTimeout, MaxHeaderBytes, or http.TimeoutHandler on non-streaming handlers. Slowloris body reads and accumulated idle keep-alive conns pin FDs unbounded. Also: no http.MaxBytesReader on any request body — every json.NewDecoder(r.Body).Decode site in internal/server/; a 50 GB body OOMs the daemon.

Scope

  • Allowed files (test): internal/server/http_server_hardening_test.go (new), internal/server/http_test.go (extend)
  • Allowed files (impl): cmd/harnessd/runtime_container.go, internal/server/http.go, internal/server/auth.go
  • Dependencies: none
  • Complexity: low — Risk: low (must ensure SSE/streaming handlers are NOT wrapped by TimeoutHandler)

Acceptance criteria

Use slice T13 in the plan as detailed acceptance criteria. Summary:

  • Failing tests written first, compile and fail on main.
  • http.Server is constructed with non-zero ReadTimeout, IdleTimeout, MaxHeaderBytes; non-streaming handlers are wrapped in http.TimeoutHandler (introspect via reflect on the chain, or assert a body bigger than the limit triggers http.MaxBytesError).
  • Sending a 5 MiB JSON body to POST /v1/runs returns 413 (or the MaxBytesReader status) and does not consume more than ~1.1 MiB of memory.
  • An SSE handler that respects r.Context().Done() is NOT wrapped by TimeoutHandler (assert a long-lived SSE stream survives a 60s TimeoutHandler boundary).
  • Impl: set ReadTimeout: 60s, IdleTimeout: 120s, MaxHeaderBytes: 1 << 20 on the http.Server. Add an authAndLimit middleware that wraps r.Body in http.MaxBytesReader(w, r.Body, maxBodyBytes) (default 4 MiB for replay, 1 MiB otherwise; per-route override). Wrap non-streaming handlers (everything whose handler name does NOT contain Events/Stream/Wait) in http.TimeoutHandler(h, 30*time.Second, {"error":{"code":"timeout"}}). Streaming handlers keep their own r.Context().Done() discipline.
  • go test ./internal/server/... ./cmd/harnessd/... -race green; ./scripts/test-regression.sh green.

Guardrails

  • Strict TDD; failing regression test must compile and fail on main first.
  • Worktree flow; own branch, merge via verify-and-merge.sh.
  • Update .context/harness-reliability/tracker.md on pick-up and merge.
  • Deterministic tests only — httptest.Server, no real FD exhaustion.
  • Append engineering-log entry on merge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreliabilityReliability and resilience concernssecuritySecurity scanning and auditing skillssmallSmall effort: < 4 hourswell-specifiedIssue is clear, scoped, and ready to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions