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.
This commit is contained in:
Julian Sommer
2026-09-28 10:49:28 +10:00
parent 9bd1860622
commit 94488dc399
4 changed files with 38 additions and 2 deletions
+2 -2
View File
@@ -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
+4
View File
@@ -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=
+6
View File
@@ -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.
+26
View File
@@ -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) {