Backend/s3: Add support for assume_role_with_web_identity block (#759)

Signed-off-by: Marcin Białoń <mbialon@spacelift.io>
This commit is contained in:
Marcin Białoń 2023-10-24 15:51:32 +02:00 committed by GitHub
parent a6a54c3777
commit 4d38f26bf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 518 additions and 67 deletions

View File

@ -53,6 +53,7 @@ S3 BACKEND:
* Adds support for `shared_config_files` and `shared_credentials_files` arguments and deprecates the `shared_credentials_file` argument. ([#690](https://github.com/opentofu/opentofu/issues/690))
* Arguments associated with assuming an IAM role were moved into a nested block - `assume_role`.
This deprecates the arguments `role_arn`, `session_name`, `external_id`, `assume_role_duration_seconds`, `assume_role_policy`, `assume_role_policy_arns`, `assume_role_tags`, and `assume_role_transitive_tag_keys`. ([#747](https://github.com/opentofu/opentofu/issues/747))
* Adds support for the `assume_role_with_web_identity` block. ([#689](https://github.com/opentofu/opentofu/issues/689))
* Adds support for account whitelisting using the `forbidden_account_ids` and `allowed_account_ids` arguments. ([#699](https://github.com/opentofu/opentofu/issues/699))
* Adds the `custom_ca_bundle` argument. ([#689](https://github.com/opentofu/opentofu/issues/689))

View File

@ -180,54 +180,46 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
Description: "The external ID to use when assuming the role",
Deprecated: true,
},
"assume_role_duration_seconds": {
Type: cty.Number,
Optional: true,
Description: "Seconds to restrict the assume role session duration.",
Deprecated: true,
},
"assume_role_policy": {
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
Deprecated: true,
},
"assume_role_policy_arns": {
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
Deprecated: true,
},
"assume_role_tags": {
Type: cty.Map(cty.String),
Optional: true,
Description: "Assume role session tags.",
Deprecated: true,
},
"assume_role_transitive_tag_keys": {
Type: cty.Set(cty.String),
Optional: true,
Description: "Assume role session tag keys to pass to any subsequent sessions.",
Deprecated: true,
},
"workspace_key_prefix": {
Type: cty.String,
Optional: true,
Description: "The prefix applied to the non-default state path inside the bucket.",
},
"force_path_style": {
Type: cty.Bool,
Optional: true,
Description: "Force s3 to use path style api.",
},
"max_retries": {
Type: cty.Number,
Optional: true,
@ -302,6 +294,49 @@ func (b *Backend) ConfigSchema(context.Context) *configschema.Block {
},
},
},
"assume_role_with_web_identity": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"role_arn": {
Type: cty.String,
Optional: true,
Description: "The Amazon Resource Name (ARN) role to assume.",
},
"web_identity_token": {
Type: cty.String,
Optional: true,
Sensitive: true,
Description: "The OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.",
},
"web_identity_token_file": {
Type: cty.String,
Optional: true,
Description: "The path to a file which contains an OAuth 2.0 access token or OpenID Connect ID token that is provided by the identity provider.",
},
"session_name": {
Type: cty.String,
Optional: true,
Description: "The name applied to this assume-role session.",
},
"policy": {
Type: cty.String,
Optional: true,
Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.",
},
"policy_arns": {
Type: cty.Set(cty.String),
Optional: true,
Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.",
},
"duration": {
Type: cty.String,
Optional: true,
Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.",
},
},
},
},
"forbidden_account_ids": {
Type: cty.Set(cty.String),
Optional: true,
@ -448,6 +483,10 @@ func (b *Backend) PrepareConfig(ctx context.Context, obj cty.Value) (cty.Value,
}
}
if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() {
diags = diags.Append(validateAssumeRoleWithWebIdentity(val, cty.GetAttrPath("assume_role_with_web_identity")))
}
validateAttributesConflict(
cty.GetAttrPath("allowed_account_ids"),
cty.GetAttrPath("forbidden_account_ids"),
@ -583,6 +622,10 @@ func (b *Backend) Configure(ctx context.Context, obj cty.Value) tfdiags.Diagnost
cfg.AssumeRole = configureAssumeRole(obj)
}
if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() {
cfg.AssumeRoleWithWebIdentity = configureAssumeRoleWithWebIdentity(val)
}
if val, ok := stringSliceAttrDefaultEnvVarOk(obj, "shared_credentials_files", "AWS_SHARED_CREDENTIALS_FILE"); ok {
cfg.SharedCredentialsFiles = val
}
@ -732,6 +775,27 @@ func configureAssumeRole(obj cty.Value) *awsbase.AssumeRole {
return &assumeRole
}
func configureAssumeRoleWithWebIdentity(obj cty.Value) *awsbase.AssumeRoleWithWebIdentity {
cfg := &awsbase.AssumeRoleWithWebIdentity{
RoleARN: stringAttrDefaultEnvVar(obj, "role_arn", "AWS_ROLE_ARN"),
Policy: stringAttr(obj, "policy"),
PolicyARNs: stringSliceAttr(obj, "policy_arns"),
SessionName: stringAttrDefaultEnvVar(obj, "session_name", "AWS_ROLE_SESSION_NAME"),
WebIdentityToken: stringAttrDefaultEnvVar(obj, "web_identity_token", "AWS_WEB_IDENTITY_TOKEN"),
WebIdentityTokenFile: stringAttrDefaultEnvVar(obj, "web_identity_token_file", "AWS_WEB_IDENTITY_TOKEN_FILE"),
}
if val, ok := stringAttrOk(obj, "duration"); ok {
d, err := time.ParseDuration(val)
if err != nil {
// This should never happen because the schema should have
// already validated the duration.
panic(fmt.Sprintf("invalid duration %q: %s", val, err))
}
cfg.Duration = d
}
return cfg
}
func stringValue(val cty.Value) string {
v, _ := stringValueOk(val)
return v
@ -761,6 +825,11 @@ func stringAttrDefault(obj cty.Value, name, def string) string {
}
}
func stringSliceValue(val cty.Value) []string {
v, _ := stringSliceValueOk(val)
return v
}
func stringSliceValueOk(val cty.Value) ([]string, bool) {
if val.IsNull() {
return nil, false
@ -773,6 +842,10 @@ func stringSliceValueOk(val cty.Value) ([]string, bool) {
return v, true
}
func stringSliceAttr(obj cty.Value, name string) []string {
return stringSliceValue(obj.GetAttr(name))
}
func stringSliceAttrOk(obj cty.Value, name string) ([]string, bool) {
return stringSliceValueOk(obj.GetAttr(name))
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"testing"
@ -1617,6 +1618,290 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
}
}
func TestBackendConfig_Authentication_AssumeRoleWithWebIdentity(t *testing.T) {
testCases := map[string]struct {
config map[string]any
SetConfig bool
ExpandEnvVars bool
EnvironmentVariables map[string]string
SetTokenFileEnvironmentVariable bool
SharedConfigurationFile string
SetSharedConfigurationFile bool
ExpectedCredentialsValue aws.Credentials
ValidateDiags diagsValidator
MockStsEndpoints []*servicemocks.MockEndpoint
}{
"config with inline token": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
"web_identity_token": servicemocks.MockWebIdentityToken,
},
},
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"config with token file": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
},
},
SetConfig: true,
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"config with expanded path": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
},
},
SetConfig: true,
ExpandEnvVars: true,
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"envvar": {
config: map[string]any{},
EnvironmentVariables: map[string]string{
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
},
SetTokenFileEnvironmentVariable: true,
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"shared configuration file": {
config: map[string]any{},
SharedConfigurationFile: fmt.Sprintf(`
[default]
role_arn = %[1]s
role_session_name = %[2]s
`, servicemocks.MockStsAssumeRoleWithWebIdentityArn, servicemocks.MockStsAssumeRoleWithWebIdentitySessionName),
SetSharedConfigurationFile: true,
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"config overrides envvar": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
"web_identity_token": servicemocks.MockWebIdentityToken,
},
},
EnvironmentVariables: map[string]string{
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn,
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName,
"AWS_WEB_IDENTITY_TOKEN_FILE": "no-such-file",
},
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"envvar overrides shared configuration": {
config: map[string]any{},
EnvironmentVariables: map[string]string{
"AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
},
SetTokenFileEnvironmentVariable: true,
SharedConfigurationFile: fmt.Sprintf(`
[default]
role_arn = %[1]s
role_session_name = %[2]s
web_identity_token_file = no-such-file
`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName),
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"config overrides shared configuration": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
"web_identity_token": servicemocks.MockWebIdentityToken,
},
},
SharedConfigurationFile: fmt.Sprintf(`
[default]
role_arn = %[1]s
role_session_name = %[2]s
web_identity_token_file = no-such-file
`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName),
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint,
},
},
"with duration": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
"web_identity_token": servicemocks.MockWebIdentityToken,
"duration": "1h",
},
},
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"DurationSeconds": "3600"}),
},
},
"with policy": {
config: map[string]any{
"assume_role_with_web_identity": map[string]any{
"role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn,
"session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName,
"web_identity_token": servicemocks.MockWebIdentityToken,
"policy": "{}",
},
},
ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"Policy": "{}"}),
},
},
}
for name, tc := range testCases {
tc := tc
t.Run(name, func(t *testing.T) {
oldEnv := servicemocks.InitSessionTestEnv()
defer servicemocks.PopEnv(oldEnv)
ctx := context.TODO()
// Populate required fields
tc.config["region"] = "us-east-1"
tc.config["bucket"] = "bucket"
tc.config["key"] = "key"
if tc.ValidateDiags == nil {
tc.ValidateDiags = ExpectNoDiags
}
for k, v := range tc.EnvironmentVariables {
os.Setenv(k, v)
}
ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints)
defer ts.Close()
tc.config["sts_endpoint"] = ts.URL
tempdir, err := os.MkdirTemp("", "temp")
if err != nil {
t.Fatalf("error creating temp dir: %s", err)
}
defer os.Remove(tempdir)
os.Setenv("TMPDIR", tempdir)
tokenFile, err := os.CreateTemp("", "aws-sdk-go-base-web-identity-token-file")
if err != nil {
t.Fatalf("unexpected error creating temporary web identity token file: %s", err)
}
tokenFileName := tokenFile.Name()
defer os.Remove(tokenFileName)
err = os.WriteFile(tokenFileName, []byte(servicemocks.MockWebIdentityToken), 0600)
if err != nil {
t.Fatalf("unexpected error writing web identity token file: %s", err)
}
if tc.ExpandEnvVars {
tmpdir := os.Getenv("TMPDIR")
rel, err := filepath.Rel(tmpdir, tokenFileName)
if err != nil {
t.Fatalf("error making path relative: %s", err)
}
t.Logf("relative: %s", rel)
tokenFileName = filepath.Join("$TMPDIR", rel)
t.Logf("env tempfile: %s", tokenFileName)
}
if tc.SetConfig {
ar := tc.config["assume_role_with_web_identity"].(map[string]any)
ar["web_identity_token_file"] = tokenFileName
}
if tc.SetTokenFileEnvironmentVariable {
os.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenFileName)
}
if tc.SharedConfigurationFile != "" {
file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file")
if err != nil {
t.Fatalf("unexpected error creating temporary shared configuration file: %s", err)
}
defer os.Remove(file.Name())
if tc.SetSharedConfigurationFile {
tc.SharedConfigurationFile += fmt.Sprintf("web_identity_token_file = %s\n", tokenFileName)
}
err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600)
if err != nil {
t.Fatalf("unexpected error writing shared configuration file: %s", err)
}
tc.config["shared_config_files"] = []any{file.Name()}
}
tc.config["skip_credentials_validation"] = true
b, diags := configureBackend(t, tc.config)
tc.ValidateDiags(t, diags)
if diags.HasErrors() {
return
}
credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
if err != nil {
t.Fatalf("Error when requesting credentials: %s", err)
}
if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" {
t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff)
}
})
}
}
func TestBackendConfig_Region(t *testing.T) {
testCases := map[string]struct {
config map[string]any

View File

@ -78,77 +78,50 @@ func validateNestedAssumeRole(obj cty.Value, objPath cty.Path) tfdiags.Diagnosti
}
if val, ok := stringAttrOk(obj, "duration"); ok {
path := objPath.GetAttr("duration")
d, err := time.ParseDuration(val)
if err != nil {
diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err),
path,
))
} else {
min := 15 * time.Minute
max := 12 * time.Hour
if d < min || d > max {
diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val),
path,
))
}
}
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
}
if val, ok := stringAttrOk(obj, "external_id"); ok {
if len(strings.TrimSpace(val)) == 0 {
diags = diags.Append(attributeErrDiag(
"Invalid Value",
"The value cannot be empty or all whitespace",
objPath.GetAttr("external_id"),
))
}
validateNonEmptyString(val, objPath.GetAttr("external_id"), &diags)
}
if val, ok := stringAttrOk(obj, "policy"); ok {
if len(strings.TrimSpace(val)) == 0 {
diags = diags.Append(attributeErrDiag(
"Invalid Value",
"The value cannot be empty or all whitespace",
objPath.GetAttr("policy"),
))
}
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
}
if val, ok := stringAttrOk(obj, "session_name"); ok {
if len(strings.TrimSpace(val)) == 0 {
diags = diags.Append(attributeErrDiag(
"Invalid Value",
"The value cannot be empty or all whitespace",
objPath.GetAttr("session_name"),
))
}
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
}
if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
for _, v := range val {
arn, err := arn.Parse(v)
if err != nil {
diags = diags.Append(attributeErrDiag(
"Invalid ARN",
fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err),
objPath.GetAttr("policy_arns"),
))
break
} else {
if !strings.HasPrefix(arn.Resource, "policy/") {
diags = diags.Append(attributeErrDiag(
"Invalid IAM Policy ARN",
fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val),
objPath.GetAttr("policy_arns"),
))
}
}
}
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
}
return diags
}
func validateAssumeRoleWithWebIdentity(obj cty.Value, objPath cty.Path) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
validateAttributesConflict(
cty.GetAttrPath("web_identity_token"),
cty.GetAttrPath("web_identity_token_file"),
)(obj, objPath, &diags)
if val, ok := stringAttrOk(obj, "session_name"); ok {
validateNonEmptyString(val, objPath.GetAttr("session_name"), &diags)
}
if val, ok := stringAttrOk(obj, "policy"); ok {
validateNonEmptyString(val, objPath.GetAttr("policy"), &diags)
}
if val, ok := stringSliceAttrOk(obj, "policy_arns"); ok {
validatePolicyARNSlice(val, objPath.GetAttr("policy_arns"), &diags)
}
if val, ok := stringAttrOk(obj, "duration"); ok {
validateDuration(val, 15*time.Minute, 12*time.Hour, objPath.GetAttr("duration"), &diags)
}
return diags
@ -222,3 +195,54 @@ func attributeErrDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnos
func attributeWarningDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic {
return tfdiags.AttributeValue(tfdiags.Warning, summary, detail, attrPath.Copy())
}
func validateNonEmptyString(val string, path cty.Path, diags *tfdiags.Diagnostics) {
if len(strings.TrimSpace(val)) == 0 {
*diags = diags.Append(attributeErrDiag(
"Invalid Value",
"The value cannot be empty or all whitespace",
path,
))
}
}
func validatePolicyARNSlice(val []string, path cty.Path, diags *tfdiags.Diagnostics) {
for _, v := range val {
arn, err := arn.Parse(v)
if err != nil {
*diags = diags.Append(attributeErrDiag(
"Invalid ARN",
fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err),
path,
))
break
} else {
if !strings.HasPrefix(arn.Resource, "policy/") {
*diags = diags.Append(attributeErrDiag(
"Invalid IAM Policy ARN",
fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val),
path,
))
}
}
}
}
func validateDuration(val string, min, max time.Duration, path cty.Path, diags *tfdiags.Diagnostics) {
d, err := time.ParseDuration(val)
if err != nil {
*diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err),
path,
))
return
}
if (min > 0 && d < min) || (max > 0 && d > max) {
*diags = diags.Append(attributeErrDiag(
"Invalid Duration",
fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val),
path,
))
}
}

