From a99d155fd4e091504ded67a36ea3111b0e765f5d Mon Sep 17 00:00:00 2001 From: Vladislav Tropnikov Date: Sat, 29 Nov 2025 14:53:00 +0100 Subject: [PATCH] s3: The ability to specify an IAM role for cross-account interaction --- backend/s3/s3.go | 58 +++++++++++++++++++++++++++++++++++++++++++ docs/content/s3.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/backend/s3/s3.go b/backend/s3/s3.go index 728594864..48af2e8e7 100644 --- a/backend/s3/s3.go +++ b/backend/s3/s3.go @@ -30,9 +30,11 @@ import ( v4signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/smithy-go" "github.com/aws/smithy-go/logging" "github.com/aws/smithy-go/middleware" @@ -325,6 +327,30 @@ If empty it will default to the environment variable "AWS_PROFILE" or Help: "An AWS session token.", Advanced: true, Sensitive: true, + }, { + Name: "role_arn", + Help: `ARN of the IAM role to assume. + +Leave blank if not using assume role.`, + Advanced: true, + }, { + Name: "role_session_name", + Help: `Session name for assumed role. + +If empty, a session name will be generated automatically.`, + Advanced: true, + }, { + Name: "role_session_duration", + Help: `Session duration for assumed role. + +If empty, the default session duration will be used.`, + Advanced: true, + }, { + Name: "role_external_id", + Help: `External ID for assumed role. + +Leave blank if not using an external ID.`, + Advanced: true, }, { Name: "upload_concurrency", Help: `Concurrency for multipart uploads and copies. @@ -927,6 +953,10 @@ type Options struct { SharedCredentialsFile string `config:"shared_credentials_file"` Profile string `config:"profile"` SessionToken string `config:"session_token"` + RoleARN string `config:"role_arn"` + RoleSessionName string `config:"role_session_name"` + RoleSessionDuration fs.Duration `config:"role_session_duration"` + RoleExternalID string `config:"role_external_id"` UploadConcurrency int `config:"upload_concurrency"` ForcePathStyle bool `config:"force_path_style"` V2Auth bool `config:"v2_auth"` @@ -1290,6 +1320,34 @@ func s3Connection(ctx context.Context, opt *Options, client *http.Client) (s3Cli opt.Region = "us-east-1" } + // Handle assume role if RoleARN is specified + if opt.RoleARN != "" { + fs.Debugf(nil, "Using assume role with ARN: %s", opt.RoleARN) + + // Set region for the config before creating STS client + awsConfig.Region = opt.Region + + // Create STS client using the base credentials + stsClient := sts.NewFromConfig(awsConfig) + + // Configure AssumeRole options + assumeRoleOptions := func(aro *stscreds.AssumeRoleOptions) { + // Set session name if provided, otherwise use a default + if opt.RoleSessionName != "" { + aro.RoleSessionName = opt.RoleSessionName + } + if opt.RoleSessionDuration != 0 { + aro.Duration = time.Duration(opt.RoleSessionDuration) + } + if opt.RoleExternalID != "" { + aro.ExternalID = &opt.RoleExternalID + } + } + + // Create AssumeRole credentials provider + awsConfig.Credentials = stscreds.NewAssumeRoleProvider(stsClient, opt.RoleARN, assumeRoleOptions) + } + provider = loadProvider(opt.Provider) if provider == nil { fs.Logf("s3", "s3 provider %q not known - please set correctly", opt.Provider) diff --git a/docs/content/s3.md b/docs/content/s3.md index f16779bcc..ecd5473b2 100644 --- a/docs/content/s3.md +++ b/docs/content/s3.md @@ -745,6 +745,68 @@ If none of these option actually end up providing `rclone` with AWS credentials then S3 interaction will be non-authenticated (see the [anonymous access](#anonymous-access) section for more info). +#### Assume Role (Cross-Account Access) + +If you need to access S3 resources in a different AWS account, you can use IAM role assumption. +This is useful for cross-account access scenarios where you have credentials in one account +but need to access resources in another account. + +To use assume role, configure the following parameters: + +- `role_arn` - The ARN (Amazon Resource Name) of the IAM role to assume in the target account. + Format: `arn:aws:iam::ACCOUNT-ID:role/ROLE-NAME` +- `role_session_name` (optional) - A name for the assumed role session. If not specified, + rclone will generate one automatically. +- `role_session_duration` (optional) - Duration for which the assumed role credentials are valid. + If not specified, AWS default duration will be used (typically 1 hour). +- `role_external_id` (optional) - An external ID required by the role's trust policy for additional security. + This is typically used when the role is accessed by a third party. + +The assume role feature works with both direct credentials (`env_auth = false`) and environment-based +authentication (`env_auth = true`). Rclone will first authenticate using the base credentials, then +use those credentials to assume the specified role. + +Example configuration for cross-account access: + +``` +[s3-cross-account] +type = s3 +provider = AWS +env_auth = true +region = us-east-1 +role_arn = arn:aws:iam::123456789012:role/CrossAccountS3Role +role_session_name = rclone-session +role_external_id = unique-role-external-id-12345 +``` + +In this example: +- Base credentials are obtained from the environment (IAM role, credentials file, or environment variables) +- These credentials are then used to assume the role `CrossAccountS3Role` in account `123456789012` +- An external ID is provided for additional security as required by the role's trust policy + +The target role's trust policy in the destination account must allow the source account or user to assume it. +Example trust policy: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::SOURCE-ACCOUNT-ID:root" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalID": "unique-role-external-id-12345" + } + } + } + ] +} +``` + ### S3 Permissions When using the `sync` subcommand of `rclone` the following minimum diff --git a/go.mod b/go.mod index 60805a4cf..3440bb78f 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.18.21 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 github.com/aws/smithy-go v1.23.2 github.com/buengese/sgzip v0.1.1 github.com/cloudinary/cloudinary-go/v2 v2.13.0 @@ -133,7 +134,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect