diff --git a/.gitignore b/.gitignore index a96eae5..9195471 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ vendor/ coverage.out drone-s3 + +update_script.sh \ No newline at end of file diff --git a/README.md b/README.md index 7fa7c6d..492437d 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,25 @@ docker run --rm \ -w $(pwd) \ plugins/s3 --dry-run ``` + +## Configuration Variables for Secondary Role Assumption with External ID + +The following environment variables enable the plugin to assume a secondary IAM role using IRSA, with an External ID if required by the role’s trust policy. + +### Variables + +#### `PLUGIN_USER_ROLE_ARN` + +- **Type**: String +- **Required**: No +- **Description**: Specifies the secondary IAM role to be assumed by the plugin, allowing it to inherit permissions associated with this role and access specific AWS resources. + +#### `PLUGIN_USER_ROLE_EXTERNAL_ID` + +- **Type**: String +- **Required**: No +- **Description**: Provide the External ID necessary for the role assumption process if the secondary role’s trust policy mandates it. This is often required for added security, ensuring that only authorized entities assume the role. + +### Usage Notes + +- If the role secondary role (`PLUGIN_USER_ROLE_ARN`) requires an External ID then pass it through `PLUGIN_USER_ROLE_EXTERNAL_ID`. \ No newline at end of file diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl index 1698274..2b23b01 100644 --- a/docker/manifest.tmpl +++ b/docker/manifest.tmpl @@ -28,4 +28,4 @@ manifests: platform: architecture: amd64 os: windows - version: ltsc2022 + version: ltsc2022 \ No newline at end of file diff --git a/main.go b/main.go index df4f859..91deb7e 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "os" "github.com/joho/godotenv" - "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -52,6 +52,11 @@ func main() { Usage: "AWS user role", EnvVar: "PLUGIN_USER_ROLE_ARN,AWS_USER_ROLE_ARN", }, + cli.StringFlag{ + Name: "user-role-external-id", + Usage: "external ID to use when assuming secondary role", + EnvVar: "PLUGIN_USER_ROLE_EXTERNAL_ID", + }, cli.StringFlag{ Name: "bucket", Usage: "aws bucket", @@ -149,7 +154,7 @@ func main() { } if err := app.Run(os.Args); err != nil { - logrus.Fatal(err) + log.Fatal(err) } } @@ -158,6 +163,7 @@ func run(c *cli.Context) error { _ = godotenv.Load(c.String("env-file")) } + plugin := Plugin{ Endpoint: c.String("endpoint"), Key: c.String("access-key"), @@ -166,6 +172,7 @@ func run(c *cli.Context) error { AssumeRoleSessionName: c.String("assume-role-session-name"), Bucket: c.String("bucket"), UserRoleArn: c.String("user-role-arn"), + UserRoleExternalID: c.String("user-role-external-id"), Region: c.String("region"), Access: c.String("acl"), Source: c.String("source"), @@ -186,3 +193,4 @@ func run(c *cli.Context) error { return plugin.Exec() } + diff --git a/plugin.go b/plugin.go index b2eed3f..a6dd797 100644 --- a/plugin.go +++ b/plugin.go @@ -29,6 +29,7 @@ type Plugin struct { AssumeRoleSessionName string Bucket string UserRoleArn string + UserRoleExternalID string // if not "", enable server-side encryption // valid values are: @@ -99,7 +100,7 @@ type Plugin struct { // set externalID for assume role ExternalID string - // set OIDC ID Token to retrieve temporary credentials + // set OIDC ID Token to retrieve temporary credentials IdToken string } @@ -281,6 +282,7 @@ func matchExtension(match string, stringMap map[string]string) string { } func assumeRole(roleArn, roleSessionName, externalID string) *credentials.Credentials { + sess, _ := session.NewSession() client := sts.New(sess) duration := time.Hour * 1 @@ -295,7 +297,9 @@ func assumeRole(roleArn, roleSessionName, externalID string) *credentials.Creden stsProvider.ExternalID = &externalID } - return credentials.NewCredentials(stsProvider) + creds := credentials.NewCredentials(stsProvider) + + return creds } // resolveKey is a helper function that returns s3 object key where file present at srcPath is uploaded to. @@ -434,60 +438,71 @@ func (p *Plugin) downloadS3Objects(client *s3.S3, sourceDir string) error { // createS3Client creates and returns an S3 client based on the plugin configuration func (p *Plugin) createS3Client() *s3.S3 { - conf := &aws.Config{ - Region: aws.String(p.Region), - Endpoint: &p.Endpoint, - DisableSSL: aws.Bool(strings.HasPrefix(p.Endpoint, "http://")), - S3ForcePathStyle: aws.Bool(p.PathStyle), - } - - sess, err := session.NewSession(conf) - if err != nil { - log.Fatalf("failed to create AWS session: %v", err) - } - - if p.Key != "" && p.Secret != "" { - conf.Credentials = credentials.NewStaticCredentials(p.Key, p.Secret, "") - } else if p.IdToken != "" && p.AssumeRole != "" { - creds, err := assumeRoleWithWebIdentity(sess, p.AssumeRole, p.AssumeRoleSessionName, p.IdToken) - if err != nil { - log.Fatalf("failed to assume role with web identity: %v", err) - } - conf.Credentials = creds - } else if p.AssumeRole != "" { - conf.Credentials = assumeRole(p.AssumeRole, p.AssumeRoleSessionName, p.ExternalID) - } else { - log.Warn("AWS Key and/or Secret not provided (falling back to ec2 instance profile)") - } - - sess, err = session.NewSession(conf) - if err != nil { - log.Fatalf("failed to create AWS session: %v", err) - } - - client := s3.New(sess, conf) - - if len(p.UserRoleArn) > 0 { - confRoleArn := aws.Config{ - Region: aws.String(p.Region), - Credentials: stscreds.NewCredentials(sess, p.UserRoleArn), - } - client = s3.New(sess, &confRoleArn) - } - - return client + + conf := &aws.Config{ + Region: aws.String(p.Region), + Endpoint: &p.Endpoint, + DisableSSL: aws.Bool(strings.HasPrefix(p.Endpoint, "http://")), + S3ForcePathStyle: aws.Bool(p.PathStyle), + } + + sess, err := session.NewSession(conf) + if err != nil { + log.Fatalf("failed to create AWS session: %v", err) + } + + if p.Key != "" && p.Secret != "" { + conf.Credentials = credentials.NewStaticCredentials(p.Key, p.Secret, "") + } else if p.IdToken != "" && p.AssumeRole != "" { + creds, err := assumeRoleWithWebIdentity(sess, p.AssumeRole, p.AssumeRoleSessionName, p.IdToken) + if err != nil { + log.Fatalf("failed to assume role with web identity: %v", err) + } + conf.Credentials = creds + } else if p.AssumeRole != "" { + conf.Credentials = assumeRole(p.AssumeRole, p.AssumeRoleSessionName, p.ExternalID) + } else { + log.Warn("AWS Key and/or Secret not provided (falling back to ec2 instance profile)") + } + + client := s3.New(sess, conf) + + if len(p.UserRoleArn) > 0 { + log.WithField("UserRoleArn", p.UserRoleArn).Info("Using user role ARN") + // Create new credentials by assuming the UserRoleArn (with ExternalID when provided) + creds := stscreds.NewCredentials(sess, p.UserRoleArn, func(provider *stscreds.AssumeRoleProvider) { + if p.UserRoleExternalID != "" { + provider.ExternalID = aws.String(p.UserRoleExternalID) + } + }) + + // Create a new session with the new credentials + confWithUserRole := &aws.Config{ + Region: aws.String(p.Region), + Credentials: creds, + } + + sessWithUserRole, err := session.NewSession(confWithUserRole) + if err != nil { + log.Fatalf("failed to create AWS session with user role: %v", err) + } + + client = s3.New(sessWithUserRole) + } + + return client } func assumeRoleWithWebIdentity(sess *session.Session, roleArn, roleSessionName, idToken string) (*credentials.Credentials, error) { - svc := sts.New(sess) - input := &sts.AssumeRoleWithWebIdentityInput{ - RoleArn: aws.String(roleArn), - RoleSessionName: aws.String(roleSessionName), - WebIdentityToken: aws.String(idToken), - } - result, err := svc.AssumeRoleWithWebIdentity(input) - if err != nil { - log.Fatalf("failed to assume role with web identity: %v", err) - } - return credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), nil + svc := sts.New(sess) + input := &sts.AssumeRoleWithWebIdentityInput{ + RoleArn: aws.String(roleArn), + RoleSessionName: aws.String(roleSessionName), + WebIdentityToken: aws.String(idToken), + } + result, err := svc.AssumeRoleWithWebIdentity(input) + if err != nil { + log.Fatalf("failed to assume role with web identity: %v", err) + } + return credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), nil }