Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ vendor/

coverage.out
drone-s3

update_script.sh
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,26 @@ docker run --rm \
-w $(pwd) \
plugins/s3 --dry-run
```

## Configuration Variables for AWS Role Assumption with External ID

The following environment variables allow the plugin to assume a specified AWS IAM role using IRSA, using credentials and an ExternalID if required by the role’s trust policy.

### Variables

#### `PLUGIN_USER_ROLE_ARN`

- **Type**: String
- **Required**: No
- **Description**: Specifies the Amazon Resource Name (ARN) for the IAM role to be assumed by the plugin. This allows the plugin to inherit permissions associated with this role, facilitating access to specific AWS resources.

#### `PLUGIN_USER_ROLE_EXTERNAL_ID`

- **Type**: String
- **Required**: No
- **Description**: Provides the ExternalID necessary for the role assumption process when the role's trust policy mandates an ExternalID. This is often required for added security, ensuring that only authorized entities assume the role.

### Usage Notes

- **Role Assumption without ExternalID**: If only `PLUGIN_USER_ROLE_ARN` is set, the plugin may fail with an AccessDenied 403 error if the role requires an ExternalID.
**Solution** : If the role (Secondary role) requires an External ID then pass it through `PLUGIN_USER_ROLE_EXTERNAL_ID`.
2 changes: 1 addition & 1 deletion docker/manifest.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ manifests:
platform:
architecture: amd64
os: windows
version: ltsc2022
version: ltsc2022
12 changes: 10 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"

"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -149,7 +154,7 @@ func main() {
}

if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
}

Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -186,3 +193,4 @@ func run(c *cli.Context) error {

return plugin.Exec()
}

125 changes: 70 additions & 55 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Plugin struct {
AssumeRoleSessionName string
Bucket string
UserRoleArn string
UserRoleExternalID string

// if not "", enable server-side encryption
// valid values are:
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
}