Skip to content

Commit dfae145

Browse files
committed
fix assume role tgb
1 parent db09cb9 commit dfae145

File tree

10 files changed

+388
-94
lines changed

10 files changed

+388
-94
lines changed

docs/guide/targetgroupbinding/targetgroupbinding.md

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,110 @@ spec:
112112
### AssumeRole
113113

114114
Sometimes the AWS LoadBalancer controller needs to manipulate target groups from different AWS accounts.
115-
The way to do that is assuming a role from such account. The following spec fields help you with that.
115+
The way to do that is assuming a role from such an account. The following spec fields help you with that.
116116

117117
* `iamRoleArnToAssume`: the ARN that you need to assume
118118
* `assumeRoleExternalId`: the external ID for the assume role operation. Optional, but recommended. It helps you to prevent the confused deputy problem ( https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html )
119119

120120

121+
```yaml
122+
apiVersion: elbv2.k8s.aws/v1beta1
123+
kind: TargetGroupBinding
124+
metadata:
125+
name: peered-tg
126+
namespace: nlb-game-2048-1
127+
spec:
128+
assumeRoleExternalId: very-secret-string-2
129+
iamRoleArnToAssume: arn:aws:iam::155642222660:role/tg-management-role
130+
networking:
131+
ingress:
132+
- from:
133+
- securityGroup:
134+
groupID: sg-0b6a41a2fd959623f
135+
ports:
136+
- port: 80
137+
protocol: TCP
138+
serviceRef:
139+
name: service-2048
140+
port: 80
141+
targetGroupARN: arn:aws:elasticloadbalancing:us-west-2:155642222660:targetgroup/peered-tg/6a4ecf7bfae473c1
142+
```
143+
144+
In the following examples, we will refer to Cluster Owner (CO) and Target Group Owner (TGO) accounts.
145+
146+
First, in the TGO account creates a role that will allow the AWS LBC in the CO account to assume it.
147+
For improved security, we only allow the AWS LBC role in CO account to assume the role.
148+
149+
```json
150+
{
151+
"Version": "2012-10-17",
152+
"Statement": [
153+
{
154+
"Sid": "",
155+
"Effect": "Allow",
156+
"Principal": {
157+
"AWS": "arn:aws:iam::565768096483:role/eksctl-awslbc-loadtest-addon-iamserviceaccoun-Role1-13RdJCMqV6p2"
158+
},
159+
"Action": "sts:AssumeRole",
160+
"Condition": {
161+
"StringEquals": {
162+
"sts:ExternalId": "very-secret-string"
163+
}
164+
}
165+
}
166+
]
167+
}
168+
```
169+
170+
Next, still in the TGO account we need to add the following permissions to the Role created in the first step.
171+
172+
```json
173+
{
174+
"Version": "2012-10-17",
175+
"Statement": [
176+
{
177+
"Sid": "VisualEditor0",
178+
"Effect": "Allow",
179+
"Action": [
180+
"elasticloadbalancing:RegisterTargets",
181+
"elasticloadbalancing:DeregisterTargets"
182+
],
183+
"Resource": [
184+
"arn:aws:elasticloadbalancing:us-west-2:155642222660:targetgroup/tg1/*",
185+
"arn:aws:elasticloadbalancing:us-west-2:155642222660:targetgroup/tg2/*"
186+
// add more here //
187+
]
188+
},
189+
{
190+
"Sid": "VisualEditor1",
191+
"Effect": "Allow",
192+
"Action": [
193+
"elasticloadbalancing:DescribeTargetGroups",
194+
"elasticloadbalancing:DescribeTargetHealth"
195+
],
196+
"Resource": "*"
197+
}
198+
]
199+
}
200+
```
201+
202+
203+
Next, in the CO account, we need to allow the AWS LBC to perform the AssumeRole call.
204+
By default, this permission is not a part of the standard IAM policy that is vended with the LBC installation scripts.
205+
For improved security, it is possible to scope the AssumeRole permissions down to only roles that you know ahead of time the
206+
LBC will need to Assume.
207+
208+
```json
209+
{
210+
"Effect": "Allow",
211+
"Action": [
212+
"sts:AssumeRole"
213+
],
214+
"Resource": "*"
215+
}
216+
```
217+
218+
121219
## Sample YAML
122220

123221
```yaml

helm/aws-load-balancer-controller/crds/crds.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,15 @@ spec:
317317
spec:
318318
description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
319319
properties:
320+
assumeRoleExternalId:
321+
description: IAM Role ARN to assume when calling AWS APIs. Needed
322+
to assume a role in another account and prevent the confused deputy
323+
problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
324+
type: string
325+
iamRoleArnToAssume:
326+
description: IAM Role ARN to assume when calling AWS APIs. Useful
327+
if the target group is in a different AWS account
328+
type: string
320329
multiClusterTargetGroup:
321330
description: MultiClusterTargetGroup Denotes if the TargetGroup is
322331
shared among multiple clusters
@@ -494,6 +503,15 @@ spec:
494503
spec:
495504
description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding
496505
properties:
506+
assumeRoleExternalId:
507+
description: IAM Role ARN to assume when calling AWS APIs. Needed
508+
to assume a role in another account and prevent the confused deputy
509+
problem. https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html
510+
type: string
511+
iamRoleArnToAssume:
512+
description: IAM Role ARN to assume when calling AWS APIs. Useful
513+
if the target group is in a different AWS account
514+
type: string
497515
ipAddressType:
498516
description: ipAddressType specifies whether the target group is of
499517
type IPv4 or IPv6. If unspecified, it will be automatically inferred.

pkg/aws/aws_config.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"github.com/aws/aws-sdk-go-v2/aws"
6+
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
7+
"github.com/aws/aws-sdk-go-v2/aws/ratelimit"
8+
"github.com/aws/aws-sdk-go-v2/aws/retry"
9+
"github.com/aws/aws-sdk-go-v2/config"
10+
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
11+
smithymiddleware "github.com/aws/smithy-go/middleware"
12+
"sigs.k8s.io/aws-load-balancer-controller/pkg/aws/throttle"
13+
awsmetrics "sigs.k8s.io/aws-load-balancer-controller/pkg/metrics/aws"
14+
"sigs.k8s.io/aws-load-balancer-controller/pkg/version"
15+
)
16+
17+
const (
18+
userAgent = "elbv2.k8s.aws"
19+
)
20+
21+
func NewAWSConfigGenerator(cfg CloudConfig, ec2IMDSEndpointMode imds.EndpointModeState, metricsCollector *awsmetrics.Collector) AWSConfigGenerator {
22+
return &awsConfigGeneratorImpl{
23+
cfg: cfg,
24+
ec2IMDSEndpointMode: ec2IMDSEndpointMode,
25+
metricsCollector: metricsCollector,
26+
}
27+
28+
}
29+
30+
// AWSConfigGenerator is responsible for generating an aws config based on the running environment
31+
type AWSConfigGenerator interface {
32+
GenerateAWSConfig(optFns ...func(*config.LoadOptions) error) (aws.Config, error)
33+
}
34+
35+
type awsConfigGeneratorImpl struct {
36+
cfg CloudConfig
37+
ec2IMDSEndpointMode imds.EndpointModeState
38+
metricsCollector *awsmetrics.Collector
39+
}
40+
41+
func (gen *awsConfigGeneratorImpl) GenerateAWSConfig(optFns ...func(*config.LoadOptions) error) (aws.Config, error) {
42+
43+
defaultOpts := []func(*config.LoadOptions) error{
44+
config.WithRegion(gen.cfg.Region),
45+
config.WithRetryer(func() aws.Retryer {
46+
return retry.NewStandard(func(o *retry.StandardOptions) {
47+
o.RateLimiter = ratelimit.None
48+
o.MaxAttempts = gen.cfg.MaxRetries
49+
})
50+
}),
51+
config.WithEC2IMDSEndpointMode(gen.ec2IMDSEndpointMode),
52+
config.WithAPIOptions([]func(stack *smithymiddleware.Stack) error{
53+
awsmiddleware.AddUserAgentKeyValue(userAgent, version.GitVersion),
54+
}),
55+
}
56+
57+
defaultOpts = append(defaultOpts, optFns...)
58+
59+
awsConfig, err := config.LoadDefaultConfig(context.TODO(),
60+
defaultOpts...,
61+
)
62+
63+
if err != nil {
64+
return aws.Config{}, err
65+
}
66+
67+
if gen.cfg.ThrottleConfig != nil {
68+
throttler := throttle.NewThrottler(gen.cfg.ThrottleConfig)
69+
awsConfig.APIOptions = append(awsConfig.APIOptions, func(stack *smithymiddleware.Stack) error {
70+
return throttle.WithSDKRequestThrottleMiddleware(throttler)(stack)
71+
})
72+
}
73+
74+
if gen.metricsCollector != nil {
75+
awsConfig.APIOptions = awsmetrics.WithSDKMetricCollector(gen.metricsCollector, awsConfig.APIOptions)
76+
}
77+
78+
return awsConfig, nil
79+
}
80+
81+
var _ AWSConfigGenerator = &awsConfigGeneratorImpl{}

pkg/aws/cloud.go

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,11 @@ import (
1010
"sync"
1111
"time"
1212

13-
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
14-
"github.com/aws/aws-sdk-go-v2/aws/ratelimit"
15-
"github.com/aws/aws-sdk-go-v2/aws/retry"
1613
"github.com/aws/aws-sdk-go-v2/config"
1714
"github.com/aws/aws-sdk-go-v2/credentials"
1815
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
1916
"github.com/aws/aws-sdk-go-v2/service/sts"
2017

21-
smithymiddleware "github.com/aws/smithy-go/middleware"
22-
"sigs.k8s.io/aws-load-balancer-controller/pkg/aws/throttle"
23-
"sigs.k8s.io/aws-load-balancer-controller/pkg/version"
24-
2518
"github.com/aws/aws-sdk-go-v2/aws"
2619
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
2720
"github.com/aws/aws-sdk-go-v2/service/ec2"
@@ -35,7 +28,6 @@ import (
3528
)
3629

3730
const (
38-
userAgent = "elbv2.k8s.aws"
3931
cacheTTLBufferTime = 30 * time.Second
4032
)
4133

@@ -81,29 +73,11 @@ func NewCloud(cfg CloudConfig, clusterName string, metricsCollector *aws_metrics
8173
}
8274
cfg.Region = region
8375
}
84-
awsConfig, err := config.LoadDefaultConfig(context.TODO(),
85-
config.WithRegion(cfg.Region),
86-
config.WithRetryer(func() aws.Retryer {
87-
return retry.NewStandard(func(o *retry.StandardOptions) {
88-
o.RateLimiter = ratelimit.None
89-
o.MaxAttempts = cfg.MaxRetries
90-
})
91-
}),
92-
config.WithEC2IMDSEndpointMode(ec2IMDSEndpointMode),
93-
config.WithAPIOptions([]func(stack *smithymiddleware.Stack) error{
94-
awsmiddleware.AddUserAgentKeyValue(userAgent, version.GitVersion),
95-
}),
96-
)
97-
98-
if cfg.ThrottleConfig != nil {
99-
throttler := throttle.NewThrottler(cfg.ThrottleConfig)
100-
awsConfig.APIOptions = append(awsConfig.APIOptions, func(stack *smithymiddleware.Stack) error {
101-
return throttle.WithSDKRequestThrottleMiddleware(throttler)(stack)
102-
})
103-
}
10476

105-
if metricsCollector != nil {
106-
awsConfig.APIOptions = aws_metrics.WithSDKMetricCollector(metricsCollector, awsConfig.APIOptions)
77+
awsConfigGenerator := NewAWSConfigGenerator(cfg, ec2IMDSEndpointMode, metricsCollector)
78+
awsConfig, err := awsConfigGenerator.GenerateAWSConfig()
79+
if err != nil {
80+
return nil, errors.Wrap(err, "Unable to generate AWS config")
10781
}
10882

10983
if awsClientsProvider == nil {
@@ -132,6 +106,8 @@ func NewCloud(cfg CloudConfig, clusterName string, metricsCollector *aws_metrics
132106
shield: services.NewShield(awsClientsProvider),
133107
rgt: services.NewRGT(awsClientsProvider),
134108

109+
awsConfigGenerator: awsConfigGenerator,
110+
135111
assumeRoleElbV2Cache: cache.NewExpiring(),
136112

137113
awsClientsProvider: awsClientsProvider,
@@ -229,6 +205,8 @@ type defaultCloud struct {
229205

230206
clusterName string
231207

208+
awsConfigGenerator AWSConfigGenerator
209+
232210
// A cache holding elbv2 clients that are assuming a role.
233211
assumeRoleElbV2Cache *cache.Expiring
234212
// assumeRoleElbV2CacheMutex protects assumeRoleElbV2Cache
@@ -251,31 +229,33 @@ func (c *defaultCloud) GetAssumedRoleELBV2(ctx context.Context, assumeRoleArn st
251229
if exists {
252230
return assumedRoleELBV2.(services.ELBV2), nil
253231
}
254-
c.logger.Info("awsCloud", "method", "GetAssumedRoleELBV2", "AssumeRoleArn", assumeRoleArn, "externalId", externalId)
232+
c.logger.Info("Constructing new elbv2 client", "AssumeRoleArn", assumeRoleArn, "externalId", externalId)
255233

256-
existingAwsConfig, _ := c.awsClientsProvider.GetAWSConfig(ctx, "GetAWSConfigForIAMRoleImpersonation")
234+
stsClient, err := c.awsClientsProvider.GetSTSClient(ctx, "AssumeRole")
235+
if err != nil {
236+
// This should never happen, but let's be forward-looking.
237+
return nil, err
238+
}
257239

258-
sourceAccount := sts.NewFromConfig(*existingAwsConfig)
259-
response, err := sourceAccount.AssumeRole(ctx, &sts.AssumeRoleInput{
240+
response, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{
260241
RoleArn: aws.String(assumeRoleArn),
261242
RoleSessionName: aws.String(generateAssumeRoleSessionName(c.clusterName)),
262243
ExternalId: aws.String(externalId),
263244
})
264245
if err != nil {
265-
c.logger.Error(err, "Unable to assume target role, %v")
246+
c.logger.Error(err, "Unable to assume target role", "roleArn", assumeRoleArn)
266247
return nil, err
267248
}
268249
assumedRoleCreds := response.Credentials
269250
newCreds := credentials.NewStaticCredentialsProvider(*assumedRoleCreds.AccessKeyId, *assumedRoleCreds.SecretAccessKey, *assumedRoleCreds.SessionToken)
270-
newAwsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.cfg.Region), config.WithCredentialsProvider(newCreds))
251+
newAwsConfig, err := c.awsConfigGenerator.GenerateAWSConfig(config.WithCredentialsProvider(newCreds))
271252
if err != nil {
272-
c.logger.Error(err, "Unable to load static credentials for service client config, %v. Attempting to use default client")
253+
c.logger.Error(err, "Create new service client config service client config", "roleArn", assumeRoleArn)
273254
return nil, err
274255
}
275256

276257
cacheTTL := assumedRoleCreds.Expiration.Sub(time.Now())
277-
existingAwsConfig.Credentials = newAwsConfig.Credentials
278-
elbv2WithAssumedRole := services.NewELBV2(c.awsClientsProvider, c)
258+
elbv2WithAssumedRole := services.NewELBV2FromStaticClient(c.awsClientsProvider.GenerateNewELBv2Client(newAwsConfig), c)
279259

280260
c.assumeRoleElbV2CacheMutex.Lock()
281261
defer c.assumeRoleElbV2CacheMutex.Unlock()

0 commit comments

Comments
 (0)