From d9011a63d5d417dd2b10b4a9357057a7d1c1a954 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 29 Jan 2025 14:59:36 +0200 Subject: [PATCH 01/10] cloud connectors role chainning --- internal/config/config.go | 5 +- internal/resources/providers/awslib/config.go | 74 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index c59bc3aa86..7eebf6d622 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,8 +67,9 @@ type CloudConfig struct { } type AwsConfig struct { - Cred aws.ConfigAWS `config:"credentials"` - AccountType string `config:"account_type"` + Cred aws.ConfigAWS `config:"credentials"` + AccountType string `config:"account_type"` + CloudConnectors bool `config:"supports_cloud_connectors"` } type GcpConfig struct { diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 0c7c08d333..278c5cb04b 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -18,10 +18,16 @@ package awslib import ( + "context" + "fmt" "net/http" + "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/sts" libbeataws "github.com/elastic/beats/v7/x-pack/libbeat/common/aws" ) @@ -43,3 +49,71 @@ func InitializeAWSConfig(cfg libbeataws.ConfigAWS) (*aws.Config, error) { return &awsConfig, nil } + +func CloudConnectorsExternalID(resourceID, externalIDPart string) string { + return fmt.Sprintf("%s-%s", resourceID, externalIDPart) +} + +func InitializeAWSConfigCloudConnectors( + ctx context.Context, + elasticSuperRoleLocalARN string, + elasticSuperRoleGlobalARN string, + resourceID string, + cfg libbeataws.ConfigAWS, +) (aws.Config, error) { + // 1. Load initial config + // (TODO: check directly assuming the first role in chain and/or libbeataws.InitializeAWSConfig(cfg)) + // (TODO: consider os.Setenv("AWS_EC2_METADATA_DISABLED", "true")) + awsConfig, err := config.LoadDefaultConfig(ctx) + if err != nil { + return aws.Config{}, err + } + + // Create an STS client using the base credentials + firstClient := sts.NewFromConfig(awsConfig) + + const defaultDuration = 5 * time.Minute + + // Chain Part 1 - Elastic Super Role Local + localSuperRoleProvider := stscreds.NewAssumeRoleProvider( + firstClient, + elasticSuperRoleLocalARN, + func(aro *stscreds.AssumeRoleOptions) { + aro.RoleSessionName = "cloudbeat-super-role-local" + aro.Duration = defaultDuration + }, + ) + localSuperRoleCredentialsCache := aws.NewCredentialsCache(localSuperRoleProvider) + + // Chain Part 2 - Elastic Super Role Global + globalSuperRoleCfg := awsConfig + globalSuperRoleCfg.Credentials = localSuperRoleCredentialsCache + globalSuperRoleProvider := stscreds.NewAssumeRoleProvider( + sts.NewFromConfig(globalSuperRoleCfg), + elasticSuperRoleGlobalARN, + func(aro *stscreds.AssumeRoleOptions) { + aro.RoleSessionName = "cloudbeat-super-role-global" + aro.Duration = defaultDuration + }, + ) + globalSuperRoleCredentialsCache := aws.NewCredentialsCache(globalSuperRoleProvider) + + // Chain Part 3 - Elastic Super Role Local + customerRemoteRoleCfg := awsConfig + customerRemoteRoleCfg.Credentials = globalSuperRoleCredentialsCache + customerRemoteRoleProvider := stscreds.NewAssumeRoleProvider( + sts.NewFromConfig(customerRemoteRoleCfg), + cfg.RoleArn, // Customer Remote Role passed in package policy. + func(aro *stscreds.AssumeRoleOptions) { + aro.RoleSessionName = "cloudbeat-remote-role" + aro.Duration = cfg.AssumeRoleDuration + aro.ExternalID = aws.String(CloudConnectorsExternalID(resourceID, cfg.ExternalID)) + }, + ) + customerRemoteRoleCredentialsCache := aws.NewCredentialsCache(customerRemoteRoleProvider) + + ret := awsConfig + ret.Credentials = customerRemoteRoleCredentialsCache + + return ret, nil +} From 052f4e4815360b996da490d0edec624c46b7ceee Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 30 Jan 2025 13:04:22 +0200 Subject: [PATCH 02/10] add cloud connectors config --- internal/config/config.go | 34 ++++++++- internal/config/config_test.go | 124 ++++++++++++++++++++++++++++++--- 2 files changed, 146 insertions(+), 12 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 7eebf6d622..bddddec5c9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,9 +67,10 @@ type CloudConfig struct { } type AwsConfig struct { - Cred aws.ConfigAWS `config:"credentials"` - AccountType string `config:"account_type"` - CloudConnectors bool `config:"supports_cloud_connectors"` + Cred aws.ConfigAWS `config:"credentials"` + AccountType string `config:"account_type"` + CloudConnectors bool `config:"supports_cloud_connectors"` + CloudConnectorsConfig CloudConnectorsConfig } type GcpConfig struct { @@ -170,6 +171,10 @@ func New(cfg *config.C) (*Config, error) { )) } + if c.CloudConfig.Aws.CloudConnectors { + c.CloudConfig.Aws.CloudConnectorsConfig = newCloudConnectorsConfig() + } + return c, nil } @@ -204,3 +209,26 @@ func isSupportedBenchmark(benchmark string) bool { } return false } + +// Cloud Connectors roles and resource id must be provided by the system (controller) +// and not user input (package policy) for security reasons. + +const ( + CloudConnectorsLocalRoleEnvVar = "CLOUD_CONNECTORS_LOCAL_ROLE" + CloudConnectorsGlobalRoleEnvVar = "CLOUD_CONNECTORS_GLOBAL_ROLE" + ResourceIDEnvVar = "RESOURCE_ID" +) + +type CloudConnectorsConfig struct { + LocalRoleARN string + GlobalRoleARN string + ResourceID string +} + +func newCloudConnectorsConfig() CloudConnectorsConfig { + return CloudConnectorsConfig{ + LocalRoleARN: os.Getenv(CloudConnectorsLocalRoleEnvVar), + GlobalRoleARN: os.Getenv(CloudConnectorsGlobalRoleEnvVar), + ResourceID: os.Getenv(ResourceIDEnvVar), + } +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b72accba9e..f6333efed0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -47,25 +47,25 @@ func (s *ConfigTestSuite) TestNew() { expectedCloudConfig CloudConfig }{ { - ` + config: ` config: v1: benchmark: cis_k8s `, - "cis_k8s", - CloudConfig{}, + expectedType: "cis_k8s", + expectedCloudConfig: CloudConfig{}, }, { - ` + config: ` config: v1: benchmark: cis_azure `, - "cis_azure", - CloudConfig{}, + expectedType: "cis_azure", + expectedCloudConfig: CloudConfig{}, }, { - ` + config: ` config: v1: benchmark: cis_eks @@ -79,8 +79,8 @@ config: credential_profile_name: credential_profile_name role_arn: role_arn `, - "cis_eks", - CloudConfig{ + expectedType: "cis_eks", + expectedCloudConfig: CloudConfig{ Aws: AwsConfig{ Cred: aws.ConfigAWS{ AccessKeyID: "key", @@ -229,3 +229,109 @@ revision: 1`, }) } } + +func (s *ConfigTestSuite) TestCloudConnectorsConfig() { + tests := map[string]struct { + config string + overwriteEnv func(t *testing.T) + expectedType string + expectedCloudConfig CloudConfig + }{ + "happy path cloud connectors enabled": { + config: ` +config: + v1: + benchmark: cis_aws + aws: + supports_cloud_connectors: true + credentials: + external_id: abc123 +`, + expectedType: "cis_aws", + expectedCloudConfig: CloudConfig{ + Aws: AwsConfig{ + CloudConnectors: true, + Cred: aws.ConfigAWS{ + ExternalID: "abc123", + }, + CloudConnectorsConfig: CloudConnectorsConfig{}, + }, + }, + }, + "happy path cloud connectors enabled - attempt overwrite roles": { + config: ` +config: + v1: + benchmark: cis_aws + aws: + account_type: single-account + supports_cloud_connectors: true + credentials: + external_id: abc123 + CloudConnectorsConfig: + LocalRoleARN: "abc123" + LocalRoleARN: "abc123" +`, + expectedType: "cis_aws", + expectedCloudConfig: CloudConfig{ + Aws: AwsConfig{ + AccountType: SingleAccount, + CloudConnectors: true, + Cred: aws.ConfigAWS{ + ExternalID: "abc123", + }, + CloudConnectorsConfig: CloudConnectorsConfig{}, + }, + }, + }, + "happy path cloud connectors enabled - env vars set": { + config: ` +config: + v1: + benchmark: cis_aws + aws: + account_type: single-account + supports_cloud_connectors: true + credentials: + external_id: abc123 +`, + overwriteEnv: func(t *testing.T) { + t.Helper() + t.Setenv(CloudConnectorsLocalRoleEnvVar, "abc123") + t.Setenv(CloudConnectorsGlobalRoleEnvVar, "abc456") + t.Setenv(ResourceIDEnvVar, "abc789") + }, + expectedType: "cis_aws", + expectedCloudConfig: CloudConfig{ + Aws: AwsConfig{ + AccountType: SingleAccount, + CloudConnectors: true, + Cred: aws.ConfigAWS{ + ExternalID: "abc123", + }, + CloudConnectorsConfig: CloudConnectorsConfig{ + LocalRoleARN: "abc123", + GlobalRoleARN: "abc456", + ResourceID: "abc789", + }, + }, + }, + }, + } + + for i, test := range tests { + s.Run(fmt.Sprint(i), func() { + if test.overwriteEnv != nil { + test.overwriteEnv(s.T()) + } + cfg, err := config.NewConfigFrom(test.config) + s.Require().NoError(err) + + c, err := New(cfg) + s.Require().NoError(err) + + s.Equal(test.expectedType, c.Benchmark) + s.Equal(test.expectedCloudConfig, c.CloudConfig) + }) + } +} From ceb4ed98cc01c03cb92bdb77a5bcf5bc303d9003 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 30 Jan 2025 13:43:07 +0200 Subject: [PATCH 03/10] use cloudconnectors init on getIdentity --- internal/flavors/benchmark/aws.go | 9 +++++- internal/resources/providers/awslib/config.go | 28 ++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/internal/flavors/benchmark/aws.go b/internal/flavors/benchmark/aws.go index 9b70217cee..1c79ed8aa6 100644 --- a/internal/flavors/benchmark/aws.go +++ b/internal/flavors/benchmark/aws.go @@ -84,7 +84,14 @@ func (a *AWS) initialize(ctx context.Context, log *clog.Logger, cfg *config.Conf } func (a *AWS) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) { - awsConfig, err := awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred) + var awsConfig *awssdk.Config + var err error + + if cfg.CloudConfig.Aws.CloudConnectors { + awsConfig, err = awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws) + } else { + awsConfig, err = awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred) + } if err != nil { return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err) } diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 278c5cb04b..dfb20d19fc 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -25,10 +25,12 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" - "github.com/aws/aws-sdk-go-v2/config" + awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" libbeataws "github.com/elastic/beats/v7/x-pack/libbeat/common/aws" + + "github.com/elastic/cloudbeat/internal/config" ) func InitializeAWSConfig(cfg libbeataws.ConfigAWS) (*aws.Config, error) { @@ -54,19 +56,13 @@ func CloudConnectorsExternalID(resourceID, externalIDPart string) string { return fmt.Sprintf("%s-%s", resourceID, externalIDPart) } -func InitializeAWSConfigCloudConnectors( - ctx context.Context, - elasticSuperRoleLocalARN string, - elasticSuperRoleGlobalARN string, - resourceID string, - cfg libbeataws.ConfigAWS, -) (aws.Config, error) { +func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfig) (*aws.Config, error) { // 1. Load initial config // (TODO: check directly assuming the first role in chain and/or libbeataws.InitializeAWSConfig(cfg)) // (TODO: consider os.Setenv("AWS_EC2_METADATA_DISABLED", "true")) - awsConfig, err := config.LoadDefaultConfig(ctx) + awsConfig, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { - return aws.Config{}, err + return nil, err } // Create an STS client using the base credentials @@ -77,7 +73,7 @@ func InitializeAWSConfigCloudConnectors( // Chain Part 1 - Elastic Super Role Local localSuperRoleProvider := stscreds.NewAssumeRoleProvider( firstClient, - elasticSuperRoleLocalARN, + cfg.CloudConnectorsConfig.LocalRoleARN, func(aro *stscreds.AssumeRoleOptions) { aro.RoleSessionName = "cloudbeat-super-role-local" aro.Duration = defaultDuration @@ -90,7 +86,7 @@ func InitializeAWSConfigCloudConnectors( globalSuperRoleCfg.Credentials = localSuperRoleCredentialsCache globalSuperRoleProvider := stscreds.NewAssumeRoleProvider( sts.NewFromConfig(globalSuperRoleCfg), - elasticSuperRoleGlobalARN, + cfg.CloudConnectorsConfig.GlobalRoleARN, func(aro *stscreds.AssumeRoleOptions) { aro.RoleSessionName = "cloudbeat-super-role-global" aro.Duration = defaultDuration @@ -103,11 +99,11 @@ func InitializeAWSConfigCloudConnectors( customerRemoteRoleCfg.Credentials = globalSuperRoleCredentialsCache customerRemoteRoleProvider := stscreds.NewAssumeRoleProvider( sts.NewFromConfig(customerRemoteRoleCfg), - cfg.RoleArn, // Customer Remote Role passed in package policy. + cfg.Cred.RoleArn, // Customer Remote Role passed in package policy. func(aro *stscreds.AssumeRoleOptions) { aro.RoleSessionName = "cloudbeat-remote-role" - aro.Duration = cfg.AssumeRoleDuration - aro.ExternalID = aws.String(CloudConnectorsExternalID(resourceID, cfg.ExternalID)) + aro.Duration = cfg.Cred.AssumeRoleDuration + aro.ExternalID = aws.String(CloudConnectorsExternalID(cfg.CloudConnectorsConfig.LocalRoleARN, cfg.Cred.ExternalID)) }, ) customerRemoteRoleCredentialsCache := aws.NewCredentialsCache(customerRemoteRoleProvider) @@ -115,5 +111,5 @@ func InitializeAWSConfigCloudConnectors( ret := awsConfig ret.Credentials = customerRemoteRoleCredentialsCache - return ret, nil + return &ret, nil } From 3fd97074688363c793c59474f6f1ba57c5d08cf2 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 30 Jan 2025 13:46:20 +0200 Subject: [PATCH 04/10] fix aws config retrier --- internal/resources/providers/awslib/config.go | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index dfb20d19fc..0c6b742d96 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -33,21 +33,23 @@ import ( "github.com/elastic/cloudbeat/internal/config" ) +func awsConfigRetrier() aws.Retryer { + return retry.NewStandard(func(o *retry.StandardOptions) { + o.Retryables = append(o.Retryables, retry.RetryableHTTPStatusCode{ + Codes: map[int]struct{}{ + http.StatusTooManyRequests: {}, + }, + }) + }) +} + func InitializeAWSConfig(cfg libbeataws.ConfigAWS) (*aws.Config, error) { awsConfig, err := libbeataws.InitializeAWSConfig(cfg) if err != nil { return nil, err } - awsConfig.Retryer = func() aws.Retryer { - return retry.NewStandard(func(o *retry.StandardOptions) { - o.Retryables = append(o.Retryables, retry.RetryableHTTPStatusCode{ - Codes: map[int]struct{}{ - http.StatusTooManyRequests: {}, - }, - }) - }) - } + awsConfig.Retryer = awsConfigRetrier return &awsConfig, nil } @@ -111,5 +113,7 @@ func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfi ret := awsConfig ret.Credentials = customerRemoteRoleCredentialsCache + ret.Retryer = awsConfigRetrier + return &ret, nil } From a2ba145a49d39502cec3f2753c6e55efae257cad Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 30 Jan 2025 14:29:53 +0200 Subject: [PATCH 05/10] add tests --- internal/config/config.go | 4 +- internal/config/config_test.go | 2 +- internal/flavors/benchmark/aws_test.go | 51 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index bddddec5c9..0c24ee235d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -216,7 +216,7 @@ func isSupportedBenchmark(benchmark string) bool { const ( CloudConnectorsLocalRoleEnvVar = "CLOUD_CONNECTORS_LOCAL_ROLE" CloudConnectorsGlobalRoleEnvVar = "CLOUD_CONNECTORS_GLOBAL_ROLE" - ResourceIDEnvVar = "RESOURCE_ID" + CloudResourceIDEnvVar = "CLOUD_RESOURCE_ID" ) type CloudConnectorsConfig struct { @@ -229,6 +229,6 @@ func newCloudConnectorsConfig() CloudConnectorsConfig { return CloudConnectorsConfig{ LocalRoleARN: os.Getenv(CloudConnectorsLocalRoleEnvVar), GlobalRoleARN: os.Getenv(CloudConnectorsGlobalRoleEnvVar), - ResourceID: os.Getenv(ResourceIDEnvVar), + ResourceID: os.Getenv(CloudResourceIDEnvVar), } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f6333efed0..b980357a42 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -299,7 +299,7 @@ config: t.Helper() t.Setenv(CloudConnectorsLocalRoleEnvVar, "abc123") t.Setenv(CloudConnectorsGlobalRoleEnvVar, "abc456") - t.Setenv(ResourceIDEnvVar, "abc789") + t.Setenv(CloudResourceIDEnvVar, "abc789") }, expectedType: "cis_aws", expectedCloudConfig: CloudConfig{ diff --git a/internal/flavors/benchmark/aws_test.go b/internal/flavors/benchmark/aws_test.go index dd581fca71..9ca7869612 100644 --- a/internal/flavors/benchmark/aws_test.go +++ b/internal/flavors/benchmark/aws_test.go @@ -21,7 +21,13 @@ import ( "errors" "testing" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + libbeataws "github.com/elastic/beats/v7/x-pack/libbeat/common/aws" + "github.com/stretchr/testify/mock" + "github.com/elastic/cloudbeat/internal/config" + "github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud" "github.com/elastic/cloudbeat/internal/resources/fetching" "github.com/elastic/cloudbeat/internal/resources/providers/awslib" "github.com/elastic/cloudbeat/internal/resources/utils/testhelper" @@ -60,6 +66,51 @@ func TestAWS_Initialize(t *testing.T) { fetching.S3Type, }, }, + { + name: "cloud connectors", + cfg: config.Config{ + Benchmark: "cis_aws", + CloudConfig: config.CloudConfig{ + Aws: config.AwsConfig{ + AccountType: config.SingleAccount, + Cred: libbeataws.ConfigAWS{}, + CloudConnectors: true, + CloudConnectorsConfig: config.CloudConnectorsConfig{ + LocalRoleARN: "abc123", + GlobalRoleARN: "abc456", + ResourceID: "abc789", + }, + }, + }, + }, + identityProvider: func() awslib.IdentityProviderGetter { + cfgMatcher := mock.MatchedBy(func(cfg aws.Config) bool { + c, is := cfg.Credentials.(*aws.CredentialsCache) + if !is { + return false + } + return c.IsCredentialsProvider(&stscreds.AssumeRoleProvider{}) + }) + identityProvider := &awslib.MockIdentityProviderGetter{} + identityProvider.EXPECT().GetIdentity(mock.Anything, cfgMatcher).Return( + &cloud.Identity{ + Account: "test-account", + }, + nil, + ) + + return identityProvider + }(), + want: []string{ + fetching.IAMType, + fetching.KmsType, + fetching.TrailType, + fetching.AwsMonitoringType, + fetching.EC2NetworkingType, + fetching.RdsType, + fetching.S3Type, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 2fe9751ce681f7a0be019bd67bba928439b1cad2 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Thu, 30 Jan 2025 14:41:48 +0200 Subject: [PATCH 06/10] more tests --- internal/flavors/benchmark/aws_test.go | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/internal/flavors/benchmark/aws_test.go b/internal/flavors/benchmark/aws_test.go index 9ca7869612..b07315c429 100644 --- a/internal/flavors/benchmark/aws_test.go +++ b/internal/flavors/benchmark/aws_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" libbeataws "github.com/elastic/beats/v7/x-pack/libbeat/common/aws" "github.com/stretchr/testify/mock" @@ -111,6 +112,46 @@ func TestAWS_Initialize(t *testing.T) { fetching.S3Type, }, }, + { + name: "no credential cache in non cloud connectors setup", + cfg: config.Config{ + Benchmark: "cis_aws", + CloudConfig: config.CloudConfig{ + Aws: config.AwsConfig{ + AccountType: config.SingleAccount, + Cred: libbeataws.ConfigAWS{ + AccessKeyID: "keyid", + SecretAccessKey: "key", + }, + CloudConnectors: false, + }, + }, + }, + identityProvider: func() awslib.IdentityProviderGetter { + cfgMatcher := mock.MatchedBy(func(cfg aws.Config) bool { + _, is := cfg.Credentials.(credentials.StaticCredentialsProvider) + return is + }) + identityProvider := &awslib.MockIdentityProviderGetter{} + identityProvider.EXPECT().GetIdentity(mock.Anything, cfgMatcher).Return( + &cloud.Identity{ + Account: "test-account", + }, + nil, + ) + + return identityProvider + }(), + want: []string{ + fetching.IAMType, + fetching.KmsType, + fetching.TrailType, + fetching.AwsMonitoringType, + fetching.EC2NetworkingType, + fetching.RdsType, + fetching.S3Type, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 0a61a30a9e869c6ea76b9091bbdc1af63d2e3d99 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 12 Feb 2025 12:51:51 +0200 Subject: [PATCH 07/10] fix duration --- internal/resources/providers/awslib/config.go | 2 +- scripts/packaging/docker/elastic-agent/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 0c6b742d96..4e49814bfa 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -70,7 +70,7 @@ func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfi // Create an STS client using the base credentials firstClient := sts.NewFromConfig(awsConfig) - const defaultDuration = 5 * time.Minute + const defaultDuration = 20 * time.Minute // Chain Part 1 - Elastic Super Role Local localSuperRoleProvider := stscreds.NewAssumeRoleProvider( diff --git a/scripts/packaging/docker/elastic-agent/Dockerfile b/scripts/packaging/docker/elastic-agent/Dockerfile index 700312bc38..72fe7957e8 100644 --- a/scripts/packaging/docker/elastic-agent/Dockerfile +++ b/scripts/packaging/docker/elastic-agent/Dockerfile @@ -1,6 +1,6 @@ ARG ELASTIC_AGENT_IMAGE=docker.elastic.co/beats/elastic-agent:8.14.0-SNAPSHOT -FROM ${ELASTIC_AGENT_IMAGE} as elastic_agent_cloudbeat +FROM ${ELASTIC_AGENT_IMAGE} AS elastic_agent_cloudbeat COPY --chown=elastic-agent:elastic-agent --chmod=755 cloudbeat /tmp/components/cloudbeat COPY --chown=elastic-agent:elastic-agent --chmod=666 bundle.tar.gz /tmp/components/bundle.tar.gz COPY --chown=elastic-agent:elastic-agent --chmod=644 cloudbeat.yml /tmp/components/cloudbeat.yml From 74085a3eb79b5f19c154551969357444f6e83338 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 12 Feb 2025 13:50:30 +0200 Subject: [PATCH 08/10] fix chain flow --- internal/resources/providers/awslib/config.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 4e49814bfa..5e6780c2d9 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -59,7 +59,7 @@ func CloudConnectorsExternalID(resourceID, externalIDPart string) string { } func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfig) (*aws.Config, error) { - // 1. Load initial config + // 1. Load initial config - Chain Part 1 - Elastic Super Role Local implicitly assumed. // (TODO: check directly assuming the first role in chain and/or libbeataws.InitializeAWSConfig(cfg)) // (TODO: consider os.Setenv("AWS_EC2_METADATA_DISABLED", "true")) awsConfig, err := awsconfig.LoadDefaultConfig(ctx) @@ -72,22 +72,9 @@ func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfi const defaultDuration = 20 * time.Minute - // Chain Part 1 - Elastic Super Role Local - localSuperRoleProvider := stscreds.NewAssumeRoleProvider( - firstClient, - cfg.CloudConnectorsConfig.LocalRoleARN, - func(aro *stscreds.AssumeRoleOptions) { - aro.RoleSessionName = "cloudbeat-super-role-local" - aro.Duration = defaultDuration - }, - ) - localSuperRoleCredentialsCache := aws.NewCredentialsCache(localSuperRoleProvider) - // Chain Part 2 - Elastic Super Role Global - globalSuperRoleCfg := awsConfig - globalSuperRoleCfg.Credentials = localSuperRoleCredentialsCache globalSuperRoleProvider := stscreds.NewAssumeRoleProvider( - sts.NewFromConfig(globalSuperRoleCfg), + firstClient, cfg.CloudConnectorsConfig.GlobalRoleARN, func(aro *stscreds.AssumeRoleOptions) { aro.RoleSessionName = "cloudbeat-super-role-global" @@ -105,7 +92,7 @@ func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfi func(aro *stscreds.AssumeRoleOptions) { aro.RoleSessionName = "cloudbeat-remote-role" aro.Duration = cfg.Cred.AssumeRoleDuration - aro.ExternalID = aws.String(CloudConnectorsExternalID(cfg.CloudConnectorsConfig.LocalRoleARN, cfg.Cred.ExternalID)) + aro.ExternalID = aws.String(CloudConnectorsExternalID(cfg.CloudConnectorsConfig.ResourceID, cfg.Cred.ExternalID)) }, ) customerRemoteRoleCredentialsCache := aws.NewCredentialsCache(customerRemoteRoleProvider) From d816de4125559d9c5c6d437172683ac859817df9 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Wed, 12 Feb 2025 14:35:33 +0200 Subject: [PATCH 09/10] remove comments --- internal/resources/providers/awslib/config.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 5e6780c2d9..24e87e75ed 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -60,8 +60,6 @@ func CloudConnectorsExternalID(resourceID, externalIDPart string) string { func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfig) (*aws.Config, error) { // 1. Load initial config - Chain Part 1 - Elastic Super Role Local implicitly assumed. - // (TODO: check directly assuming the first role in chain and/or libbeataws.InitializeAWSConfig(cfg)) - // (TODO: consider os.Setenv("AWS_EC2_METADATA_DISABLED", "true")) awsConfig, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { return nil, err From eea5b40ef0661a5f9eb234f4923d10fa528c0985 Mon Sep 17 00:00:00 2001 From: Kostas Stamatakis Date: Tue, 18 Feb 2025 11:19:03 +0200 Subject: [PATCH 10/10] make role chaining dynamic --- internal/flavors/benchmark/aws_org.go | 10 ++- internal/resources/providers/awslib/config.go | 83 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/internal/flavors/benchmark/aws_org.go b/internal/flavors/benchmark/aws_org.go index 4cf8de1483..3f7c51f019 100644 --- a/internal/flavors/benchmark/aws_org.go +++ b/internal/flavors/benchmark/aws_org.go @@ -218,7 +218,15 @@ func (a *AWSOrg) pickManagementAccountRole(ctx context.Context, log *clog.Logger } func (a *AWSOrg) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) { - awsConfig, err := awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred) + var awsConfig *awssdk.Config + var err error + + if cfg.CloudConfig.Aws.CloudConnectors { + awsConfig, err = awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws) + } else { + awsConfig, err = awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred) + } + if err != nil { return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err) } diff --git a/internal/resources/providers/awslib/config.go b/internal/resources/providers/awslib/config.go index 24e87e75ed..d30b58f648 100644 --- a/internal/resources/providers/awslib/config.go +++ b/internal/resources/providers/awslib/config.go @@ -59,46 +59,65 @@ func CloudConnectorsExternalID(resourceID, externalIDPart string) string { } func InitializeAWSConfigCloudConnectors(ctx context.Context, cfg config.AwsConfig) (*aws.Config, error) { - // 1. Load initial config - Chain Part 1 - Elastic Super Role Local implicitly assumed. + const defaultDuration = 20 * time.Minute + + // 1. Load initial config - Chain Step 1 - Elastic Super Role Local implicitly assumed through IRSA. awsConfig, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { return nil, err } - // Create an STS client using the base credentials - firstClient := sts.NewFromConfig(awsConfig) - - const defaultDuration = 20 * time.Minute - - // Chain Part 2 - Elastic Super Role Global - globalSuperRoleProvider := stscreds.NewAssumeRoleProvider( - firstClient, - cfg.CloudConnectorsConfig.GlobalRoleARN, - func(aro *stscreds.AssumeRoleOptions) { - aro.RoleSessionName = "cloudbeat-super-role-global" - aro.Duration = defaultDuration + chain := []AWSRoleChainingStep{ + // Chain Step 2 - Elastic Super Role Global + { + RoleARN: cfg.CloudConnectorsConfig.GlobalRoleARN, + Options: func(aro *stscreds.AssumeRoleOptions) { + aro.RoleSessionName = "cloudbeat-super-role-global" + aro.Duration = defaultDuration + }, }, - ) - globalSuperRoleCredentialsCache := aws.NewCredentialsCache(globalSuperRoleProvider) - - // Chain Part 3 - Elastic Super Role Local - customerRemoteRoleCfg := awsConfig - customerRemoteRoleCfg.Credentials = globalSuperRoleCredentialsCache - customerRemoteRoleProvider := stscreds.NewAssumeRoleProvider( - sts.NewFromConfig(customerRemoteRoleCfg), - cfg.Cred.RoleArn, // Customer Remote Role passed in package policy. - func(aro *stscreds.AssumeRoleOptions) { - aro.RoleSessionName = "cloudbeat-remote-role" - aro.Duration = cfg.Cred.AssumeRoleDuration - aro.ExternalID = aws.String(CloudConnectorsExternalID(cfg.CloudConnectorsConfig.ResourceID, cfg.Cred.ExternalID)) + // Chain Step 3 - Elastic Super Role Local + { + RoleARN: cfg.Cred.RoleArn, + Options: func(aro *stscreds.AssumeRoleOptions) { + aro.RoleSessionName = "cloudbeat-remote-role" + aro.Duration = cfg.Cred.AssumeRoleDuration + aro.ExternalID = aws.String(CloudConnectorsExternalID(cfg.CloudConnectorsConfig.ResourceID, cfg.Cred.ExternalID)) + }, }, - ) - customerRemoteRoleCredentialsCache := aws.NewCredentialsCache(customerRemoteRoleProvider) + } + + retConf := AWSConfigRoleChaining(awsConfig, chain) + retConf.Retryer = awsConfigRetrier + + return retConf, nil +} - ret := awsConfig - ret.Credentials = customerRemoteRoleCredentialsCache +// AWSConfigRoleChaining initializes an assume role provider and an credential cache for each step on the chain, using the previous one as client. +func AWSConfigRoleChaining(initialConfig aws.Config, chain []AWSRoleChainingStep) *aws.Config { + var client *sts.Client + var assumeRoleProvider *stscreds.AssumeRoleProvider + var credentialsCache *aws.CredentialsCache + cnf := initialConfig + + for _, c := range chain { + client = sts.NewFromConfig(cnf) // create client using the credentials from previous or initial step. + + // create a assume role provider for the current chain part role. + assumeRoleProvider = stscreds.NewAssumeRoleProvider( + client, + c.RoleARN, + c.Options, + ) + credentialsCache = aws.NewCredentialsCache(assumeRoleProvider) + + cnf.Credentials = credentialsCache + } - ret.Retryer = awsConfigRetrier + return &cnf +} - return &ret, nil +type AWSRoleChainingStep struct { + RoleARN string + Options func(aro *stscreds.AssumeRoleOptions) }