From 4f2b340105fa7a972c75cc9b5db14a67e346cd64 Mon Sep 17 00:00:00 2001 From: "STeve (Xin) Huang" Date: Wed, 21 Aug 2024 13:39:46 -0400 Subject: [PATCH] [buddy] Truncate AssumeRole session name to API limits (#45202) (#45658) * 44833 Truncate AssumeRole session name to API limits * Link reference * Hash the username and add audit log * review comments * remove audit --------- Co-authored-by: Joao Ubaldo <3534309+joaoubaldo@users.noreply.github.com> --- lib/srv/app/cloud.go | 2 +- lib/utils/aws/aws.go | 38 ++++++++++++++++++++++++++++++++++-- lib/utils/aws/aws_test.go | 30 ++++++++++++++++++++++++++++ lib/utils/aws/credentials.go | 2 +- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/lib/srv/app/cloud.go b/lib/srv/app/cloud.go index 1757188d3fe36..3675bc14910ed 100644 --- a/lib/srv/app/cloud.go +++ b/lib/srv/app/cloud.go @@ -191,7 +191,7 @@ func (c *cloud) getAWSSigninToken(ctx context.Context, req *AWSSigninRequest, en options = append(options, func(creds *stscreds.AssumeRoleProvider) { // Setting role session name to Teleport username will allow to // associate CloudTrail events with the Teleport user. - creds.RoleSessionName = req.Identity.Username + creds.RoleSessionName = awsutils.MaybeHashRoleSessionName(req.Identity.Username) // Setting web console session duration through AssumeRole call for AWS // sessions with temporary credentials. diff --git a/lib/utils/aws/aws.go b/lib/utils/aws/aws.go index 950a572946d10..b79afbdd23f0a 100644 --- a/lib/utils/aws/aws.go +++ b/lib/utils/aws/aws.go @@ -21,7 +21,10 @@ package aws import ( "bytes" "context" + "crypto/sha1" + "encoding/hex" "fmt" + "log/slog" "net/http" "net/textproto" "sort" @@ -33,7 +36,6 @@ import ( v4 "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/aws/aws-sdk-go/service/iam" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" apievents "github.com/gravitational/teleport/api/types/events" apiawsutils "github.com/gravitational/teleport/api/utils/aws" @@ -68,6 +70,11 @@ const ( AmzJSON1_0 = "application/x-amz-json-1.0" // AmzJSON1_1 is an AWS Content-Type header that indicates the media type is JSON. AmzJSON1_1 = "application/x-amz-json-1.1" + + // MaxRoleSessionNameLength is the maximum length of the role session name + // used by the AssumeRole call. + // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html + MaxRoleSessionNameLength = 64 ) // SigV4 contains parsed content of the AWS Authorization header. @@ -249,7 +256,7 @@ func FilterAWSRoles(arns []string, accountID string) (result Roles) { for _, roleARN := range arns { parsed, err := ParseRoleARN(roleARN) if err != nil { - logrus.Warnf("skipping invalid AWS role ARN: %v", err) + slog.WarnContext(context.Background(), "Skipping invalid AWS role ARN.", "error", err) continue } if accountID != "" && parsed.AccountID != accountID { @@ -484,3 +491,30 @@ func iamResourceARN(partition, accountID, resourceType, resourceName string) str Resource: fmt.Sprintf("%s/%s", resourceType, resourceName), }.String() } + +// MaybeHashRoleSessionName truncates the role session name and adds a hash +// when the original role session name is greater than AWS character limit +// (64). +func MaybeHashRoleSessionName(roleSessionName string) (ret string) { + if len(roleSessionName) <= MaxRoleSessionNameLength { + return roleSessionName + } + + const hashLen = 16 + hash := sha1.New() + hash.Write([]byte(roleSessionName)) + hex := hex.EncodeToString(hash.Sum(nil))[:hashLen] + + // "1" for the delimiter. + keepPrefixIndex := MaxRoleSessionNameLength - len(hex) - 1 + + // Sanity check. This should never happen since hash length and + // MaxRoleSessionNameLength are both constant. + if keepPrefixIndex < 0 { + keepPrefixIndex = 0 + } + + ret = fmt.Sprintf("%s-%s", roleSessionName[:keepPrefixIndex], hex) + slog.DebugContext(context.Background(), "AWS role session name is too long. Using a hash instead.", "hashed", ret, "original", roleSessionName) + return ret +} diff --git a/lib/utils/aws/aws_test.go b/lib/utils/aws/aws_test.go index 61ca9dfc48c4f..e42da143252fa 100644 --- a/lib/utils/aws/aws_test.go +++ b/lib/utils/aws/aws_test.go @@ -493,3 +493,33 @@ func TestResourceARN(t *testing.T) { }) } } + +func TestMaybeHashRoleSessionName(t *testing.T) { + for _, tt := range []struct { + name string + role string + expected string + }{ + { + name: "role session name not hashed, less than 64 characters", + role: "MyRole", + expected: "MyRole", + }, + { + name: "role session name not hashed, exactly 64 characters", + role: "Role123456789012345678901234567890123456789012345678901234567890", + expected: "Role123456789012345678901234567890123456789012345678901234567890", + }, + { + name: "role session name hashed, longer than 64 characters", + role: "remote-raimundo.oliveira@abigcompany.com-teleport.abigcompany.com", + expected: "remote-raimundo.oliveira@abigcompany.com-telepo-8fe1f87e599b043e", + }, + } { + t.Run(tt.name, func(t *testing.T) { + actual := MaybeHashRoleSessionName(tt.role) + require.Equal(t, tt.expected, actual) + require.LessOrEqual(t, len(actual), MaxRoleSessionNameLength) + }) + } +} diff --git a/lib/utils/aws/credentials.go b/lib/utils/aws/credentials.go index 4c415c98448f1..257d6606d42a9 100644 --- a/lib/utils/aws/credentials.go +++ b/lib/utils/aws/credentials.go @@ -74,7 +74,7 @@ func (g *credentialsGetter) Get(_ context.Context, request GetCredentialsRequest logrus.Debugf("Creating STS session %q for %q.", request.SessionName, request.RoleARN) return stscreds.NewCredentials(request.Provider, request.RoleARN, func(cred *stscreds.AssumeRoleProvider) { - cred.RoleSessionName = request.SessionName + cred.RoleSessionName = MaybeHashRoleSessionName(request.SessionName) cred.Expiry.SetExpiration(request.Expiry, 0) if request.ExternalID != "" {