View File

@ -227,6 +227,74 @@ terraform {
}
```
#### Assume Role With Web Identity Configuration
The following `assume_role_with_web_identity` configuration block is optional:
* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM Role to assume.
Can also be set with the `AWS_ROLE_ARN` environment variable.
* `duration` - (Optional) The duration individual credentials will be valid.
Credentials are automatically renewed up to the maximum defined by the AWS account.
Specified using the format `<hours>h<minutes>m<seconds>s` with any unit being optional.
For example, an hour and a half can be specified as `1h30m` or `90m`.
Must be between 15 minutes (15m) and 12 hours (12h).
* `policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.
* `policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.
* `session_name` - (Optional) Session name to use when assuming the role.
Can also be set with the `AWS_ROLE_SESSION_NAME` environment variable.
* `web_identity_token` - (Optional) The value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.
One of `web_identity_token` or `web_identity_token_file` is required.
* `web_identity_token_file` - (Optional) File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.
One of `web_identity_token_file` or `web_identity_token` is required.
Can also be set with the `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable.
```hcl
terraform {
backend "s3" {
bucket = "mybucket"
key = "my/key.tfstate"
region = "us-east-1"
assume_role_with_web_identity = {
role_arn = "arn:aws:iam::ACCOUNT-ID:role/Opentofu"
web_identity_token = "<token value>"
}
}
}
```
It's possible to constrain the assumed role by providing a policy.
```hcl
terraform {
backend "s3" {
bucket = "mybucket"
key = "my/key.tfstate"
region = "us-east-1"
assume_role_with_web_identity = {
role_arn = "arn:aws:iam::ACCOUNT-ID:role/Opentofu"
web_identity_token = "<token value>"
policy = <<-JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::mybucket/*",
"arn:aws:s3:::mybucket"
]
}
]
}
JSON
}
}
}
```
### S3 State Storage
The following configuration is required: