S3 backend add account ID whitelisting arguments (#760)

Signed-off-by: tomasmik <tomasmik@protonmail.com>
This commit is contained in:
Tomas 2023-10-20 15:03:54 +03:00 committed by GitHub
parent 080d89c9b6
commit e1b3b4ff82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 0 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 account whitelisting using the `forbidden_account_ids` and `allowed_account_ids` arguments. ([#699](https://github.com/opentofu/opentofu/issues/699))
## Previous Releases

View File

@ -297,6 +297,16 @@ func (b *Backend) ConfigSchema() *configschema.Block {
},
},
},
"forbidden_account_ids": {
Type: cty.Set(cty.String),
Optional: true,
Description: "List of forbidden AWS account IDs.",
},
"allowed_account_ids": {
Type: cty.Set(cty.String),
Optional: true,
Description: "List of allowed AWS account IDs.",
},
},
}
}
@ -433,6 +443,11 @@ func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics)
}
}
validateAttributesConflict(
cty.GetAttrPath("allowed_account_ids"),
cty.GetAttrPath("forbidden_account_ids"),
)(obj, cty.Path{}, &diags)
return obj, diags
}
@ -569,6 +584,14 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
cfg.SharedConfigFiles = val
}
if val, ok := stringSliceAttrOk(obj, "allowed_account_ids"); ok {
cfg.AllowedAccountIds = val
}
if val, ok := stringSliceAttrOk(obj, "forbidden_account_ids"); ok {
cfg.ForbiddenAccountIds = val
}
ctx := context.TODO()
_, awsConfig, awsDiags := awsbase.GetAwsConfig(ctx, cfg)
@ -580,6 +603,10 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
))
}
if d := verifyAllowedAccountID(ctx, awsConfig, cfg); len(d) != 0 {
diags = diags.Append(d)
}
if diags.HasErrors() {
return diags
}
@ -593,6 +620,28 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
return diags
}
func verifyAllowedAccountID(ctx context.Context, awsConfig aws.Config, cfg *awsbase.Config) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
accountID, _, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, awsConfig, cfg)
for _, d := range awsDiags {
diags = diags.Append(tfdiags.Sourceless(
baseSeverityToTofuSeverity(d.Severity()),
fmt.Sprintf("Retrieving AWS account details: %s", d.Summary()),
d.Detail(),
))
}
err := cfg.VerifyAccountIDAllowed(accountID)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid account ID",
err.Error(),
))
}
return diags
}
func getDynamoDBConfig(obj cty.Value) func(options *dynamodb.Options) {
return func(options *dynamodb.Options) {
if v, ok := stringAttrDefaultEnvVarOk(obj, "dynamodb_endpoint", "AWS_DYNAMODB_ENDPOINT", "AWS_ENDPOINT_URL_DYNAMODB"); ok {

View File

@ -156,6 +156,9 @@ func TestBackendConfig_Authentication(t *testing.T) {
}{
"empty config": {
config: map[string]any{},
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
ValidateDiags: ExpectDiagsMatching(
tfdiags.Error,
equalsMatcher("No valid credential sources found"),
@ -175,6 +178,23 @@ func TestBackendConfig_Authentication(t *testing.T) {
ValidateDiags: ExpectNoDiags,
},
"config AccessKey forbidden account": {
config: map[string]any{
"access_key": servicemocks.MockStaticAccessKey,
"secret_key": servicemocks.MockStaticSecretKey,
"forbidden_account_ids": []any{"222222222222"},
},
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
ExpectedCredentialsValue: mockdata.MockStaticCredentials,
ValidateDiags: ExpectDiagsMatching(
tfdiags.Error,
equalsMatcher("Invalid account ID"),
equalsMatcher("AWS account ID not allowed: 222222222222"),
),
},
"config Profile shared credentials profile aws_access_key_id": {
config: map[string]any{
"profile": "SharedCredentialsProfile",
@ -545,6 +565,9 @@ region = us-east-1
EnvironmentVariables: map[string]string{
"AWS_PROFILE": "no-such-profile",
},
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
SharedCredentialsFile: `
[some-profile]
aws_access_key_id = DefaultSharedCredentialsAccessKey
@ -566,6 +589,9 @@ aws_secret_access_key = DefaultSharedCredentialsSecretKey
aws_access_key_id = DefaultSharedCredentialsAccessKey
aws_secret_access_key = DefaultSharedCredentialsSecretKey
`,
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
ValidateDiags: ExpectDiagsMatching(
tfdiags.Error,
equalsMatcher("failed to get shared config profile, no-such-profile"),
@ -603,6 +629,9 @@ aws_secret_access_key = ProfileSharedCredentialsSecretKey
"AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey,
"AWS_PROFILE": "no-such-profile",
},
MockStsEndpoints: []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
},
SharedCredentialsFile: `
[some-profile]
aws_access_key_id = DefaultSharedCredentialsAccessKey
@ -1766,6 +1795,13 @@ region = us-west-2
defer closeEc2Metadata()
}
sts := servicemocks.MockAwsApiServer("STS", []*servicemocks.MockEndpoint{
servicemocks.MockStsGetCallerIdentityValidEndpoint,
})
defer sts.Close()
tc.config["sts_endpoint"] = sts.URL
if tc.SharedConfigurationFile != "" {
file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file")

View File

@ -713,6 +713,16 @@ func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
}),
expectedErr: `Only one of "kms_key_id" and "sse_customer_key" can be set`,
},
"allowed forbidden account ids conflict": {
config: cty.ObjectVal(map[string]cty.Value{
"bucket": cty.StringVal("test"),
"key": cty.StringVal("test"),
"region": cty.StringVal("us-west-2"),
"allowed_account_ids": cty.SetVal([]cty.Value{cty.StringVal("111111111111")}),
"forbidden_account_ids": cty.SetVal([]cty.Value{cty.StringVal("111111111111")}),
}),
expectedErr: "Invalid Attribute Combination: Only one of allowed_account_ids, forbidden_account_ids can be set.",
},
}
for name, tc := range cases {

View File

@ -170,6 +170,8 @@ The following configuration is optional:
* `skip_metadata_api_check` - (Optional) Skip usage of EC2 Metadata API.
* `sts_endpoint` - (Optional) Custom endpoint for the AWS Security Token Service (STS) API. This can also be sourced from the `AWS_STS_ENDPOINT` environment variable.
* `token` - (Optional) Multi-Factor Authentication (MFA) token. This can also be sourced from the `AWS_SESSION_TOKEN` environment variable.
* `allowed_account_ids` (Optional): A list of permitted AWS account IDs to safeguard against accidental disruption of a live environment. This option conflicts with `forbidden_account_ids`.
* `forbidden_account_ids` (Optional): A list of prohibited AWS account IDs to prevent unintentional disruption of a live environment. This option conflicts with `allowed_account_ids`.
* `use_legacy_workflow` - (Optional) Prefer environment variables for legacy authentication; default is 'true.' This method doesn't match AWS CLI or SDK authentication and will be removed in the future.
#### Assume Role Configuration