diff --git a/pkg/server/server.go b/pkg/server/server.go index 045f948c2..f7fdec0df 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -333,9 +333,9 @@ func (h *handler) authenticateEndpoint(w http.ResponseWriter, req *http.Request) // if the token is invalid, reject with a 403 identity, err := h.verifier.Verify(tokenReview.Spec.Token) if err != nil { - if _, ok := err.(token.STSThrottling); ok { + if throttleErr, ok := err.(token.STSThrottling); ok { metrics.Get().Latency.WithLabelValues(metrics.STSThrottling).Observe(duration(start)) - log.WithError(err).Warn("access denied") + log.WithError(throttleErr).WithField("accountID", throttleErr.AccountID()).Warn("access denied") w.WriteHeader(http.StatusTooManyRequests) w.Write(tokenReviewDenyJSON) return diff --git a/pkg/token/akid.go b/pkg/token/akid.go new file mode 100644 index 000000000..beac55d3b --- /dev/null +++ b/pkg/token/akid.go @@ -0,0 +1,39 @@ +package token + +import ( + "encoding/base32" + "encoding/hex" + "fmt" +) + +// accountForAKID is a best-effort method to extract the account ID from an AKID for +// logging on throttled requests. This should not be called on untrusted input (i.e. +// AKID from the request before validating the request from STS). +// +// This is not foolproof, but avoids an `sts:GetAccessKeyInfo` call per AKID. +// adapted from https://hackingthe.cloud/aws/enumeration/get-account-id-from-keys/ +func accountForAKID(akid string) string { + if len(akid) < 20 { + // too short + return "" + } + decoded, err := base32.StdEncoding.DecodeString(akid[4:]) + if err != nil { + // decoding error + return "" + } + y := decoded[:6] + z := uint64(0) + for i := 0; i < len(y); i++ { + z = (z << 8) | uint64(y[i]) + } + // this mask bytestring is always valid + maskBytes, _ := hex.DecodeString("7fffffffff80") + mask := uint64(0) + for i := 0; i < len(maskBytes); i++ { + mask = (mask << 8) | uint64(maskBytes[i]) + } + // Apply mask and shift right by 7 bits + e := (z & mask) >> 7 + return fmt.Sprintf("%012d", e) +} diff --git a/pkg/token/akid_test.go b/pkg/token/akid_test.go new file mode 100644 index 000000000..26861914d --- /dev/null +++ b/pkg/token/akid_test.go @@ -0,0 +1,44 @@ +package token + +import ( + "testing" +) + +func TestAccountForAKID(t *testing.T) { + testcases := []struct { + name string + akid string + expected string + wantErr error + }{ + { + name: "empty akid", + akid: "", + expected: "", + }, + { + name: "akid with account", + akid: "ASIAR2TG44V5PDTTBZRR", + expected: "125843596666", + }, + { + name: "account starting with a 0", + akid: "ASIAQNZGKIQY56JQ7WML", + expected: "029608264753", + }, + { + name: "non base32 encoded akid", + akid: "ASIAc29tZXRoaW5nCg==", + expected: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + actual := accountForAKID(tc.akid) + if actual != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, actual) + } + }) + } +} diff --git a/pkg/token/token.go b/pkg/token/token.go index 716a8cb12..721e8302a 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -142,16 +142,21 @@ func NewSTSError(m string) STSError { // STSThrottling is returned when there was STS Throttling. type STSThrottling struct { - message string + message string + accountID string +} + +func (e STSThrottling) AccountID() string { + return e.accountID } func (e STSThrottling) Error() string { - return "sts getCallerIdentity was throttled: " + e.message + return "sts getCallerIdentity for account " + e.accountID + " was throttled: " + e.message } // NewSTSError creates a error of type STS. -func NewSTSThrottling(m string) STSThrottling { - return STSThrottling{message: m} +func NewSTSThrottling(m, accountID string) STSThrottling { + return STSThrottling{message: m, accountID: accountID} } var parameterWhitelist = map[string]bool{ @@ -586,8 +591,9 @@ func (v tokenVerifier) Verify(token string) (*Identity, error) { // refer to https://docs.aws.amazon.com/STS/latest/APIReference/CommonErrors.html and log // response body for STS Throttling is {"Error":{"Code":"Throttling","Message":"Rate exceeded","Type":"Sender"},"RequestId":"xxx"} if strings.Contains(responseStr, "Throttling") { + // STS validated the request, but throttled it: we can trust that enough to report the accountID metrics.Get().StsThrottling.Inc() - return nil, NewSTSThrottling(responseStr) + return nil, NewSTSThrottling(responseStr, accountForAKID(accessKeyID)) } return nil, NewSTSError(fmt.Sprintf("error from AWS (expected 200, got %d). Body: %s", response.StatusCode, responseStr)) } diff --git a/pkg/token/token_test.go b/pkg/token/token_test.go index a8e997c86..d5eb1dd11 100644 --- a/pkg/token/token_test.go +++ b/pkg/token/token_test.go @@ -209,7 +209,7 @@ func TestVerifyTokenPreSTSValidations(t *testing.T) { func TestVerifyHTTPThrottling(t *testing.T) { testVerifier := newVerifier("aws", 400, "{\\\"Error\\\":{\\\"Code\\\":\\\"Throttling\\\",\\\"Message\\\":\\\"Rate exceeded\\\",\\\"Type\\\":\\\"Sender\\\"},\\\"RequestId\\\":\\\"8c2d3520-24e1-4d5c-ac55-7e226335f447\\\"}", nil) _, err := testVerifier.Verify(validToken) - errorContains(t, err, "sts getCallerIdentity was throttled") + errorContains(t, err, "sts getCallerIdentity for account 073224499664 was throttled") assertSTSThrottling(t, err) }