Skip to content

Commit cdffa9f

Browse files
committed
feat(ecr): add STS AssumeRole for cross-account ECR access
Added AssumeRoleArn field across all services that interact with ECR: common-lib: - helmLib/registry: Configuration bean + extractCredentialsForRegistry - utils/bean: DockerAuthConfig.AssumeRoleArnEcr - utils/dockerOperations: LoadEcrCredentials with AssumeRole kubelink: - gRPC proto: AssumeRoleArn field (tag 15) on RegistryCredential - adapter: pass AssumeRoleArn to registry Configuration ci-runner: - DockerCredentials struct + DockerLogin: STS AssumeRole before ECR auth - CommonWorkflowRequest: AssumeRoleArn field - GetDockerAuthConfigForPrivateRegistries: pass ARN - Added log lines for cross-account login confirmation image-scanner: - DockerArtifactStore model: AssumeRoleArn field - RoundTripperService: STS AssumeRole in GetAuthenticatorByDockerRegistryId
1 parent c98656a commit cdffa9f

11 files changed

Lines changed: 130 additions & 4 deletions

File tree

ci-runner/helper/DockerHelper.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
2929
"github.com/aws/aws-sdk-go/aws/session"
3030
"github.com/aws/aws-sdk-go/service/ecr"
31+
"github.com/aws/aws-sdk-go/service/sts"
3132
"github.com/caarlos0/env"
3233
cicxt "github.com/devtron-labs/ci-runner/executor/context"
3334
bean2 "github.com/devtron-labs/ci-runner/helper/bean"
@@ -210,7 +211,7 @@ const CacheModeMax = "max"
210211
const CacheModeMin = "min"
211212

212213
type DockerCredentials struct {
213-
DockerUsername, DockerPassword, AwsRegion, AccessKey, SecretKey, DockerRegistryURL, DockerRegistryType, CredentialsType string
214+
DockerUsername, DockerPassword, AwsRegion, AccessKey, SecretKey, AssumeRoleArn, DockerRegistryURL, DockerRegistryType, CredentialsType string
214215
}
215216

216217
type EnvironmentVariables struct {
@@ -257,6 +258,34 @@ func (impl *DockerHelperImpl) DockerLogin(ciContext cicxt.CiContext, dockerCrede
257258
log.Println(err)
258259
return err
259260
}
261+
262+
// If an assume role ARN is provided, use STS to assume the cross-account role
263+
if len(dockerCredentials.AssumeRoleArn) > 0 {
264+
stsClient := sts.New(sess)
265+
assumeOutput, err := stsClient.AssumeRole(&sts.AssumeRoleInput{
266+
RoleArn: aws.String(dockerCredentials.AssumeRoleArn),
267+
RoleSessionName: aws.String("devtron-ecr-cross-account"),
268+
})
269+
if err != nil {
270+
log.Printf("error in assuming role %s: %v", dockerCredentials.AssumeRoleArn, err)
271+
return err
272+
}
273+
assumedCreds := credentials.NewStaticCredentials(
274+
*assumeOutput.Credentials.AccessKeyId,
275+
*assumeOutput.Credentials.SecretAccessKey,
276+
*assumeOutput.Credentials.SessionToken,
277+
)
278+
sess, err = session.NewSession(&aws.Config{
279+
Region: &dockerCredentials.AwsRegion,
280+
Credentials: assumedCreds,
281+
})
282+
if err != nil {
283+
log.Println(err)
284+
return err
285+
}
286+
log.Printf("STS AssumeRole successful for cross-account ECR access, roleArn: %s", dockerCredentials.AssumeRoleArn)
287+
}
288+
260289
svc := ecr.New(sess)
261290
input := &ecr.GetAuthorizationTokenInput{}
262291
authData, err := svc.GetAuthorizationToken(input)
@@ -293,7 +322,11 @@ func (impl *DockerHelperImpl) DockerLogin(ciContext cicxt.CiContext, dockerCrede
293322
log.Println(err)
294323
return err
295324
}
296-
log.Println("Docker login successful with username ", username, " on docker registry URL ", dockerCredentials.DockerRegistryURL)
325+
if len(dockerCredentials.AssumeRoleArn) > 0 {
326+
log.Printf("Docker login successful (cross-account via AssumeRole %s) with username %s on registry %s", dockerCredentials.AssumeRoleArn, username, dockerCredentials.DockerRegistryURL)
327+
} else {
328+
log.Println("Docker login successful with username ", username, " on docker registry URL ", dockerCredentials.DockerRegistryURL)
329+
}
297330
return nil
298331
}
299332

@@ -1428,6 +1461,7 @@ func (impl *DockerHelperImpl) GetDockerAuthConfigForPrivateRegistries(workflowRe
14281461
AccessKeyEcr: workflowRequest.AccessKey,
14291462
SecretAccessKeyEcr: workflowRequest.SecretKey,
14301463
EcrRegion: workflowRequest.AwsRegion,
1464+
AssumeRoleArnEcr: workflowRequest.AssumeRoleArn,
14311465
IsRegistryPrivate: true,
14321466
}
14331467
}

