From a9be7dbfbb9d1e36a77148d93f8a4446270bc0a1 Mon Sep 17 00:00:00 2001 From: Gavin Frazar Date: Wed, 29 Jan 2025 13:16:56 -0800 Subject: [PATCH] Migrate AWS STS client to AWS SDK v2 (#51573) --- lib/cloud/clients.go | 20 ----- lib/cloud/mocks/aws.go | 2 +- lib/cloud/mocks/aws_sts.go | 17 ++++- lib/srv/app/aws/handler_test.go | 6 ++ lib/srv/db/cloud/iam.go | 6 +- lib/srv/db/cloud/iam_test.go | 73 ++++++++----------- lib/srv/db/cloud/meta.go | 11 +++ lib/srv/db/cloud/meta_test.go | 6 ++ lib/srv/db/cloud/resource_checker.go | 2 +- .../db/cloud/resource_checker_credentials.go | 30 +++++--- lib/srv/db/common/auth.go | 16 +++- lib/srv/db/common/auth_test.go | 59 +++++++++------ .../discovery/fetchers/aws-sync/aws-sync.go | 50 +++++-------- lib/srv/discovery/fetchers/aws-sync/ec2.go | 4 +- lib/srv/discovery/fetchers/aws-sync/eks.go | 2 +- lib/srv/discovery/fetchers/aws-sync/groups.go | 6 +- lib/srv/discovery/fetchers/aws-sync/idp.go | 4 +- .../discovery/fetchers/aws-sync/policies.go | 2 +- lib/srv/discovery/fetchers/aws-sync/rds.go | 2 +- .../discovery/fetchers/aws-sync/rds_test.go | 6 ++ lib/srv/discovery/fetchers/aws-sync/roles.go | 6 +- lib/srv/discovery/fetchers/aws-sync/s3.go | 4 +- lib/srv/discovery/fetchers/aws-sync/users.go | 8 +- 23 files changed, 185 insertions(+), 157 deletions(-) diff --git a/lib/cloud/clients.go b/lib/cloud/clients.go index a9101dd1db9..0c8fe0306a6 100644 --- a/lib/cloud/clients.go +++ b/lib/cloud/clients.go @@ -91,8 +91,6 @@ type GCPClients interface { type AWSClients interface { // GetAWSSession returns AWS session for the specified region and any role(s). GetAWSSession(ctx context.Context, region string, opts ...AWSOptionsFn) (*awssession.Session, error) - // GetAWSSTSClient returns AWS STS client for the specified region. - GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error) } // AzureClients is an interface for Azure-specific API clients @@ -468,15 +466,6 @@ func (c *cloudClients) GetAWSSession(ctx context.Context, region string, opts .. return c.getAWSSessionForRole(ctx, region, options) } -// GetAWSSTSClient returns AWS STS client for the specified region. -func (c *cloudClients) GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error) { - session, err := c.GetAWSSession(ctx, region, opts...) - if err != nil { - return nil, trace.Wrap(err) - } - return sts.New(session), nil -} - // GetGCPIAMClient returns GCP IAM client. func (c *cloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) { c.mtx.RLock() @@ -964,15 +953,6 @@ func (c *TestCloudClients) getAWSSessionForRegion(region string) (*awssession.Se }) } -// GetAWSSTSClient returns AWS STS client for the specified region. -func (c *TestCloudClients) GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error) { - _, err := c.GetAWSSession(ctx, region, opts...) - if err != nil { - return nil, trace.Wrap(err) - } - return c.STS, nil -} - // GetGCPIAMClient returns GCP IAM client. func (c *TestCloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) { return gcpcredentials.NewIamCredentialsClient(ctx, diff --git a/lib/cloud/mocks/aws.go b/lib/cloud/mocks/aws.go index f07981464c9..d48050dc5f3 100644 --- a/lib/cloud/mocks/aws.go +++ b/lib/cloud/mocks/aws.go @@ -81,7 +81,7 @@ func (m *STSClientV1) AssumeRoleWithContext(ctx aws.Context, in *sts.AssumeRoleI expiry := time.Now().Add(60 * time.Minute) return &sts.AssumeRoleOutput{ Credentials: &sts.Credentials{ - AccessKeyId: in.RoleArn, + AccessKeyId: aws.String("FAKEACCESSKEYID"), SecretAccessKey: aws.String("secret"), SessionToken: aws.String("token"), Expiration: &expiry, diff --git a/lib/cloud/mocks/aws_sts.go b/lib/cloud/mocks/aws_sts.go index cf117788e69..09ae2796d19 100644 --- a/lib/cloud/mocks/aws_sts.go +++ b/lib/cloud/mocks/aws_sts.go @@ -46,6 +46,7 @@ import ( type STSClient struct { STSClientV1 + Unauth bool // credentialProvider is only set when a chain of assumed roles is used. credentialProvider aws.CredentialsProvider // recordFn records the role and external ID when a role is assumed. @@ -55,17 +56,25 @@ type STSClient struct { } func (m *STSClient) GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) { + if m.Unauth { + return nil, trace.AccessDenied("unauthorized") + } + return &sts.GetCallerIdentityOutput{ Arn: aws.String(m.ARN), }, nil } func (m *STSClient) AssumeRoleWithWebIdentity(ctx context.Context, in *sts.AssumeRoleWithWebIdentityInput, _ ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error) { + if m.Unauth { + return nil, trace.AccessDenied("unauthorized") + } + m.record(aws.ToString(in.RoleArn), "") expiry := time.Now().Add(60 * time.Minute) return &sts.AssumeRoleWithWebIdentityOutput{ Credentials: &ststypes.Credentials{ - AccessKeyId: in.RoleArn, + AccessKeyId: aws.String("WEBIDENTITYFAKEACCESSKEYID"), SecretAccessKey: aws.String("secret"), SessionToken: aws.String("token"), Expiration: &expiry, @@ -74,6 +83,10 @@ func (m *STSClient) AssumeRoleWithWebIdentity(ctx context.Context, in *sts.Assum } func (m *STSClient) AssumeRole(ctx context.Context, in *sts.AssumeRoleInput, _ ...func(*sts.Options)) (*sts.AssumeRoleOutput, error) { + if m.Unauth { + return nil, trace.AccessDenied("unauthorized") + } + // Retrieve credentials if we have a credential provider, so that all // assume-role providers in a role chain are triggered to call AssumeRole. if m.credentialProvider != nil { @@ -87,7 +100,7 @@ func (m *STSClient) AssumeRole(ctx context.Context, in *sts.AssumeRoleInput, _ . expiry := time.Now().Add(60 * time.Minute) return &sts.AssumeRoleOutput{ Credentials: &ststypes.Credentials{ - AccessKeyId: in.RoleArn, + AccessKeyId: aws.String("FAKEACCESSKEYID"), SecretAccessKey: aws.String("secret"), SessionToken: aws.String("token"), Expiration: &expiry, diff --git a/lib/srv/app/aws/handler_test.go b/lib/srv/app/aws/handler_test.go index 9189bb7e3e5..90dbd4ed46d 100644 --- a/lib/srv/app/aws/handler_test.go +++ b/lib/srv/app/aws/handler_test.go @@ -26,6 +26,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" "time" @@ -57,6 +58,11 @@ import ( awsutils "github.com/gravitational/teleport/lib/utils/aws" ) +func TestMain(m *testing.M) { + utils.InitLoggerForTests() + os.Exit(m.Run()) +} + type makeRequest func(url string, provider client.ConfigProvider, awsHost string) error func s3Request(url string, provider client.ConfigProvider, awsHost string) error { diff --git a/lib/srv/db/cloud/iam.go b/lib/srv/db/cloud/iam.go index ef49e061e59..2cd2da23a35 100644 --- a/lib/srv/db/cloud/iam.go +++ b/lib/srv/db/cloud/iam.go @@ -269,11 +269,13 @@ func (c *IAM) getAWSIdentity(ctx context.Context, database types.Database) (awsl return c.agentIdentity, nil } c.mu.RUnlock() - sts, err := c.cfg.Clients.GetAWSSTSClient(ctx, meta.Region, cloud.WithAmbientCredentials()) + + awsCfg, err := c.cfg.AWSConfigProvider.GetConfig(ctx, meta.Region, awsconfig.WithAmbientCredentials()) if err != nil { return nil, trace.Wrap(err) } - awsIdentity, err := awslib.GetIdentityWithClient(ctx, sts) + clt := c.cfg.awsClients.getSTSClient(awsCfg) + awsIdentity, err := awslib.GetIdentityWithClientV2(ctx, clt) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/srv/db/cloud/iam_test.go b/lib/srv/db/cloud/iam_test.go index bbee3f9f350..3c7e4c63f28 100644 --- a/lib/srv/db/cloud/iam_test.go +++ b/lib/srv/db/cloud/iam_test.go @@ -156,16 +156,15 @@ func TestAWSIAM(t *testing.T) { AWSConfigProvider: &mocks.AWSConfigProvider{ STSClient: stsClient, }, - Clients: &clients.TestCloudClients{ - STS: &stsClient.STSClientV1, - }, - HostID: "host-id", + Clients: &clients.TestCloudClients{}, + HostID: "host-id", onProcessedTask: func(iamTask, error) { taskChan <- struct{}{} }, awsClients: fakeAWSClients{ iamClient: iamClient, rdsClient: clt, + stsClient: stsClient, }, }) require.NoError(t, err) @@ -291,8 +290,10 @@ func TestAWSIAMNoPermissions(t *testing.T) { t.Cleanup(cancel) // Create unauthorized mocks for AWS services. - stsClient := &mocks.STSClientV1{ - ARN: "arn:aws:iam::123456789012:role/test-role", + stsClient := &mocks.STSClient{ + STSClientV1: mocks.STSClientV1{ + ARN: "arn:aws:iam::123456789012:role/test-role", + }, } tests := []struct { name string @@ -301,70 +302,64 @@ func TestAWSIAMNoPermissions(t *testing.T) { awsClients awsClientProvider }{ { - name: "RDS database", - meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{InstanceID: "postgres-rds", ResourceID: "postgres-rds-resource-id"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "RDS database", + meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{InstanceID: "postgres-rds", ResourceID: "postgres-rds-resource-id"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{Unauth: true}, rdsClient: &mocks.RDSClient{Unauth: true}, + stsClient: stsClient, }, }, { - name: "Aurora cluster", - meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{ClusterID: "postgres-aurora", ResourceID: "postgres-aurora-resource-id"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "Aurora cluster", + meta: types.AWS{Region: "localhost", AccountID: "123456789012", RDS: types.RDS{ClusterID: "postgres-aurora", ResourceID: "postgres-aurora-resource-id"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{Unauth: true}, rdsClient: &mocks.RDSClient{Unauth: true}, + stsClient: stsClient, }, }, { - name: "RDS database missing metadata", - meta: types.AWS{Region: "localhost", RDS: types.RDS{ClusterID: "postgres-aurora"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "RDS database missing metadata", + meta: types.AWS{Region: "localhost", RDS: types.RDS{ClusterID: "postgres-aurora"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{Unauth: true}, rdsClient: &mocks.RDSClient{Unauth: true}, + stsClient: stsClient, }, }, { - name: "Redshift cluster", - meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "Redshift cluster", + meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{Unauth: true}, + stsClient: stsClient, }, }, { - name: "ElastiCache", - meta: types.AWS{Region: "localhost", AccountID: "123456789012", ElastiCache: types.ElastiCache{ReplicationGroupID: "some-group"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "ElastiCache", + meta: types.AWS{Region: "localhost", AccountID: "123456789012", ElastiCache: types.ElastiCache{ReplicationGroupID: "some-group"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{Unauth: true}, + stsClient: stsClient, }, }, { - name: "IAM UnmodifiableEntityException", - meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}}, - clients: &clients.TestCloudClients{ - STS: stsClient, - }, + name: "IAM UnmodifiableEntityException", + meta: types.AWS{Region: "localhost", AccountID: "123456789012", Redshift: types.Redshift{ClusterID: "redshift-cluster-1"}}, + clients: &clients.TestCloudClients{}, awsClients: fakeAWSClients{ iamClient: &mocks.IAMMock{ Error: &iamtypes.UnmodifiableEntityException{ Message: aws.String("Cannot perform the operation on the protected role"), }, }, + stsClient: stsClient, }, }, } @@ -377,11 +372,7 @@ func TestAWSIAMNoPermissions(t *testing.T) { Clients: test.clients, HostID: "host-id", AWSConfigProvider: &mocks.AWSConfigProvider{ - STSClient: &mocks.STSClient{ - STSClientV1: mocks.STSClientV1{ - ARN: "arn:aws:iam::123456789012:role/test-role", - }, - }, + STSClient: stsClient, }, awsClients: test.awsClients, }) diff --git a/lib/srv/db/cloud/meta.go b/lib/srv/db/cloud/meta.go index cfa0e75ed24..ca84c8a0a60 100644 --- a/lib/srv/db/cloud/meta.go +++ b/lib/srv/db/cloud/meta.go @@ -36,6 +36,7 @@ import ( redshifttypes "github.com/aws/aws-sdk-go-v2/service/redshift/types" rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless" rsstypes "github.com/aws/aws-sdk-go-v2/service/redshiftserverless/types" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" "github.com/gravitational/teleport" @@ -93,6 +94,11 @@ type rssClient interface { GetWorkgroup(ctx context.Context, params *rss.GetWorkgroupInput, optFns ...func(*rss.Options)) (*rss.GetWorkgroupOutput, error) } +// stsClient defines a subset of the AWS STS client API. +type stsClient interface { + GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) +} + // awsClientProvider is an AWS SDK client provider. type awsClientProvider interface { getElastiCacheClient(cfg aws.Config, optFns ...func(*elasticache.Options)) elasticacheClient @@ -102,6 +108,7 @@ type awsClientProvider interface { getRDSClient(cfg aws.Config, optFns ...func(*rds.Options)) rdsClient getRedshiftClient(cfg aws.Config, optFns ...func(*redshift.Options)) redshiftClient getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient + getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient } type defaultAWSClients struct{} @@ -134,6 +141,10 @@ func (defaultAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...f return rss.NewFromConfig(cfg, optFns...) } +func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return sts.NewFromConfig(cfg, optFns...) +} + // MetadataConfig is the cloud metadata service config. type MetadataConfig struct { // Clients is an interface for retrieving cloud clients. diff --git a/lib/srv/db/cloud/meta_test.go b/lib/srv/db/cloud/meta_test.go index 76c0668a62c..1aea1d19e38 100644 --- a/lib/srv/db/cloud/meta_test.go +++ b/lib/srv/db/cloud/meta_test.go @@ -35,6 +35,7 @@ import ( redshifttypes "github.com/aws/aws-sdk-go-v2/service/redshift/types" rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless" rsstypes "github.com/aws/aws-sdk-go-v2/service/redshiftserverless/types" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/types" @@ -511,6 +512,7 @@ type fakeAWSClients struct { rdsClient rdsClient redshiftClient redshiftClient rssClient rssClient + stsClient stsClient } func (f fakeAWSClients) getElastiCacheClient(cfg aws.Config, optFns ...func(*elasticache.Options)) elasticacheClient { @@ -540,3 +542,7 @@ func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options) func (f fakeAWSClients) getRedshiftServerlessClient(aws.Config, ...func(*rss.Options)) rssClient { return f.rssClient } + +func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return f.stsClient +} diff --git a/lib/srv/db/cloud/resource_checker.go b/lib/srv/db/cloud/resource_checker.go index ea38c69abec..12fe017c3ca 100644 --- a/lib/srv/db/cloud/resource_checker.go +++ b/lib/srv/db/cloud/resource_checker.go @@ -80,7 +80,7 @@ func NewDiscoveryResourceChecker(cfg DiscoveryResourceCheckerConfig) (DiscoveryR return nil, trace.Wrap(err) } - credentialsChecker, err := newCrednentialsChecker(cfg) + credentialsChecker, err := newCredentialsChecker(cfg) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/srv/db/cloud/resource_checker_credentials.go b/lib/srv/db/cloud/resource_checker_credentials.go index 3a35bd7614d..0e6d5e7770d 100644 --- a/lib/srv/db/cloud/resource_checker_credentials.go +++ b/lib/srv/db/cloud/resource_checker_credentials.go @@ -30,6 +30,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/cloud" "github.com/gravitational/teleport/lib/cloud/aws" + "github.com/gravitational/teleport/lib/cloud/awsconfig" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/utils" ) @@ -40,13 +41,15 @@ import ( // Note that this checker warns the user with suggestions on how to configure // the credentials correctly instead of returning errors. type credentialsChecker struct { - cloudClients cloud.Clients - resourceMatchers []services.ResourceMatcher - logger *slog.Logger - cache *utils.FnCache + awsConfigProvider awsconfig.Provider + awsClients awsClientProvider + azureClients cloud.AzureClients + resourceMatchers []services.ResourceMatcher + logger *slog.Logger + cache *utils.FnCache } -func newCrednentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChecker, error) { +func newCredentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChecker, error) { cache, err := utils.NewFnCache(utils.FnCacheConfig{ TTL: 10 * time.Minute, Context: cfg.Context, @@ -56,10 +59,12 @@ func newCrednentialsChecker(cfg DiscoveryResourceCheckerConfig) (*credentialsChe } return &credentialsChecker{ - cloudClients: cfg.Clients, - resourceMatchers: cfg.ResourceMatchers, - logger: cfg.Logger, - cache: cache, + awsConfigProvider: cfg.AWSConfigProvider, + awsClients: defaultAWSClients{}, + azureClients: cfg.Clients, + resourceMatchers: cfg.ResourceMatchers, + logger: cfg.Logger, + cache: cache, }, nil } @@ -111,18 +116,19 @@ func (c *credentialsChecker) getAWSIdentity(ctx context.Context, meta *types.AWS } identity, err := utils.FnCacheGet(ctx, c.cache, types.CloudAWS, func(ctx context.Context) (aws.Identity, error) { - client, err := c.cloudClients.GetAWSSTSClient(ctx, "", cloud.WithAmbientCredentials()) + awsCfg, err := c.awsConfigProvider.GetConfig(ctx, meta.Region, awsconfig.WithAmbientCredentials()) if err != nil { return nil, trace.Wrap(err) } - return aws.GetIdentityWithClient(ctx, client) + client := c.awsClients.getSTSClient(awsCfg) + return aws.GetIdentityWithClientV2(ctx, client) }) return identity, trace.Wrap(err) } func (c *credentialsChecker) checkAzure(ctx context.Context, database types.Database) { allSubIDs, err := utils.FnCacheGet(ctx, c.cache, types.CloudAzure, func(ctx context.Context) ([]string, error) { - client, err := c.cloudClients.GetAzureSubscriptionClient() + client, err := c.azureClients.GetAzureSubscriptionClient() if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/srv/db/common/auth.go b/lib/srv/db/common/auth.go index 375133b5905..d0347f3c6a6 100644 --- a/lib/srv/db/common/auth.go +++ b/lib/srv/db/common/auth.go @@ -38,6 +38,7 @@ import ( rdsauth "github.com/aws/aws-sdk-go-v2/feature/rds/auth" "github.com/aws/aws-sdk-go-v2/service/redshift" rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/aws-sdk-go/aws/credentials" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/gravitational/trace" @@ -135,10 +136,16 @@ type rssClient interface { GetCredentials(context.Context, *rss.GetCredentialsInput, ...func(*rss.Options)) (*rss.GetCredentialsOutput, error) } +// stsClient defines a subset of the AWS STS client API. +type stsClient interface { + GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) +} + // awsClientProvider is an AWS SDK client provider. type awsClientProvider interface { getRedshiftClient(cfg aws.Config, optFns ...func(*redshift.Options)) redshiftClient getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient + getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient } type defaultAWSClients struct{} @@ -151,6 +158,10 @@ func (defaultAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...f return rss.NewFromConfig(cfg, optFns...) } +func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return sts.NewFromConfig(cfg, optFns...) +} + // AuthConfig is the database access authenticator configuration. type AuthConfig struct { // AuthClient is the cluster auth client. @@ -1141,12 +1152,13 @@ func (a *dbAuth) buildAWSRoleARNFromDatabaseUser(ctx context.Context, database t awsAccountID = assumeRoleARN.AccountID default: a.cfg.Logger.DebugContext(ctx, "Fetching AWS Account ID to build role ARN") - stsClient, err := a.cfg.Clients.GetAWSSTSClient(ctx, dbAWS.Region, cloud.WithAmbientCredentials()) + awsCfg, err := a.cfg.AWSConfigProvider.GetConfig(ctx, dbAWS.Region, awsconfig.WithAmbientCredentials()) if err != nil { return "", trace.Wrap(err) } + clt := a.cfg.awsClients.getSTSClient(awsCfg) - identity, err := awslib.GetIdentityWithClient(ctx, stsClient) + identity, err := awslib.GetIdentityWithClientV2(ctx, clt) if err != nil { return "", trace.Wrap(err) } diff --git a/lib/srv/db/common/auth_test.go b/lib/srv/db/common/auth_test.go index f98de77911f..a1cfd04f167 100644 --- a/lib/srv/db/common/auth_test.go +++ b/lib/srv/db/common/auth_test.go @@ -34,6 +34,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/redshift" rss "github.com/aws/aws-sdk-go-v2/service/redshiftserverless" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/stretchr/testify/require" @@ -591,7 +592,7 @@ func TestAuthGetAWSTokenWithAssumedRole(t *testing.T) { require.Equal(t, "some-user", query.Get("User")) require.Equal(t, "host", query.Get("X-Amz-SignedHeaders")) require.Equal(t, "token", query.Get("X-Amz-Security-Token")) - require.Equal(t, "arn:aws:iam::123456789012:role/RedisRole/20010203/ca-central-1/elasticache/aws4_request", + require.Equal(t, "FAKEACCESSKEYID/20010203/ca-central-1/elasticache/aws4_request", query.Get("X-Amz-Credential")) }, checkSTS: func(t *testing.T, stsMock *mocks.STSClient) { @@ -622,6 +623,7 @@ func TestAuthGetAWSTokenWithAssumedRole(t *testing.T) { rssClient: &mocks.RedshiftServerlessClient{ GetCredentialsOutput: mocks.RedshiftServerlessGetCredentialsOutput("IAM:some-user", "some-password", clock), }, + stsClient: fakeSTS, }, }) require.NoError(t, err) @@ -643,58 +645,55 @@ func TestGetAWSIAMCreds(t *testing.T) { for name, tt := range map[string]struct { db types.Database - stsMock *mocks.STSClientV1 + stsMock *mocks.STSClient username string - expectedKeyId string expectedAssumedRoles []string expectedExternalIDs []string - expectErr require.ErrorAssertionFunc + wantErrContains string }{ "username is full role ARN": { db: newMongoAtlasDatabase(t, types.AWS{}), - stsMock: &mocks.STSClientV1{}, + stsMock: &mocks.STSClient{}, username: "arn:aws:iam::123456789012:role/role-name", - expectedKeyId: "arn:aws:iam::123456789012:role/role-name", expectedAssumedRoles: []string{"arn:aws:iam::123456789012:role/role-name"}, expectedExternalIDs: []string{""}, - expectErr: require.NoError, }, "username is partial role ARN": { db: newMongoAtlasDatabase(t, types.AWS{}), - stsMock: &mocks.STSClientV1{ - // This is the role returned by the STS GetCallerIdentity. - ARN: "arn:aws:iam::222222222222:role/teleport-service-role", + stsMock: &mocks.STSClient{ + STSClientV1: mocks.STSClientV1{ + // This is the role returned by the STS GetCallerIdentity. + ARN: "arn:aws:iam::222222222222:role/teleport-service-role", + }, }, username: "role/role-name", - expectedKeyId: "arn:aws:iam::222222222222:role/role-name", expectedAssumedRoles: []string{"arn:aws:iam::222222222222:role/role-name"}, expectedExternalIDs: []string{""}, - expectErr: require.NoError, }, "unable to fetch account ID": { db: newMongoAtlasDatabase(t, types.AWS{}), - stsMock: &mocks.STSClientV1{ - ARN: "", + stsMock: &mocks.STSClient{ + Unauth: true, }, - username: "role/role-name", - expectErr: require.Error, + username: "role/role-name", + wantErrContains: "unauthorized", }, "chained IAM role": { db: newMongoAtlasDatabase(t, types.AWS{ ExternalID: "123123", AssumeRoleARN: "arn:aws:iam::222222222222:role/teleport-service-role-external", }), - stsMock: &mocks.STSClientV1{ - ARN: "arn:aws:iam::111111111111:role/teleport-service-role", + stsMock: &mocks.STSClient{ + STSClientV1: mocks.STSClientV1{ + ARN: "arn:aws:iam::111111111111:role/teleport-service-role", + }, }, - username: "role/role-name", - expectedKeyId: "arn:aws:iam::222222222222:role/role-name", + username: "role/role-name", expectedAssumedRoles: []string{ "arn:aws:iam::222222222222:role/teleport-service-role-external", "arn:aws:iam::222222222222:role/role-name", }, expectedExternalIDs: []string{"123123", ""}, - expectErr: require.NoError, }, } { t.Run(name, func(t *testing.T) { @@ -703,15 +702,22 @@ func TestGetAWSIAMCreds(t *testing.T) { AuthClient: new(authClientMock), AccessPoint: new(accessPointMock), Clients: &cloud.TestCloudClients{ - STS: tt.stsMock, + STS: &tt.stsMock.STSClientV1, }, AWSConfigProvider: &mocks.AWSConfigProvider{}, + awsClients: fakeAWSClients{ + stsClient: tt.stsMock, + }, }) require.NoError(t, err) keyId, _, _, err := auth.GetAWSIAMCreds(ctx, tt.db, tt.username) - tt.expectErr(t, err) - require.Equal(t, tt.expectedKeyId, keyId) + if tt.wantErrContains != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.wantErrContains) + return + } + require.Equal(t, "FAKEACCESSKEYID", keyId) require.ElementsMatch(t, tt.expectedAssumedRoles, tt.stsMock.GetAssumedRoleARNs()) require.ElementsMatch(t, tt.expectedExternalIDs, tt.stsMock.GetAssumedRoleExternalIDs()) }) @@ -1025,6 +1031,7 @@ func (m *imdsMock) GetType() types.InstanceMetadataType { type fakeAWSClients struct { redshiftClient redshiftClient rssClient rssClient + stsClient stsClient } func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options)) redshiftClient { @@ -1034,3 +1041,7 @@ func (f fakeAWSClients) getRedshiftClient(aws.Config, ...func(*redshift.Options) func (f fakeAWSClients) getRedshiftServerlessClient(cfg aws.Config, optFns ...func(*rss.Options)) rssClient { return f.rssClient } + +func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return f.stsClient +} diff --git a/lib/srv/discovery/fetchers/aws-sync/aws-sync.go b/lib/srv/discovery/fetchers/aws-sync/aws-sync.go index eecc06b0ef2..21a5ca1af85 100644 --- a/lib/srv/discovery/fetchers/aws-sync/aws-sync.go +++ b/lib/srv/discovery/fetchers/aws-sync/aws-sync.go @@ -29,8 +29,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/service/sts" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" "golang.org/x/sync/errgroup" @@ -106,6 +105,11 @@ type iamClient interface { ListSAMLProviders(context.Context, *iam.ListSAMLProvidersInput, ...func(*iam.Options)) (*iam.ListSAMLProvidersOutput, error) } +// stsClient defines a subset of the AWS STS client API. +type stsClient interface { + GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error) +} + // awsClientProvider provides AWS service API clients. type awsClientProvider interface { // getIAMClient provides an [iamClient]. @@ -114,6 +118,8 @@ type awsClientProvider interface { getRDSClient(cfg aws.Config, optFns ...func(*rds.Options)) rdsClient // getS3Client provides an [s3Client]. getS3Client(cfg aws.Config, optFns ...func(*s3.Options)) s3Client + // getSTSClient provides an [stsClient]. + getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient } type defaultAWSClients struct{} @@ -130,6 +136,10 @@ func (defaultAWSClients) getS3Client(cfg aws.Config, optFns ...func(*s3.Options) return s3.NewFromConfig(cfg, optFns...) } +func (defaultAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return sts.NewFromConfig(cfg, optFns...) +} + // AssumeRole is the configuration for assuming an AWS role. type AssumeRole struct { // RoleARN is the ARN of the role to assume. @@ -355,36 +365,9 @@ func (a *Fetcher) poll(ctx context.Context, features Features) (*Resources, erro return result, trace.NewAggregate(errs...) } -// getAWSOptions returns a list of AWSAssumeRoleOptionFn to be used when -// creating AWS clients. -func (a *Fetcher) getAWSOptions() []cloud.AWSOptionsFn { - opts := []cloud.AWSOptionsFn{ - cloud.WithCredentialsMaybeIntegration(a.Config.Integration), - } - - if a.Config.AssumeRole != nil { - opts = append(opts, cloud.WithAssumeRole(a.Config.AssumeRole.RoleARN, a.Config.AssumeRole.ExternalID)) - } - const maxRetries = 10 - opts = append(opts, - cloud.WithMaxRetries(maxRetries), - cloud.WithRetryer( - client.DefaultRetryer{ - NumMaxRetries: maxRetries, - MinRetryDelay: time.Second, - MinThrottleDelay: time.Second, - MaxRetryDelay: 300 * time.Second, - MaxThrottleDelay: 300 * time.Second, - }, - ), - ) - - return opts -} - -// getAWSV2Options returns a list of options to be used when +// getAWSOptions returns a list of options to be used when // creating AWS clients with the v2 sdk. -func (a *Fetcher) getAWSV2Options() []awsconfig.OptionsFn { +func (a *Fetcher) getAWSOptions() []awsconfig.OptionsFn { opts := []awsconfig.OptionsFn{ awsconfig.WithCredentialsMaybeIntegration(a.Config.Integration), } @@ -404,7 +387,7 @@ func (a *Fetcher) getAWSV2Options() []awsconfig.OptionsFn { } func (a *Fetcher) getAccountId(ctx context.Context) (string, error) { - stsClient, err := a.CloudClients.GetAWSSTSClient( + awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because groups are global */ a.getAWSOptions()..., @@ -412,9 +395,10 @@ func (a *Fetcher) getAccountId(ctx context.Context) (string, error) { if err != nil { return "", trace.Wrap(err) } + stsClient := a.awsClients.getSTSClient(awsCfg) input := &sts.GetCallerIdentityInput{} - req, err := stsClient.GetCallerIdentityWithContext(ctx, input) + req, err := stsClient.GetCallerIdentity(ctx, input) if err != nil { return "", trace.Wrap(err) } diff --git a/lib/srv/discovery/fetchers/aws-sync/ec2.go b/lib/srv/discovery/fetchers/aws-sync/ec2.go index 1ae520e743c..1587ef66246 100644 --- a/lib/srv/discovery/fetchers/aws-sync/ec2.go +++ b/lib/srv/discovery/fetchers/aws-sync/ec2.go @@ -89,7 +89,7 @@ func (a *Fetcher) fetchAWSEC2Instances(ctx context.Context) ([]*accessgraphv1alp return h.Region == region && h.AccountId == a.AccountID }, ) - ec2Client, err := a.GetEC2Client(ctx, region, a.getAWSV2Options()...) + ec2Client, err := a.GetEC2Client(ctx, region, a.getAWSOptions()...) if err != nil { collectHosts(prevIterationEc2, trace.Wrap(err)) return nil @@ -155,7 +155,7 @@ func (a *Fetcher) fetchInstanceProfiles(ctx context.Context) ([]*accessgraphv1al awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return existing, trace.Wrap(err) diff --git a/lib/srv/discovery/fetchers/aws-sync/eks.go b/lib/srv/discovery/fetchers/aws-sync/eks.go index beba3a5a9a3..1fec520ae96 100644 --- a/lib/srv/discovery/fetchers/aws-sync/eks.go +++ b/lib/srv/discovery/fetchers/aws-sync/eks.go @@ -103,7 +103,7 @@ func (a *Fetcher) fetchAWSSEKSClusters(ctx context.Context) (fetchAWSEKSClusters for _, region := range a.Regions { region := region eG.Go(func() error { - eksClient, err := a.GetEKSClient(ctx, region, a.getAWSV2Options()...) + eksClient, err := a.GetEKSClient(ctx, region, a.getAWSOptions()...) if err != nil { collectClusters(nil, nil, nil, trace.Wrap(err)) return nil diff --git a/lib/srv/discovery/fetchers/aws-sync/groups.go b/lib/srv/discovery/fetchers/aws-sync/groups.go index b212ee1629b..47be4da6b47 100644 --- a/lib/srv/discovery/fetchers/aws-sync/groups.go +++ b/lib/srv/discovery/fetchers/aws-sync/groups.go @@ -94,7 +94,7 @@ func (a *Fetcher) fetchGroups(ctx context.Context) ([]*accessgraphv1alpha.AWSGro awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return a.lastResult.Groups, trace.Wrap(err) @@ -144,7 +144,7 @@ func (a *Fetcher) fetchGroupInlinePolicies(ctx context.Context, group *accessgra awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -201,7 +201,7 @@ func (a *Fetcher) fetchGroupAttachedPolicies(ctx context.Context, group *accessg awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/srv/discovery/fetchers/aws-sync/idp.go b/lib/srv/discovery/fetchers/aws-sync/idp.go index a564c5f02eb..f1aa70db618 100644 --- a/lib/srv/discovery/fetchers/aws-sync/idp.go +++ b/lib/srv/discovery/fetchers/aws-sync/idp.go @@ -37,7 +37,7 @@ func (a *Fetcher) pollAWSSAMLProviders(ctx context.Context, result *Resources, c awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because saml providers are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { collectErr(trace.Wrap(err, "failed to get AWS IAM client")) @@ -138,7 +138,7 @@ func (a *Fetcher) pollAWSOIDCProviders(ctx context.Context, result *Resources, c awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because oidc providers are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { collectErr(trace.Wrap(err, "failed to get AWS IAM client")) diff --git a/lib/srv/discovery/fetchers/aws-sync/policies.go b/lib/srv/discovery/fetchers/aws-sync/policies.go index 511245a4b73..72dd1857870 100644 --- a/lib/srv/discovery/fetchers/aws-sync/policies.go +++ b/lib/srv/discovery/fetchers/aws-sync/policies.go @@ -53,7 +53,7 @@ func (a *Fetcher) fetchPolicies(ctx context.Context) ([]*accessgraphv1alpha.AWSP awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/srv/discovery/fetchers/aws-sync/rds.go b/lib/srv/discovery/fetchers/aws-sync/rds.go index 3d7117425d6..849bd5c71df 100644 --- a/lib/srv/discovery/fetchers/aws-sync/rds.go +++ b/lib/srv/discovery/fetchers/aws-sync/rds.go @@ -81,7 +81,7 @@ func (a *Fetcher) fetchAWSRDSDatabases(ctx context.Context) ( for _, region := range a.Regions { region := region eG.Go(func() error { - awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSV2Options()...) + awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSOptions()...) if err != nil { collectDBs(nil, trace.Wrap(err)) return nil diff --git a/lib/srv/discovery/fetchers/aws-sync/rds_test.go b/lib/srv/discovery/fetchers/aws-sync/rds_test.go index 27940a1d421..330486aafc8 100644 --- a/lib/srv/discovery/fetchers/aws-sync/rds_test.go +++ b/lib/srv/discovery/fetchers/aws-sync/rds_test.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" rdstypes "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" @@ -221,6 +222,7 @@ type fakeAWSClients struct { iamClient iamClient rdsClient rdsClient s3Client s3Client + stsClient stsClient } func (f fakeAWSClients) getIAMClient(cfg aws.Config, optFns ...func(*iam.Options)) iamClient { @@ -234,3 +236,7 @@ func (f fakeAWSClients) getRDSClient(cfg aws.Config, optFns ...func(*rds.Options func (f fakeAWSClients) getS3Client(cfg aws.Config, optFns ...func(*s3.Options)) s3Client { return f.s3Client } + +func (f fakeAWSClients) getSTSClient(cfg aws.Config, optFns ...func(*sts.Options)) stsClient { + return f.stsClient +} diff --git a/lib/srv/discovery/fetchers/aws-sync/roles.go b/lib/srv/discovery/fetchers/aws-sync/roles.go index 5f076f4b525..1770c7f8c31 100644 --- a/lib/srv/discovery/fetchers/aws-sync/roles.go +++ b/lib/srv/discovery/fetchers/aws-sync/roles.go @@ -89,7 +89,7 @@ func (a *Fetcher) fetchRoles(ctx context.Context) ([]*accessgraphv1alpha.AWSRole awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because roles are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -126,7 +126,7 @@ func (a *Fetcher) fetchRoleInlinePolicies(ctx context.Context, role *accessgraph awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -175,7 +175,7 @@ func (a *Fetcher) fetchRoleAttachedPolicies(ctx context.Context, role *accessgra awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/srv/discovery/fetchers/aws-sync/s3.go b/lib/srv/discovery/fetchers/aws-sync/s3.go index 20f749f6fd9..e318b9391e7 100644 --- a/lib/srv/discovery/fetchers/aws-sync/s3.go +++ b/lib/srv/discovery/fetchers/aws-sync/s3.go @@ -218,7 +218,7 @@ func (a *Fetcher) getS3BucketDetails(ctx context.Context, bucket s3types.Bucket, var errs []error var details s3Details - awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, bucketRegion, a.getAWSV2Options()...) + awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, bucketRegion, a.getAWSOptions()...) if err != nil { errs = append(errs, trace.Wrap(err, "failed to create s3 client for bucket %q", aws.ToString(bucket.Name)), @@ -300,7 +300,7 @@ func (a *Fetcher) listS3Buckets(ctx context.Context) ([]s3types.Bucket, func(*st } // use any region to list buckets - awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSV2Options()...) + awsCfg, err := a.AWSConfigProvider.GetConfig(ctx, region, a.getAWSOptions()...) if err != nil { return nil, nil, trace.Wrap(err) } diff --git a/lib/srv/discovery/fetchers/aws-sync/users.go b/lib/srv/discovery/fetchers/aws-sync/users.go index 94f28cc9a39..4f0c552eede 100644 --- a/lib/srv/discovery/fetchers/aws-sync/users.go +++ b/lib/srv/discovery/fetchers/aws-sync/users.go @@ -108,7 +108,7 @@ func (a *Fetcher) fetchUsers(ctx context.Context) ([]*accessgraphv1alpha.AWSUser awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -174,7 +174,7 @@ func (a *Fetcher) fetchUserInlinePolicies(ctx context.Context, user *accessgraph awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -231,7 +231,7 @@ func (a *Fetcher) fetchUserAttachedPolicies(ctx context.Context, user *accessgra awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err) @@ -280,7 +280,7 @@ func (a *Fetcher) fetchGroupsForUser(ctx context.Context, user *accessgraphv1alp awsCfg, err := a.AWSConfigProvider.GetConfig( ctx, "", /* region is empty because users and groups are global */ - a.getAWSV2Options()..., + a.getAWSOptions()..., ) if err != nil { return nil, trace.Wrap(err)