diff --git a/.gitignore b/.gitignore index b7201c26b..c8c3aa69a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.idea /restic /restic.exe /.vagrant diff --git a/changelog/unreleased/issue-4472 b/changelog/unreleased/issue-4472 new file mode 100644 index 000000000..97553f946 --- /dev/null +++ b/changelog/unreleased/issue-4472 @@ -0,0 +1,14 @@ +Enhancement: Allow AWS Assume Role to be used for S3 backend + +Previously only credentials discovered via the Minio Click discovery methods +would be used to authenticate. However there are many circumstances where the +discovered credentials have lower permissions and need to assume a specific role. + +New Environment Variables: + +- RESTIC_AWS_ASSUME_ROLE_ARN +- RESTIC_AWS_ASSUME_ROLE_SESSION_NAME +- RESTIC_AWS_ASSUME_ROLE_EXTERNAL_ID +- RESTIC_AWS_ASSUME_ROLE_REGION (if need to override from us-east-1) +- RESTIC_AWS_ASSUME_ROLE_POLICY +- RESTIC_AWS_ASSUME_ROLE_STS_ENDPOINT \ No newline at end of file diff --git a/doc/040_backup.rst b/doc/040_backup.rst index acafe2694..3de8ef554 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -628,6 +628,10 @@ environment variables. The following lists these environment variables: AWS_DEFAULT_REGION Amazon S3 default region AWS_PROFILE Amazon credentials profile (alternative to specifying key and region) AWS_SHARED_CREDENTIALS_FILE Location of the AWS CLI shared credentials file (default: ~/.aws/credentials) + RESTIC_AWS_ASSUME_ROLE_ARN Amazon IAM Role ARN to assume using discovered credentials + RESTIC_AWS_ASSUME_ROLE_SESSION_NAME Session Name to use with the role assumption + RESTIC_AWS_ASSUME_ROLE_EXTERNAL_ID External ID to use with the role assumption + RESTIC_AWS_ASSUME_ROLE_REGION Region to use for IAM calls for the role assumption AZURE_ACCOUNT_NAME Account name for Azure AZURE_ACCOUNT_KEY Account key for Azure diff --git a/internal/backend/s3/s3.go b/internal/backend/s3/s3.go index 98879d0df..ff81a05d6 100644 --- a/internal/backend/s3/s3.go +++ b/internal/backend/s3/s3.go @@ -51,40 +51,9 @@ func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, erro minio.MaxRetry = int(cfg.MaxRetries) } - // Chains all credential types, in the following order: - // - Static credentials provided by user - // - AWS env vars (i.e. AWS_ACCESS_KEY_ID) - // - Minio env vars (i.e. MINIO_ACCESS_KEY) - // - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials) - // - Minio creds file (i.e. MINIO_SHARED_CREDENTIALS_FILE or ~/.mc/config.json) - // - IAM profile based credentials. (performs an HTTP - // call to a pre-defined endpoint, only valid inside - // configured ec2 instances) - creds := credentials.NewChainCredentials([]credentials.Provider{ - &credentials.EnvAWS{}, - &credentials.Static{ - Value: credentials.Value{ - AccessKeyID: cfg.KeyID, - SecretAccessKey: cfg.Secret.Unwrap(), - }, - }, - &credentials.EnvMinio{}, - &credentials.FileAWSCredentials{}, - &credentials.FileMinioClient{}, - &credentials.IAM{ - Client: &http.Client{ - Transport: http.DefaultTransport, - }, - }, - }) - - c, err := creds.Get() + creds, err := getCredentials(cfg) if err != nil { - return nil, errors.Wrap(err, "creds.Get") - } - - if c.SignerType == credentials.SignatureAnonymous { - debug.Log("using anonymous access for %#v", cfg.Endpoint) + return nil, errors.Wrap(err, "s3.getCredentials") } options := &minio.Options{ @@ -125,6 +94,93 @@ func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, erro return be, nil } +// getCredentials -- runs through the various credential types and returns the first one that works. +// additionally if the user has specified a role to assume, it will do that as well. +func getCredentials(cfg Config) (*credentials.Credentials, error) { + // Chains all credential types, in the following order: + // - Static credentials provided by user + // - AWS env vars (i.e. AWS_ACCESS_KEY_ID) + // - Minio env vars (i.e. MINIO_ACCESS_KEY) + // - AWS creds file (i.e. AWS_SHARED_CREDENTIALS_FILE or ~/.aws/credentials) + // - Minio creds file (i.e. MINIO_SHARED_CREDENTIALS_FILE or ~/.mc/config.json) + // - IAM profile based credentials. (performs an HTTP + // call to a pre-defined endpoint, only valid inside + // configured ec2 instances) + creds := credentials.NewChainCredentials([]credentials.Provider{ + &credentials.Static{ + Value: credentials.Value{ + AccessKeyID: cfg.KeyID, + SecretAccessKey: cfg.Secret.Unwrap(), + }, + }, + &credentials.EnvAWS{}, + &credentials.EnvMinio{}, + &credentials.FileAWSCredentials{}, + &credentials.FileMinioClient{}, + &credentials.IAM{ + Client: &http.Client{ + Transport: http.DefaultTransport, + }, + }, + }) + + c, err := creds.Get() + if err != nil { + return nil, errors.Wrap(err, "creds.Get") + } + + if c.SignerType == credentials.SignatureAnonymous { + debug.Log("using anonymous access for %#v", cfg.Endpoint) + } + + roleArn := os.Getenv("RESTIC_AWS_ASSUME_ROLE_ARN") + if roleArn != "" { + // use the region provided by the configuration by default + awsRegion := cfg.Region + // allow the region to be overridden if for some reason it is required + if len(os.Getenv("RESTIC_AWS_ASSUME_ROLE_REGION")) > 0 { + awsRegion = os.Getenv("RESTIC_AWS_ASSUME_ROLE_REGION") + } + + sessionName := os.Getenv("RESTIC_AWS_ASSUME_ROLE_SESSION_NAME") + externalID := os.Getenv("RESTIC_AWS_ASSUME_ROLE_EXTERNAL_ID") + policy := os.Getenv("RESTIC_AWS_ASSUME_ROLE_POLICY") + stsEndpoint := os.Getenv("RESTIC_AWS_ASSUME_ROLE_STS_ENDPOINT") + + if stsEndpoint == "" { + if len(awsRegion) > 0 { + if strings.HasPrefix(awsRegion, "cn-") { + stsEndpoint = "https://sts." + awsRegion + ".amazonaws.com.cn" + } else { + stsEndpoint = "https://sts." + awsRegion + ".amazonaws.com" + } + } else { + stsEndpoint = "https://sts.amazonaws.com" + } + } + + opts := credentials.STSAssumeRoleOptions{ + RoleARN: roleArn, + AccessKey: c.AccessKeyID, + SecretKey: c.SecretAccessKey, + SessionToken: c.SessionToken, + RoleSessionName: sessionName, + ExternalID: externalID, + Policy: policy, + } + if len(awsRegion) > 0 { + opts.Location = awsRegion + } + + creds, err = credentials.NewSTSAssumeRole(stsEndpoint, opts) + if err != nil { + return nil, errors.Wrap(err, "creds.AssumeRole") + } + } + + return creds, nil +} + // Open opens the S3 backend at bucket and region. The bucket is created if it // does not exist yet. func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (backend.Backend, error) {