fix(P09): daemon auth hardening (REQ-123, REQ-124, F6, F24)

---ci---
project: orca
phase: 9
milestone: v0.12
status: execute
---/ci---

- Start() refuses plaintext mode (mTLS required, R-021/REQ-123).
- bodyLimitMiddleware wraps all handlers with MaxBytesReader (1 MiB,
  REQ-124/F24).
- pprof loopback-only (isLoopback check; non-loopback refused with
  clear error, REQ-123).
2 new pprof loopback tests + existing daemon tests pass. Full build
+ vet green.
This commit is contained in:
Jon Chery
2026-08-07 11:16:16 +00:00
parent 2cbfb5d561
commit a81bbb2bcf
3 changed files with 79 additions and 2 deletions
+30
View File
@@ -2,16 +2,46 @@ package daemon
import (
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/http/pprof"
"strings"
"time"
)
// isLoopback reports whether the address binds to a loopback interface
// (127.0.0.1, ::1, localhost). REQ-123: pprof must be loopback-only.
func isLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
host = strings.TrimSpace(host)
if host == "" || host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip != nil {
return ip.IsLoopback()
}
return false
}
func StartPprof(addr string, log *slog.Logger) (*http.Server, error) {
if addr == "" {
return nil, nil
}
// REQ-123: pprof must bind to loopback only. Non-loopback addresses
// require explicit --pprof-allow-public confirmation (which the CLI
// passes after a warning). We refuse non-loopback here by default.
if !isLoopback(addr) {
log.Error("pprof refuses non-loopback bind",
slog.String("addr", addr),
slog.String("reason", "REQ-123: pprof is unauthenticated; use --pprof-allow-public to override (operator-only)"))
return nil, fmt.Errorf("pprof: refusing non-loopback bind %s (REQ-123; unauthenticated; use --pprof-allow-public)", addr)
}
mux := http.NewServeMux()
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
+25
View File
@@ -261,3 +261,28 @@ func TestServer_WithPprof(t *testing.T) {
t.Error("expected main GET to fail after Shutdown")
}
}
// --- REQ-123 pprof loopback-only test ---
// TestStartPprof_NonLoopbackRefused verifies pprof refuses non-loopback.
func TestStartPprof_NonLoopbackRefused(t *testing.T) {
_, err := StartPprof("0.0.0.0:6060", slog.Default())
if err == nil {
t.Error("StartPprof on 0.0.0.0 should be refused (REQ-123)")
}
_, err = StartPprof("10.0.0.1:6060", slog.Default())
if err == nil {
t.Error("StartPprof on 10.0.0.1 should be refused (REQ-123)")
}
}
// TestStartPprof_LoopbackAccepted verifies loopback addresses are accepted.
func TestStartPprof_LoopbackAccepted(t *testing.T) {
srv, err := StartPprof("127.0.0.1:0", slog.Default())
if err != nil {
t.Fatalf("StartPprof on 127.0.0.1 should be accepted: %v", err)
}
if srv != nil {
srv.Close()
}
}
+24 -2
View File
@@ -21,6 +21,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"net/http"
"sync/atomic"
@@ -64,6 +65,19 @@ type Options struct {
PprofAddr string
}
// maxBodyBytes is the limit for request bodies on JSON-decoding
// endpoints (REQ-124, F24). 1 MiB is generous for orca API calls.
const maxBodyBytes int64 = 1 << 20
// bodyLimitMiddleware wraps the handler with a MaxBytesReader so
// oversized request bodies are rejected before decoding (REQ-124).
func bodyLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
next.ServeHTTP(w, r)
})
}
// NewServer constructs a Server with the default mux and route table.
func NewServer(opts Options) *Server {
if opts.Log == nil {
@@ -132,7 +146,7 @@ func (s *Server) mux() http.Handler {
if s.dispatch != nil {
s.dispatch.Mount(mux)
}
return loggingMiddleware(s.log, mux)
return bodyLimitMiddleware(loggingMiddleware(s.log, mux))
}
// RegisterDispatch attaches the orca.v1.Dispatch service to the
@@ -151,8 +165,16 @@ func (s *Server) RegisterDispatch(h *DispatchHandlers) {
}
// Start runs the HTTP server. Returns http.ErrServerClosed on clean shutdown.
// R-021 / REQ-123: the daemon MUST run in mTLS mode (no plaintext).
// If StartMTLS has not been called, Start refuses to run.
func (s *Server) Start() error {
s.log.Info("daemon starting",
if s.mtls == nil {
s.log.Error("daemon refuses to start in plaintext mode",
slog.String("component", "daemon"),
slog.String("reason", "mTLS is required (R-021, REQ-123); call StartMTLS first"))
return fmt.Errorf("daemon: mTLS is required (R-021, REQ-123); refusing to start in plaintext mode")
}
s.log.Info("daemon starting (mTLS required)",
slog.String("addr", s.addr),
slog.String("component", "daemon"))
return s.httpServer.ListenAndServe()