ci-runner/helper/EventHelper.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ type CommonWorkflowRequest struct {
114114
AwsRegion string `json:"awsRegion"`
115115
AccessKey string `json:"accessKey"`
116116
SecretKey string `json:"secretKey"`
117+
AssumeRoleArn string `json:"assumeRoleArn"`
117118
CiCacheLocation string `json:"ciCacheLocation"`
118119
CiCacheRegion string `json:"ciCacheRegion"`
119120
CiCacheFileName string `json:"ciCacheFileName"`

common-lib/helmLib/registry/bean.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Configuration struct {
1414
AwsAccessKey string
1515
AwsSecretKey string
1616
AwsRegion string
17+
AssumeRoleArn string
1718
RegistryConnectionType string //secure, insecure, secure-with-cert
1819
RegistryCertificateString string
1920
RegistryCAFilePath string

common-lib/helmLib/registry/common.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
1010
"github.com/aws/aws-sdk-go/aws/session"
1111
"github.com/aws/aws-sdk-go/service/ecr"
12+
"github.com/aws/aws-sdk-go/service/sts"
1213
http2 "github.com/devtron-labs/common-lib/utils/http"
1314
"helm.sh/helm/v3/pkg/registry"
1415
"log"
@@ -117,6 +118,33 @@ func extractCredentialsForRegistry(config *Configuration) (string, string, error
117118
log.Printf("error in creating AWS client %w ", err)
118119
return "", "", err
119120
}
121+
122+
// If an assume role ARN is provided, use STS to assume the cross-account role
123+
if len(config.AssumeRoleArn) > 0 {
124+
stsClient := sts.New(sess)
125+
assumeOutput, err := stsClient.AssumeRole(&sts.AssumeRoleInput{
126+
RoleArn: aws.String(config.AssumeRoleArn),
127+
RoleSessionName: aws.String("devtron-ecr-cross-account"),
128+
})
129+
if err != nil {
130+
log.Printf("error in assuming role %s: %v", config.AssumeRoleArn, err)
131+
return "", "", fmt.Errorf("failed to assume role %s: %v", config.AssumeRoleArn, err)
132+
}
133+
assumedCreds := credentials.NewStaticCredentials(
134+
*assumeOutput.Credentials.AccessKeyId,
135+
*assumeOutput.Credentials.SecretAccessKey,
136+
*assumeOutput.Credentials.SessionToken,
137+
)
138+
sess, err = session.NewSession(&aws.Config{
139+
Region: &config.AwsRegion,
140+
Credentials: assumedCreds,
141+
})
142+
if err != nil {
143+
log.Printf("error in creating AWS session with assumed role credentials: %v", err)
144+
return "", "", err
145+
}
146+
}
147+
120148
svc := ecr.New(sess)
121149
input := &ecr.GetAuthorizationTokenInput{}
122150
authData, err := svc.GetAuthorizationToken(input)

common-lib/utils/bean/bean.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type DockerAuthConfig struct {
3939
AccessKeyEcr string // used for pulling from private ecr registry
4040
SecretAccessKeyEcr string // used for pulling from private ecr registry
4141
EcrRegion string // used for pulling from private ecr registry
42+
AssumeRoleArnEcr string // used for cross-account ECR access via STS AssumeRole
4243
CredentialFileJsonGcr string // used for pulling from private gcr registry
4344
IsRegistryPrivate bool
4445
}

common-lib/utils/dockerOperations/DockerUtils.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/aws/aws-sdk-go/aws/credentials"
99
"github.com/aws/aws-sdk-go/aws/session"
1010
"github.com/aws/aws-sdk-go/service/ecr"
11+
"github.com/aws/aws-sdk-go/service/sts"
1112
"github.com/devtron-labs/common-lib/utils/bean"
1213
"github.com/docker/docker/client"
1314
"github.com/sirupsen/logrus"
@@ -30,13 +31,35 @@ func LoadGcrCredentials(credsJson string) (string, string, error) {
3031

3132
}
3233

33-
func LoadEcrCredentials(ecrRegion, accessKeyEcr, secretAccessKeyEcr string) (string, string, error) {
34+
func LoadEcrCredentials(ecrRegion, accessKeyEcr, secretAccessKeyEcr, assumeRoleArn string) (string, string, error) {
3435
var username, password string
3536
awsCfg := &aws.Config{
3637
Region: aws.String(ecrRegion),
3738
Credentials: credentials.NewStaticCredentials(accessKeyEcr, secretAccessKeyEcr, ""),
3839
}
3940
sess := session.Must(session.NewSession(awsCfg))
41+
42+
// If an assume role ARN is provided, use STS to assume the cross-account role
43+
if len(assumeRoleArn) > 0 {
44+
stsClient := sts.New(sess)
45+
assumeOutput, err := stsClient.AssumeRole(&sts.AssumeRoleInput{
46+
RoleArn: aws.String(assumeRoleArn),
47+
RoleSessionName: aws.String("devtron-ecr-cross-account"),
48+
})
49+
if err != nil {
50+
return "", "", fmt.Errorf("failed to assume role %s for ecr: %v", assumeRoleArn, err)
51+
}
52+
assumedCreds := credentials.NewStaticCredentials(
53+
*assumeOutput.Credentials.AccessKeyId,
54+
*assumeOutput.Credentials.SecretAccessKey,
55+
*assumeOutput.Credentials.SessionToken,
56+
)
57+
sess = session.Must(session.NewSession(&aws.Config{
58+
Region: aws.String(ecrRegion),
59+
Credentials: assumedCreds,
60+
}))
61+
}
62+
4063
svc := ecr.New(sess)
4164
authData, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
4265
if err != nil {
@@ -59,7 +82,7 @@ func getEncodedRegistryAuthForPrivateRegistry(dockerAuth *bean.DockerAuthConfig)
5982
switch dockerAuth.RegistryType {
6083
case bean.RegistryTypeEcr:
6184
// for ecr we get username and password via region, access and secret access tokens
62-
ecrUsername, ecrPassword, err := LoadEcrCredentials(dockerAuth.EcrRegion, dockerAuth.AccessKeyEcr, dockerAuth.SecretAccessKeyEcr)
85+
ecrUsername, ecrPassword, err := LoadEcrCredentials(dockerAuth.EcrRegion, dockerAuth.AccessKeyEcr, dockerAuth.SecretAccessKeyEcr, dockerAuth.AssumeRoleArnEcr)
6386
if err != nil {
6487
logrus.Error("error in getting ecr credentials", "err", err)
6588
return "", err

image-scanner/pkg/roundTripper/RoundTripperService.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
2424
"github.com/aws/aws-sdk-go/aws/session"
2525
"github.com/aws/aws-sdk-go/service/ecr"
26+
"github.com/aws/aws-sdk-go/service/sts"
2627
"github.com/devtron-labs/common-lib/imageScan/bean"
2728
"github.com/devtron-labs/common-lib/securestore"
2829
"github.com/devtron-labs/image-scanner/pkg/security"
@@ -137,6 +138,32 @@ func (impl *RoundTripperServiceImpl) GetAuthenticatorByDockerRegistryId(dockerRe
137138
return nil, nil, err
138139
}
139140

141+
// If an assume role ARN is provided, use STS to assume the cross-account role
142+
if len(dockerRegistry.AssumeRoleArn) > 0 {
143+
stsClient := sts.New(sess)
144+
assumeOutput, err := stsClient.AssumeRole(&sts.AssumeRoleInput{
145+
RoleArn: aws.String(dockerRegistry.AssumeRoleArn),
146+
RoleSessionName: aws.String("devtron-ecr-cross-account"),
147+
})
148+
if err != nil {
149+
impl.Logger.Errorw("error in assuming role for ECR", "assumeRoleArn", dockerRegistry.AssumeRoleArn, "err", err)
150+
return nil, nil, err
151+
}
152+
assumedCreds := credentials.NewStaticCredentials(
153+
*assumeOutput.Credentials.AccessKeyId,
154+
*assumeOutput.Credentials.SecretAccessKey,
155+
*assumeOutput.Credentials.SessionToken,
156+
)
157+
sess, err = session.NewSession(&aws.Config{
158+
Region: &dockerRegistry.AWSRegion,
159+
Credentials: assumedCreds,
160+
})
161+
if err != nil {
162+
impl.Logger.Errorw("error in creating AWS session with assumed role credentials", "err", err)
163+
return nil, nil, err
164+
}
165+
}
166+
140167
// Create a ECR client with additional configuration
141168
svc := ecr.New(sess, aws.NewConfig().WithRegion(dockerRegistry.AWSRegion))
142169
token, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})

image-scanner/pkg/sql/repository/DockerArtifactStoreRepository.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type DockerArtifactStore struct {
4242
AWSAccessKeyId string `sql:"aws_accesskey_id" json:"awsAccessKeyId,omitempty" `
4343
AWSSecretAccessKey securestore.EncryptedString `sql:"aws_secret_accesskey" json:"awsSecretAccessKey,omitempty"`
4444
AWSRegion string `sql:"aws_region" json:"awsRegion,omitempty"`
45+
AssumeRoleArn string `sql:"assume_role_arn" json:"assumeRoleArn,omitempty"`
4546
Username string `sql:"username" json:"username,omitempty"`
4647
Password securestore.EncryptedString `sql:"password" json:"password,omitempty"`
4748
IsDefault bool `sql:"is_default,notnull" json:"isDefault"`

kubelink/grpc/applist.pb.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kubelink/grpc/applist.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ message RegistryCredential {
480480
string RegistryName = 12;
481481
string RegistryCertificate = 13;
482482
string CredentialsType = 14;
483+
string AssumeRoleArn = 15;
483484
}
484485

485486
enum RemoteConnectionMethod {

0 commit comments

Comments
 (0)