Skip to content

Commit 5eea2c3

Browse files
authored
cloud connectors role chaining (#2960)
* cloud connectors role chainning * add cloud connectors config * use cloudconnectors init on getIdentity * fix aws config retrier * add tests * more tests * fix duration * fix chain flow * remove comments * make role chaining dynamic
1 parent 1e2b63f commit 5eea2c3

File tree

7 files changed

+343
-23
lines changed

7 files changed

+343
-23
lines changed

internal/config/config.go

+31-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ type CloudConfig struct {
6767
}
6868

6969
type AwsConfig struct {
70-
Cred aws.ConfigAWS `config:"credentials"`
71-
AccountType string `config:"account_type"`
70+
Cred aws.ConfigAWS `config:"credentials"`
71+
AccountType string `config:"account_type"`
72+
CloudConnectors bool `config:"supports_cloud_connectors"`
73+
CloudConnectorsConfig CloudConnectorsConfig
7274
}
7375

7476
type GcpConfig struct {
@@ -169,6 +171,10 @@ func New(cfg *config.C) (*Config, error) {
169171
))
170172
}
171173

174+
if c.CloudConfig.Aws.CloudConnectors {
175+
c.CloudConfig.Aws.CloudConnectorsConfig = newCloudConnectorsConfig()
176+
}
177+
172178
return c, nil
173179
}
174180

@@ -203,3 +209,26 @@ func isSupportedBenchmark(benchmark string) bool {
203209
}
204210
return false
205211
}
212+
213+
// Cloud Connectors roles and resource id must be provided by the system (controller)
214+
// and not user input (package policy) for security reasons.
215+
216+
const (
217+
CloudConnectorsLocalRoleEnvVar = "CLOUD_CONNECTORS_LOCAL_ROLE"
218+
CloudConnectorsGlobalRoleEnvVar = "CLOUD_CONNECTORS_GLOBAL_ROLE"
219+
CloudResourceIDEnvVar = "CLOUD_RESOURCE_ID"
220+
)
221+
222+
type CloudConnectorsConfig struct {
223+
LocalRoleARN string
224+
GlobalRoleARN string
225+
ResourceID string
226+
}
227+
228+
func newCloudConnectorsConfig() CloudConnectorsConfig {
229+
return CloudConnectorsConfig{
230+
LocalRoleARN: os.Getenv(CloudConnectorsLocalRoleEnvVar),
231+
GlobalRoleARN: os.Getenv(CloudConnectorsGlobalRoleEnvVar),
232+
ResourceID: os.Getenv(CloudResourceIDEnvVar),
233+
}
234+
}

internal/config/config_test.go

