From 94488dc3991607bbf5cd263c16648170c5df43cb Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:49:28 +1000 Subject: [PATCH] fix(gateway): don't count body-read failures toward the auth lockout A request whose body cannot be read to hash failed through the catch-all branch, logging a malformed envelope, returning IncompleteSignature and recording an anonymous lockout strike, so ten dropped connections locked out an IP. Match sigv4.ErrReadingBody and answer InternalError without recording a failure. Bumps bluebottle for the new sentinel. --- go.mod | 4 ++-- go.sum | 4 ++++ spinifex/gateway/auth.go | 6 ++++++ spinifex/gateway/auth_test.go | 26 ++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 77c2734d9..6b361639b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/klauspost/cpuid/v2 v2.4.0 github.com/miekg/dns v1.1.73 github.com/minio/highwayhash v1.0.4 - github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33 + github.com/mulgadc/bluebottle v1.20.1-0.20260928004821-163f43196afd github.com/mulgadc/northstar v1.20.1-0.20260917032839-357bbc94201d github.com/mulgadc/predastore v1.20.1-0.20260918222809-019dd349cf1f github.com/mulgadc/viperblock v1.20.0 @@ -74,7 +74,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.38.0 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.51.0 // indirect - github.com/aws/smithy-go v1.28.1 // indirect + github.com/aws/smithy-go v1.28.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect diff --git a/go.sum b/go.sum index fd276baea..e8a185f1a 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.51.0 h1:Zpnqa6XtrNzXZnwbdCqHOXpXhMsa github.com/aws/aws-sdk-go-v2/service/sts v1.51.0/go.mod h1:/8JRcdTt//hG0Q4BTmGbuOplT7ABe+5rdtqUHqXvYIM= github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.28.2 h1:myhcykQcatTul2B/zITjDk203G7t0awUAs1hVry5Bvg= +github.com/aws/smithy-go v1.28.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -368,6 +370,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33 h1:Rw3mnRF2YrhVZgHpCsOi6La8cOu/V3xnhB000iWcdr8= github.com/mulgadc/bluebottle v1.20.1-0.20260916040751-5a5ca6f0fb33/go.mod h1:4rjLL3B0xZBPmxacaXPkw6sPOZZgyuCDBFmPSOC81yE= +github.com/mulgadc/bluebottle v1.20.1-0.20260928004821-163f43196afd h1:Ffa6OIUqHLUNn+NM4Ij4kGOLm7DEeicM16+Sn/Kulm8= +github.com/mulgadc/bluebottle v1.20.1-0.20260928004821-163f43196afd/go.mod h1:LH0MuQcFxet0dDEISlZsuULIYj592eCN2Fv39A6EH5o= github.com/mulgadc/northstar v1.20.1-0.20260917032839-357bbc94201d h1:xYdO+KXrn+/aBLva3yuRly9MnKrSNSouGYleITgkoXE= github.com/mulgadc/northstar v1.20.1-0.20260917032839-357bbc94201d/go.mod h1:3GB49VUE9rA2idp88se5jAMv3ql1Oq9kIcT29WAZwDU= github.com/mulgadc/predastore v1.20.1-0.20260918222809-019dd349cf1f h1:P7echVc7fCYiU2CS/L0dLAf0i/6eWcjDqqf7AnFKpDg= diff --git a/spinifex/gateway/auth.go b/spinifex/gateway/auth.go index 0e633b1bd..051944b4d 100644 --- a/spinifex/gateway/auth.go +++ b/spinifex/gateway/auth.go @@ -57,6 +57,12 @@ func (gw *GatewayConfig) SigV4AuthMiddleware() func(http.Handler) http.Handler { gw.writeSigV4Error(w, r, awserrors.ErrorMissingAuthenticationToken, "") case errors.Is(err, sigv4.ErrPayloadTooLarge): gw.writeSigV4Error(w, r, awserrors.ErrorRequestEntityTooLarge, "") + case errors.Is(err, sigv4.ErrReadingBody): + // A dropped or stalled connection, not an auth attempt: counting it + // would lock out an IP for its network trouble. + slog.Warn("Auth aborted: request body read failed", + "sourceIP", clientIP, "err", err) + gw.writeSigV4Error(w, r, awserrors.ErrorInternalError, "") case errors.Is(err, sigv4.ErrRequestTimeTooSkewed): // Skew/replay: AWS returns this as SignatureDoesNotMatch, which is // indistinguishable on the wire from a canonicalisation mismatch. diff --git a/spinifex/gateway/auth_test.go b/spinifex/gateway/auth_test.go index 1abbe6519..23c16b1af 100644 --- a/spinifex/gateway/auth_test.go +++ b/spinifex/gateway/auth_test.go @@ -14,6 +14,7 @@ import ( "strings" "sync/atomic" "testing" + "testing/iotest" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -544,6 +545,31 @@ func TestSigV4Auth_RequestBodyTooLarge(t *testing.T) { } } +// A body the gateway cannot read is a transport failure: it answers InternalError +// and leaves the lockout alone, so a flaky link cannot lock out its own IP. +func TestSigV4Auth_BodyReadFailureDoesNotCountTowardLockout(t *testing.T) { + handler := setupTestApp(testAccessKey, testSecretKey) + + for range maxFailures + 1 { + req := httptest.NewRequest(http.MethodPost, "/", iotest.ErrReader(io.ErrUnexpectedEOF)) + req.Host = "localhost:9999" + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + signTestRequest(t, req, []byte("Action=DescribeInstances"), testAccessKey, testSecretKey) + + resp := doRequest(handler, req) + body, _ := io.ReadAll(resp.Body) + require.Equal(t, http.StatusInternalServerError, resp.StatusCode, string(body)) + require.Contains(t, string(body), awserrors.ErrorInternalError) + } + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Host = "localhost:9999" + signTestRequest(t, req, nil, testAccessKey, testSecretKey) + + resp := doRequest(handler, req) + assert.Equal(t, http.StatusOK, resp.StatusCode, "body-read failures must not lock out the IP") +} + // --- Clock Skew / Replay Protection Tests --- func TestSigV4Auth_ExpiredTimestamp(t *testing.T) {