+115-9
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,25 @@ func (s *ConfigTestSuite) TestNew() {
4747
expectedCloudConfig CloudConfig
4848
}{
4949
{
50-
`
50+
config: `
5151
config:
5252
v1:
5353
benchmark: cis_k8s
5454
`,
55-
"cis_k8s",
56-
CloudConfig{},
55+
expectedType: "cis_k8s",
56+
expectedCloudConfig: CloudConfig{},
5757
},
5858
{
59-
`
59+
config: `
6060
config:
6161
v1:
6262
benchmark: cis_azure
6363
`,
64-
"cis_azure",
65-
CloudConfig{},
64+
expectedType: "cis_azure",
65+
expectedCloudConfig: CloudConfig{},
6666
},
6767
{
68-
`
68+
config: `
6969
config:
7070
v1:
7171
benchmark: cis_eks
@@ -79,8 +79,8 @@ config:
7979
credential_profile_name: credential_profile_name
8080
role_arn: role_arn
8181
`,
82-
"cis_eks",
83-
CloudConfig{
82+
expectedType: "cis_eks",
83+
expectedCloudConfig: CloudConfig{
8484
Aws: AwsConfig{
8585
Cred: aws.ConfigAWS{
8686
AccessKeyID: "key",
@@ -229,3 +229,109 @@ revision: 1`,
229229
})
230230
}
231231
}
232+
233+
func (s *ConfigTestSuite) TestCloudConnectorsConfig() {
234+
tests := map[string]struct {
235+
config string
236+
overwriteEnv func(t *testing.T)
237+
expectedType string
238+
expectedCloudConfig CloudConfig
239+
}{
240+
"happy path cloud connectors enabled": {
241+
config: `
242+
config:
243+
v1:
244+
benchmark: cis_aws
245+
aws:
246+
supports_cloud_connectors: true
247+
credentials:
248+
external_id: abc123
249+
`,
250+
expectedType: "cis_aws",
251+
expectedCloudConfig: CloudConfig{
252+
Aws: AwsConfig{
253+
CloudConnectors: true,
254+
Cred: aws.ConfigAWS{
255+
ExternalID: "abc123",
256+
},
257+
CloudConnectorsConfig: CloudConnectorsConfig{},
258+
},
259+
},
260+
},
261+
"happy path cloud connectors enabled - attempt overwrite roles": {
262+
config: `
263+
config:
264+
v1:
265+
benchmark: cis_aws
266+
aws:
267+
account_type: single-account
268+
supports_cloud_connectors: true
269+
credentials:
270+
external_id: abc123
271+
CloudConnectorsConfig:
272+
LocalRoleARN: "abc123"
273+
LocalRoleARN: "abc123"
274+
`,
275+
expectedType: "cis_aws",
276+
expectedCloudConfig: CloudConfig{
277+
Aws: AwsConfig{
278+
AccountType: SingleAccount,
279+
CloudConnectors: true,
280+
Cred: aws.ConfigAWS{
281+
ExternalID: "abc123",
282+
},
283+
CloudConnectorsConfig: CloudConnectorsConfig{},
284+
},
285+
},
286+
},
287+
"happy path cloud connectors enabled - env vars set": {
288+
config: `
289+
config:
290+
v1:
291+
benchmark: cis_aws
292+
aws:
293+
account_type: single-account
294+
supports_cloud_connectors: true
295+
credentials:
296+
external_id: abc123
297+
`,
298+
overwriteEnv: func(t *testing.T) {
299+
t.Helper()
300+
t.Setenv(CloudConnectorsLocalRoleEnvVar, "abc123")
301+
t.Setenv(CloudConnectorsGlobalRoleEnvVar, "abc456")
302+
t.Setenv(CloudResourceIDEnvVar, "abc789")
303+
},
304+
expectedType: "cis_aws",
305+
expectedCloudConfig: CloudConfig{
306+
Aws: AwsConfig{
307+
AccountType: SingleAccount,
308+
CloudConnectors: true,
309+
Cred: aws.ConfigAWS{
310+
ExternalID: "abc123",
311+
},
312+
CloudConnectorsConfig: CloudConnectorsConfig{
313+
LocalRoleARN: "abc123",
314+
GlobalRoleARN: "abc456",
315+
ResourceID: "abc789",
316+
},
317+
},
318+
},
319+
},
320+
}
321+
322+
for i, test := range tests {
323+
s.Run(fmt.Sprint(i), func() {
324+
if test.overwriteEnv != nil {
325+
test.overwriteEnv(s.T())
326+
}
327+
cfg, err := config.NewConfigFrom(test.config)
328+
s.Require().NoError(err)
329+
330+
c, err := New(cfg)
331+
s.Require().NoError(err)
332+
333+
s.Equal(test.expectedType, c.Benchmark)
334+
s.Equal(test.expectedCloudConfig, c.CloudConfig)
335+
})
336+
}
337+
}

internal/flavors/benchmark/aws.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,14 @@ func (a *AWS) initialize(ctx context.Context, log *clog.Logger, cfg *config.Conf
8484
}
8585

8686
func (a *AWS) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) {
87-
awsConfig, err := awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
87+
var awsConfig *awssdk.Config
88+
var err error
89+
90+
if cfg.CloudConfig.Aws.CloudConnectors {
91+
awsConfig, err = awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws)
92+
} else {
93+
awsConfig, err = awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
94+
}
8895
if err != nil {
8996
return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
9097
}

internal/flavors/benchmark/aws_org.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,15 @@ func (a *AWSOrg) pickManagementAccountRole(ctx context.Context, log *clog.Logger
218218
}
219219

220220
func (a *AWSOrg) getIdentity(ctx context.Context, cfg *config.Config) (*awssdk.Config, *cloud.Identity, error) {
221-
awsConfig, err := awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
221+
var awsConfig *awssdk.Config
222+
var err error
223+
224+
if cfg.CloudConfig.Aws.CloudConnectors {
225+
awsConfig, err = awslib.InitializeAWSConfigCloudConnectors(ctx, cfg.CloudConfig.Aws)
226+
} else {
227+
awsConfig, err = awslib.InitializeAWSConfig(cfg.CloudConfig.Aws.Cred)
228+
}
229+
222230
if err != nil {
223231
return nil, nil, fmt.Errorf("failed to initialize AWS credentials: %w", err)
224232
}

internal/flavors/benchmark/aws_test.go

+92
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ import (
2121
"errors"
2222
"testing"
2323

24+
"github.com/aws/aws-sdk-go-v2/aws"
25+
"github.com/aws/aws-sdk-go-v2/credentials"
26+
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
27+
libbeataws "github.com/elastic/beats/v7/x-pack/libbeat/common/aws"
28+
"github.com/stretchr/testify/mock"
29+
2430
"github.com/elastic/cloudbeat/internal/config"
31+
"github.com/elastic/cloudbeat/internal/dataprovider/providers/cloud"
2532
"github.com/elastic/cloudbeat/internal/resources/fetching"
2633
"github.com/elastic/cloudbeat/internal/resources/providers/awslib"
2734
"github.com/elastic/cloudbeat/internal/resources/utils/testhelper"
@@ -60,6 +67,91 @@ func TestAWS_Initialize(t *testing.T) {
6067
fetching.S3Type,
6168
},
6269
},
70+
{
71+
name: "cloud connectors",
72+
cfg: config.Config{
73+
Benchmark: "cis_aws",
74+
CloudConfig: config.CloudConfig{
75+
Aws: config.AwsConfig{
76+
AccountType: config.SingleAccount,
77+
Cred: libbeataws.ConfigAWS{},
78+
CloudConnectors: true,
79+
CloudConnectorsConfig: config.CloudConnectorsConfig{
80+
LocalRoleARN: "abc123",
81+
GlobalRoleARN: "abc456",
82+
ResourceID: "abc789",
83+
},
84+
},
85+
},
86+
},
87+
identityProvider: func() awslib.IdentityProviderGetter {
88+
cfgMatcher := mock.MatchedBy(func(cfg aws.Config) bool {
89+
c, is := cfg.Credentials.(*aws.CredentialsCache)
90+
if !is {
91+
return false
92+
}
93+
return c.IsCredentialsProvider(&stscreds.AssumeRoleProvider{})
94+
})
95+
identityProvider := &awslib.MockIdentityProviderGetter{}
96+
identityProvider.EXPECT().GetIdentity(mock.Anything, cfgMatcher).Return(
97+
&cloud.Identity{
98+
Account: "test-account",
99+
},
100+
nil,
101+
)
102+
103+
return identityProvider
104+
}(),
105+
want: []string{
106+
fetching.IAMType,
107+
fetching.KmsType,
108+
fetching.TrailType,
109+
fetching.AwsMonitoringType,
110+
fetching.EC2NetworkingType,
111+
fetching.RdsType,
112+
fetching.S3Type,
113+
},
114+
},
115+
{
116+
name: "no credential cache in non cloud connectors setup",
117+
cfg: config.Config{
118+
Benchmark: "cis_aws",
119+
CloudConfig: config.CloudConfig{
120+
Aws: config.AwsConfig{
121+
AccountType: config.SingleAccount,
122+
Cred: libbeataws.ConfigAWS{
123+
AccessKeyID: "keyid",
124+
SecretAccessKey: "key",
125+
},
126+
CloudConnectors: false,
127+
},
128+
},
129+
},
130+
identityProvider: func() awslib.IdentityProviderGetter {
131+
cfgMatcher := mock.MatchedBy(func(cfg aws.Config) bool {
132+
_, is := cfg.Credentials.(credentials.StaticCredentialsProvider)
133+
return is
134+
})
135+
identityProvider := &awslib.MockIdentityProviderGetter{}
136+
identityProvider.EXPECT().GetIdentity(mock.Anything, cfgMatcher).Return(
137+
&cloud.Identity{
138+
Account: "test-account",
139+
},
140+
nil,
141+
)
142+
143+
return identityProvider
144+
}(),
145+
want: []string{
146+
fetching.IAMType,
147+
fetching.KmsType,
148+
fetching.TrailType,
149+
fetching.AwsMonitoringType,
150+
fetching.EC2NetworkingType,
151+
fetching.RdsType,
152+
fetching.S3Type,
153+
},
154+
},
63155
}
64156
for _, tt := range tests {
65157
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)