From 185ebc19756c65a40068bc60a37f128d77ea271f Mon Sep 17 00:00:00 2001 From: Stephen Hoekstra Date: Mon, 27 Jan 2025 17:05:46 +0100 Subject: [PATCH] chore(permission-sets)!: replace account ID with account name in `aws_ssoadmin_account_assignment` (#219) Previously, resource keys used the account ID, which caused issues during the plan phase when account IDs were not yet known (when accounts were being created in the same run). This change updates the resource key to use the account name instead, as this does not have to be a computed value. Updates include: - Adjusted the `var.assignments` to be a list of objects - Updated examples in the README to reflect the new structure - Enhanced the variable description for clarity BREAKING CHANGE: this commit changes the structure of `var.aws_sso_permission_sets` in the root module and `var.assignments` in the `permission-set` sub-module. In addition the `aws_ssoadmin_account_assignment` resources will be recreated using the new format. Signed-off-by: Stephen Hoekstra --- README.md | 110 +++++++++--------- UPGRADING.md | 167 ++++++++++++++++++---------- modules/permission-set/main.tf | 17 ++- modules/permission-set/variables.tf | 8 +- variables.tf | 6 +- 5 files changed, 188 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 38f4c0eb..d34ce416 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ The SBP AWS Landing Zone consists of 3 repositories: - [MCAF Account Vending Machine (AVM) module](https://github.com/schubergphilis/terraform-aws-mcaf-avm): providing an AWS AVM. This module sets up an AWS account with one or more Terraform Cloud/Enterprise (TFE) workspace(s) backed by a VCS project - [MCAF Account Baseline module](https://github.com/schubergphilis/terraform-aws-mcaf-account-baseline): optional module providing baseline configuration for AWS accounts - ## Pre-Requisites > [!IMPORTANT] > Before deploying this module, ensure the following pre-requisites are met: +> > - AWS Control Tower is deployed in the `core-management` account. > - AWS Control Tower governed regions include at least `us-east-1` (and your designated home region). @@ -30,24 +30,27 @@ Refer to [examples/basic](examples/basic/main.tf) for an example of minimal setu The mandatory `regions.home_region` variable specifies the AWS Control Tower home region. This must match the region defined in your AWS provider that deploys this module. -To find your home region: -1. Log in to the **core-management account**. -2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. +To find your home region: + +1. Log in to the **core-management account**. +2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. 3. The home region is listed under **Home Region**. **Linked Regions** The optional `regions.linked_regions` variable defines the AWS Control Tower governed regions. This module ensures proper configuration of AWS Security Hub and AWS Config for all specified linked regions to collect data from them. -To find your linked regions: -1. Log in to the **core-management account**. -2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. +To find your linked regions: + +1. Log in to the **core-management account**. +2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. 3. Linked regions are listed under **Landing Zone Regions**. -*Note:* By default, `us-east-1` is included as a linked region to ensure data collection from global services. To restrict deployment of non-global resources in this region, use the `allowed_regions` functionality described in the section below. +> [!NOTE] +> By default, `us-east-1` is included as a linked region to ensure data collection from global services. To restrict deployment of non-global resources in this region, use the `allowed_regions` functionality described in the section below. > [!IMPORTANT] -> All specified linked regions need to be an AWS Control Tower governed region. This ensures that an AWS Config recorder is enabled by AWS Control Tower in all governed regions. AWS Security Hub will only function correctly if an AWS Config recorder exists in all linked regions. +> All specified linked regions need to be an AWS Control Tower governed region. This ensures that an AWS Config recorder is enabled by AWS Control Tower in all governed regions. AWS Security Hub will only function correctly if an AWS Config recorder exists in all linked regions. **Allowed Regions** @@ -57,8 +60,8 @@ The optional `regions.allowed_regions` variable defines the allowed regions with **Scenario 1: Home region only (no deployment in other regions)** -- **Home region:** `eu-central-1` -- **Requirement:** Prevent deployment in all other regions. +- **Home region:** `eu-central-1` +- **Requirement:** Prevent deployment in all other regions You need to configure the `regions` variable as follows: @@ -69,12 +72,13 @@ regions = { } ``` -*Note:* Ensure that `us-east-1` is included as a governed region in AWS Control Tower since the `linked_region` variable defaults to this value. +> [!NOTE] +> Ensure that `us-east-1` is included as a governed region in AWS Control Tower since the `linked_region` variable defaults to this value. **Scenario 2: Home region with additional governed regions** -- **Home region:** `eu-central-1` -- **Requirement:** Also allow deploying resources in `eu-west-1`. +- **Home region:** `eu-central-1` +- **Requirement:** Also allow deploying resources in `eu-west-1` You need to configure the `regions` variable as follows: @@ -115,7 +119,8 @@ By default, you have to create the email addresses for the accounts created usin By default, all CloudTrail logs will be stored in a S3 bucket in the `logging` account of your AWS Organization. However, this module also supports creating an additional CloudTrail configuration to publish logs to any S3 bucket chosen by you. This trail will be set at the Organization level, meaning that logs from all accounts will be published to the provided bucket. -NOTE: Before enabling this feature, make sure that the [bucket policy authorizing CloudTrail to deliver logs](https://aws.amazon.com/premiumsupport/knowledge-center/change-cloudtrail-trail/) is in place and that you have enabled [trusted access between AWS Organizations and CloudTrail](https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-cloudtrail.html#integrate-enable-ta-cloudtrail). If these two steps are not in place, Terraform will fail to create the trail. +> [!NOTE] +> Before enabling this feature, make sure that the [bucket policy authorizing CloudTrail to deliver logs](https://aws.amazon.com/premiumsupport/knowledge-center/change-cloudtrail-trail/) is in place and that you have enabled [trusted access between AWS Organizations and CloudTrail](https://docs.aws.amazon.com/organizations/latest/userguide/services-that-can-integrate-cloudtrail.html#integrate-enable-ta-cloudtrail). If these two steps are not in place, Terraform will fail to create the trail. Example: @@ -132,7 +137,8 @@ This module provisions by default a set of basic AWS Config Rules. In order to a If you would like to authorize other accounts to aggregate AWS Config data, the account IDs can also be passed via the variable `aws_config.aggregator_account_ids`. -NOTE: This module already authorizes the `audit` account to aggregate Config data from all other accounts in the organization, so there is no need to specify the `audit` account ID in the `aggregator_account_ids` list. +> [!NOTE] +> This module already authorizes the `audit` account to aggregate Config data from all other accounts in the organization, so there is no need to specify the `audit` account ID in the `aggregator_account_ids` list. Example: @@ -199,7 +205,7 @@ This module supports managing AWS SSO resources to control user access to all ac This feature can be controlled via the `aws_sso_permission_sets` variable by passing a map (key-value pair) where every key corresponds to an AWS SSO Permission Set name and the value follows the structure below: -- `assignments`: list of maps (key-value pair) of AWS Account IDs as keys and a list of AWS SSO Group names that should have access to the account using the permission set defined +- `assignments`: list of objects, where each object represents an AWS account with its `account_id`, `account_name`, and a list of AWS SSO Group names (`sso_groups`) that should have access to the account using the defined permission set - `inline_policy`: valid IAM policy in JSON format (maximum length of 10240 characters) - `managed_policy_arns`: list of strings that contain the ARN's of the managed policies that should be attached to the permission set - `session_duration`: length of time in the ISO-8601 standard @@ -217,15 +223,13 @@ Example: ] assignments = [ - { - for account in [ 123456789012, 012456789012 ] : account => [ - okta_group.aws["AWSPlatformAdmins"].name - ] - }, - { - for account in [ 925556789012 ] : account => [ - okta_group.aws["AWSPlatformUsers"].name - ] + for account in [ + { id = "123456789012", name = "ProductionAccount" }, + { id = "012456789012", name = "DevelopmentAccount" } + ] : { + account_id = account.id + account_name = account.name + sso_groups = [okta_group.aws["AWSPlatformAdmins"].name] } ] } @@ -238,11 +242,13 @@ Example: ] assignments = [ - { - for account in [ 123456789012, 012456789012 ] : account => [ - okta_group.aws["AWSPlatformAdmins"].name, - okta_group.aws["AWSPlatformUsers"].name - ] + for account in [ + { id = "123456789012", name = "ProductionAccount" }, + { id = "012456789012", name = "DevelopmentAccount" }, + ] : { + account_id = account.id + account_name = account.name + sso_groups = [okta_group.aws["AWSPlatformAdmins"].name, okta_group.aws["AWSPlatformUsers"].name] } ] @@ -555,30 +561,30 @@ module "landing_zone" { | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [control\_tower\_account\_ids](#input\_control\_tower\_account\_ids) | Control Tower core account IDs |
object({
audit = string
logging = string
})
| n/a | yes | -| [regions](#input\_regions) | Region configuration. See the README for more information on the configuration options. |
object({
allowed_regions = list(string)
home_region = string
linked_regions = optional(list(string), ["us-east-1"])
})
| n/a | yes | -| [additional\_auditing\_trail](#input\_additional\_auditing\_trail) | CloudTrail configuration for additional auditing trail |
object({
name = string
bucket = string
kms_key_id = string

event_selector = optional(object({
data_resource = optional(object({
type = string
values = list(string)
}))
exclude_management_event_sources = optional(set(string), null)
include_management_events = optional(bool, true)
read_write_type = optional(string, "All")
}))
})
| `null` | no | -| [aws\_account\_password\_policy](#input\_aws\_account\_password\_policy) | AWS account password policy parameters for the audit, logging and master account |
object({
allow_users_to_change = bool
max_age = number
minimum_length = number
require_lowercase_characters = bool
require_numbers = bool
require_symbols = bool
require_uppercase_characters = bool
reuse_prevention_history = number
})
|
{
"allow_users_to_change": true,
"max_age": 90,
"minimum_length": 14,
"require_lowercase_characters": true,
"require_numbers": true,
"require_symbols": true,
"require_uppercase_characters": true,
"reuse_prevention_history": 24
}
| no | -| [aws\_auditmanager](#input\_aws\_auditmanager) | AWS Audit Manager config settings |
object({
enabled = bool
reports_bucket_prefix = string
})
|
{
"enabled": true,
"reports_bucket_prefix": "audit-manager-reports"
}
| no | -| [aws\_config](#input\_aws\_config) | AWS Config settings |
object({
aggregator_account_ids = optional(list(string), [])
delivery_channel_s3_bucket_name = optional(string, null)
delivery_channel_s3_key_prefix = optional(string, null)
delivery_frequency = optional(string, "TwentyFour_Hours")
rule_identifiers = optional(list(string), [])
})
|
{
"aggregator_account_ids": [],
"delivery_channel_s3_bucket_name": null,
"delivery_channel_s3_key_prefix": null,
"delivery_frequency": "TwentyFour_Hours",
"rule_identifiers": []
}
| no | -| [aws\_config\_sns\_subscription](#input\_aws\_config\_sns\_subscription) | Subscription options for the aws-controltower-AggregateSecurityNotifications (AWS Config) SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | +| [control\_tower\_account\_ids](#input\_control\_tower\_account\_ids) | Control Tower core account IDs |
object({
audit = string
logging = string
})
| n/a | yes | +| [regions](#input\_regions) | Region configuration. See the README for more information on the configuration options. |
object({
allowed_regions = list(string)
home_region = string
linked_regions = optional(list(string), ["us-east-1"])
})
| n/a | yes | +| [additional\_auditing\_trail](#input\_additional\_auditing\_trail) | CloudTrail configuration for additional auditing trail |
object({
name = string
bucket = string
kms_key_id = string

event_selector = optional(object({
data_resource = optional(object({
type = string
values = list(string)
}))
exclude_management_event_sources = optional(set(string), null)
include_management_events = optional(bool, true)
read_write_type = optional(string, "All")
}))
})
| `null` | no | +| [aws\_account\_password\_policy](#input\_aws\_account\_password\_policy) | AWS account password policy parameters for the audit, logging and master account |
object({
allow_users_to_change = bool
max_age = number
minimum_length = number
require_lowercase_characters = bool
require_numbers = bool
require_symbols = bool
require_uppercase_characters = bool
reuse_prevention_history = number
})
|
{
"allow_users_to_change": true,
"max_age": 90,
"minimum_length": 14,
"require_lowercase_characters": true,
"require_numbers": true,
"require_symbols": true,
"require_uppercase_characters": true,
"reuse_prevention_history": 24
}
| no | +| [aws\_auditmanager](#input\_aws\_auditmanager) | AWS Audit Manager config settings |
object({
enabled = bool
reports_bucket_prefix = string
})
|
{
"enabled": true,
"reports_bucket_prefix": "audit-manager-reports"
}
| no | +| [aws\_config](#input\_aws\_config) | AWS Config settings |
object({
aggregator_account_ids = optional(list(string), [])
delivery_channel_s3_bucket_name = optional(string, null)
delivery_channel_s3_key_prefix = optional(string, null)
delivery_frequency = optional(string, "TwentyFour_Hours")
rule_identifiers = optional(list(string), [])
})
|
{
"aggregator_account_ids": [],
"delivery_channel_s3_bucket_name": null,
"delivery_channel_s3_key_prefix": null,
"delivery_frequency": "TwentyFour_Hours",
"rule_identifiers": []
}
| no | +| [aws\_config\_sns\_subscription](#input\_aws\_config\_sns\_subscription) | Subscription options for the aws-controltower-AggregateSecurityNotifications (AWS Config) SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | | [aws\_ebs\_encryption\_by\_default](#input\_aws\_ebs\_encryption\_by\_default) | Set to true to enable AWS Elastic Block Store encryption by default | `bool` | `true` | no | -| [aws\_guardduty](#input\_aws\_guardduty) | AWS GuardDuty settings |
object({
enabled = optional(bool, true)
finding_publishing_frequency = optional(string, "FIFTEEN_MINUTES")
ebs_malware_protection_status = optional(bool, true)
eks_audit_logs_status = optional(bool, true)
lambda_network_logs_status = optional(bool, true)
rds_login_events_status = optional(bool, true)
s3_data_events_status = optional(bool, true)
runtime_monitoring_status = optional(object({
enabled = optional(bool, true)
eks_addon_management_status = optional(bool, true)
ecs_fargate_agent_management_status = optional(bool, true)
ec2_agent_management_status = optional(bool, true)
}), {})
})
| `{}` | no | -| [aws\_inspector](#input\_aws\_inspector) | AWS Inspector settings, at least one of the scan options must be enabled |
object({
enabled = optional(bool, false)
enable_scan_ec2 = optional(bool, true)
enable_scan_ecr = optional(bool, true)
enable_scan_lambda = optional(bool, true)
enable_scan_lambda_code = optional(bool, true)
resource_create_timeout = optional(string, "15m")
})
|
{
"enable_scan_ec2": true,
"enable_scan_ecr": true,
"enable_scan_lambda": true,
"enable_scan_lambda_code": true,
"enabled": false,
"resource_create_timeout": "15m"
}
| no | -| [aws\_required\_tags](#input\_aws\_required\_tags) | AWS Required tags settings |
map(list(object({
name = string
values = optional(list(string))
enforced_for = optional(list(string))
})))
| `null` | no | -| [aws\_security\_hub](#input\_aws\_security\_hub) | AWS Security Hub settings |
object({
aggregator_linking_mode = optional(string, "SPECIFIED_REGIONS")
auto_enable_controls = optional(bool, true)
control_finding_generator = optional(string, "SECURITY_CONTROL")
create_cis_metric_filters = optional(bool, true)
disabled_control_identifiers = optional(list(string), null)
enabled_control_identifiers = optional(list(string), null)
product_arns = optional(list(string), [])
standards_arns = optional(list(string), null)
})
| `{}` | no | -| [aws\_security\_hub\_sns\_subscription](#input\_aws\_security\_hub\_sns\_subscription) | Subscription options for the LandingZone-SecurityHubFindings SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | -| [aws\_service\_control\_policies](#input\_aws\_service\_control\_policies) | AWS SCP's parameters to disable required/denied policies, set a list of allowed AWS regions, and set principals that are exempt from the restriction |
object({
aws_deny_disabling_security_hub = optional(bool, true)
aws_deny_leaving_org = optional(bool, true)
aws_deny_root_user_ous = optional(list(string), [])
aws_require_imdsv2 = optional(bool, true)
principal_exceptions = optional(list(string), [])
})
| `{}` | no | -| [aws\_sso\_permission\_sets](#input\_aws\_sso\_permission\_sets) | Map of AWS IAM Identity Center permission sets with AWS accounts and group names that should be granted access to each account |
map(object({
assignments = list(map(list(string)))
inline_policy = optional(string, null)
managed_policy_arns = optional(list(string), [])
session_duration = optional(string, "PT4H")
}))
| `{}` | no | -| [datadog](#input\_datadog) | Datadog integration options for the core accounts |
object({
api_key = string
cspm_resource_collection_enabled = optional(bool, false)
enable_integration = bool
extended_resource_collection_enabled = optional(bool, false)
install_log_forwarder = optional(bool, false)
log_collection_services = optional(list(string), [])
log_forwarder_version = optional(string)
metric_tag_filters = optional(map(string), {})
namespace_rules = optional(list(string), [])
site_url = string
})
| `null` | no | +| [aws\_guardduty](#input\_aws\_guardduty) | AWS GuardDuty settings |
object({
enabled = optional(bool, true)
finding_publishing_frequency = optional(string, "FIFTEEN_MINUTES")
ebs_malware_protection_status = optional(bool, true)
eks_audit_logs_status = optional(bool, true)
lambda_network_logs_status = optional(bool, true)
rds_login_events_status = optional(bool, true)
s3_data_events_status = optional(bool, true)
runtime_monitoring_status = optional(object({
enabled = optional(bool, true)
eks_addon_management_status = optional(bool, true)
ecs_fargate_agent_management_status = optional(bool, true)
ec2_agent_management_status = optional(bool, true)
}), {})
})
| `{}` | no | +| [aws\_inspector](#input\_aws\_inspector) | AWS Inspector settings, at least one of the scan options must be enabled |
object({
enabled = optional(bool, false)
enable_scan_ec2 = optional(bool, true)
enable_scan_ecr = optional(bool, true)
enable_scan_lambda = optional(bool, true)
enable_scan_lambda_code = optional(bool, true)
resource_create_timeout = optional(string, "15m")
})
|
{
"enable_scan_ec2": true,
"enable_scan_ecr": true,
"enable_scan_lambda": true,
"enable_scan_lambda_code": true,
"enabled": false,
"resource_create_timeout": "15m"
}
| no | +| [aws\_required\_tags](#input\_aws\_required\_tags) | AWS Required tags settings |
map(list(object({
name = string
values = optional(list(string))
enforced_for = optional(list(string))
})))
| `null` | no | +| [aws\_security\_hub](#input\_aws\_security\_hub) | AWS Security Hub settings |
object({
aggregator_linking_mode = optional(string, "SPECIFIED_REGIONS")
auto_enable_controls = optional(bool, true)
control_finding_generator = optional(string, "SECURITY_CONTROL")
create_cis_metric_filters = optional(bool, true)
disabled_control_identifiers = optional(list(string), null)
enabled_control_identifiers = optional(list(string), null)
product_arns = optional(list(string), [])
standards_arns = optional(list(string), null)
})
| `{}` | no | +| [aws\_security\_hub\_sns\_subscription](#input\_aws\_security\_hub\_sns\_subscription) | Subscription options for the LandingZone-SecurityHubFindings SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | +| [aws\_service\_control\_policies](#input\_aws\_service\_control\_policies) | AWS SCP's parameters to disable required/denied policies, set a list of allowed AWS regions, and set principals that are exempt from the restriction |
object({
aws_deny_disabling_security_hub = optional(bool, true)
aws_deny_leaving_org = optional(bool, true)
aws_deny_root_user_ous = optional(list(string), [])
aws_require_imdsv2 = optional(bool, true)
principal_exceptions = optional(list(string), [])
})
| `{}` | no | +| [aws\_sso\_permission\_sets](#input\_aws\_sso\_permission\_sets) | Map of AWS IAM Identity Center permission sets with AWS accounts and group names that should be granted access to each account |
map(object({
assignments = list(object({
account_id = string
account_name = string
sso_groups = list(string)
}))
inline_policy = optional(string, null)
managed_policy_arns = optional(list(string), [])
session_duration = optional(string, "PT4H")
}))
| `{}` | no | +| [datadog](#input\_datadog) | Datadog integration options for the core accounts |
object({
api_key = string
cspm_resource_collection_enabled = optional(bool, false)
enable_integration = bool
extended_resource_collection_enabled = optional(bool, false)
install_log_forwarder = optional(bool, false)
log_collection_services = optional(list(string), [])
log_forwarder_version = optional(string)
metric_tag_filters = optional(map(string), {})
namespace_rules = optional(list(string), [])
site_url = string
})
| `null` | no | | [datadog\_excluded\_regions](#input\_datadog\_excluded\_regions) | List of regions where metrics collection will be disabled. | `list(string)` | `[]` | no | | [kms\_key\_policy](#input\_kms\_key\_policy) | A list of valid KMS key policy JSON documents | `list(string)` | `[]` | no | | [kms\_key\_policy\_audit](#input\_kms\_key\_policy\_audit) | A list of valid KMS key policy JSON document for use with audit KMS key | `list(string)` | `[]` | no | | [kms\_key\_policy\_logging](#input\_kms\_key\_policy\_logging) | A list of valid KMS key policy JSON document for use with logging KMS key | `list(string)` | `[]` | no | | [monitor\_iam\_activity](#input\_monitor\_iam\_activity) | Whether IAM activity should be monitored | `bool` | `true` | no | -| [monitor\_iam\_activity\_sns\_subscription](#input\_monitor\_iam\_activity\_sns\_subscription) | Subscription options for the LandingZone-IAMActivity SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | +| [monitor\_iam\_activity\_sns\_subscription](#input\_monitor\_iam\_activity\_sns\_subscription) | Subscription options for the LandingZone-IAMActivity SNS topic |
map(object({
endpoint = string
protocol = string
}))
| `{}` | no | | [path](#input\_path) | Optional path for all IAM users, user groups, roles, and customer managed policies created by this module | `string` | `"/"` | no | -| [ses\_root\_accounts\_mail\_forward](#input\_ses\_root\_accounts\_mail\_forward) | SES config to receive and forward root account emails |
object({
domain = string
from_email = string
recipient_mapping = map(any)

dmarc = object({
policy = optional(string)
rua = optional(string)
ruf = optional(string)
})
})
| `null` | no | +| [ses\_root\_accounts\_mail\_forward](#input\_ses\_root\_accounts\_mail\_forward) | SES config to receive and forward root account emails |
object({
domain = string
from_email = string
recipient_mapping = map(any)

dmarc = object({
policy = optional(string)
rua = optional(string)
ruf = optional(string)
})
})
| `null` | no | | [tags](#input\_tags) | Map of tags | `map(string)` | `{}` | no | ## Outputs @@ -617,16 +623,16 @@ To make local development easier, we have added a pre-commit configuration to th Install the following tools: -```brew install tflint``` +`brew install tflint` Install pre-commit: -```pip3 install pre-commit --upgrade``` +`pip3 install pre-commit --upgrade` To run the pre-commit hooks to see if everything working as expected, (the first time run might take a few minutes): -```pre-commit run -a``` +`pre-commit run -a` To install the pre-commit hooks to run before each commit: -```pre-commit install``` +`pre-commit install` diff --git a/UPGRADING.md b/UPGRADING.md index 487839d8..07cb3a41 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,51 @@ This document captures required refactoring on your part when upgrading to a module version that contains breaking changes. +## Upgrading to v6.0.0 + +### Key Changes + +#### Refactored `permission-set` sub-module + +The previous version of this sub-module used the AWS account ID as a key in the `aws_ssoadmin_account_assignment.default` resource. This worked as expected when creating accounts in separate workspace, but when calling this sub-module directly in the same workspace that creates the account, the account ID is still to be computed which isn't allowed for key names. + +To fix this, the resource was refactored to use predictable values, so we swapped out the account ID for the account name: `aws_ssoadmin_account_assignment.default["${each.sso_group}:${each.aws_account_id}"]` becomes `aws_ssoadmin_account_assignment.default["${each.sso_group}:${each.aws_account_name}"]` + +Unfortunately this means that the state will be out of sync and the resources will be recreated as `moved` blocks do not yet support `for_each`. + +#### Variables + +- `aws_sso_permission_sets` in the root has had it's `assignments` field changed from a map to a list of objects. See the README for more detailed information. The same change has been implemented in the `assignments` variable of the `permission-set` sub-module. + +### How to upgrade + +Change the `aws_sso_permission_sets` variable format to use a list objects, e.g. + +```hcl + assignments = [ + { + for account in [ 123456789012, 012456789012 ] : account => [ + okta_group.aws["AWSPlatformAdmins"].name + ] + }, + ] +``` + +Becomes: + +```hcl + assignments = [ + for account in [ + { id = "123456789012", name = "ProductionAccount" }, + { id = "012456789012", name = "DevelopmentAccount" } + ] : { + account_id = account.id + account_name = account.name + sso_groups = [okta_group.aws["AWSPlatformAdmins"].name] + } + ] +``` + ## Upgrading to v5.0.0 ### Key Changes @@ -23,65 +68,69 @@ This version transitions Security Hub configuration from **Local** to **Central* ### Variables The following variables have been replaced: -* `aws_service_control_policies.allowed_regions` → `regions.allowed_regions` -* `aws_config.aggregator_regions` → the union of `regions.home_region` and `regions.linked_regions` + +- `aws_service_control_policies.allowed_regions` → `regions.allowed_regions` +- `aws_config.aggregator_regions` → the union of `regions.home_region` and `regions.linked_regions` The following variables have been introduced: -* `aws_security_hub.aggregator_linking_mode`. Indicates whether to aggregate findings from all of the available Regions or from a specified list. -* `aws_security_hub.disabled_control_identifiers`. List of Security Hub control IDs that are disabled in the organisation. -* `aws_security_hub.enabled_control_identifiers`. List of Security Hub control IDs that are enabled in the organisation. + +- `aws_security_hub.aggregator_linking_mode`. Indicates whether to aggregate findings from all of the available Regions or from a specified list. +- `aws_security_hub.disabled_control_identifiers`. List of Security Hub control IDs that are disabled in the organisation. +- `aws_security_hub.enabled_control_identifiers`. List of Security Hub control IDs that are enabled in the organisation. The following variables have been removed: -* `aws_security_hub.auto_enable_new_accounts`. This variable is not configurable anymore using security hub central configuration. -* `aws_security_hub.auto_enable_default_standards`. This variable is not configurable anymore using security hub central configuration. -### How to upgrade. +- `aws_security_hub.auto_enable_new_accounts`. This variable is not configurable anymore using security hub central configuration. +- `aws_security_hub.auto_enable_default_standards`. This variable is not configurable anymore using security hub central configuration. + +### How to upgrade 1. Verify Control Tower Governed Regions. - Ensure your AWS Control Tower Landing Zone regions includes `us-east-1`. + Ensure your AWS Control Tower Landing Zone regions includes `us-east-1`. - To check: - 1. Log in to the **core-management account**. - 2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. - 3. Confirm `us-east-1` is listed under **Landing Zone Regions**. + To check: - If `us-east-1` is missing, update your AWS Control Tower settings **before upgrading**. + 1. Log in to the **core-management account**. + 2. Navigate to **AWS Control Tower** → **Landing Zone Settings**. + 3. Confirm `us-east-1` is listed under **Landing Zone Regions**. + + If `us-east-1` is missing, update your AWS Control Tower settings **before upgrading**. > [!NOTE] > For more details on the `regions` variable, refer to the [Specifying the correct regions section in the readme](README.md). -2. Update the variables according to the variables section above. +2. Update the variables according to the variables section above. 3. Manually Removing Local Security Hub Standards - Previous versions managed `aws_securityhub_standards_subscription` resources locally in core accounts. These are now centrally configured using `aws_securityhub_configuration_policy`. **Terraform will attempt to remove these resources from the state**. To prevent disabling them, the resources must be manually removed from the Terraform state. + Previous versions managed `aws_securityhub_standards_subscription` resources locally in core accounts. These are now centrally configured using `aws_securityhub_configuration_policy`. **Terraform will attempt to remove these resources from the state**. To prevent disabling them, the resources must be manually removed from the Terraform state. - *Steps to Remove Resources:* + _Steps to Remove Resources:_ - a. Generate Removal Commands. Run the following shell snippet: + a. Generate Removal Commands. Run the following shell snippet: - ```shell - terraform init - for local_standard in $(terraform state list | grep "module.landing_zone.aws_securityhub_standards_subscription"); do - echo "terraform state rm '$local_standard'" - done - ``` + ```shell + terraform init + for local_standard in $(terraform state list | grep "module.landing_zone.aws_securityhub_standards_subscription"); do + echo "terraform state rm '$local_standard'" + done + ``` - b. Execute Commands: Evaluate and run the generated statements. They will look like: + b. Execute Commands: Evaluate and run the generated statements. They will look like: - ```shell - terraform state rm 'module.landing_zone.aws_securityhub_standards_subscription.logging["arn:aws:securityhub:eu-central-1::standards/pci-dss/v/3.2.1"]' - ... - ``` + ```shell + terraform state rm 'module.landing_zone.aws_securityhub_standards_subscription.logging["arn:aws:securityhub:eu-central-1::standards/pci-dss/v/3.2.1"]' + ... + ``` - *Why Manual Removal is Required* + _Why Manual Removal is Required_ - Terraform cannot handle `for_each` loops in `removed` statements ([HashiCorp Issue #34439](https://github.com/hashicorp/terraform/issues/34439)). Therefore the resources created with a `for_each` loop on `local.security_hub_standards_arns` must be manually removed from the Terraform state to prevent unintended deletions. + Terraform cannot handle `for_each` loops in `removed` statements ([HashiCorp Issue #34439](https://github.com/hashicorp/terraform/issues/34439)). Therefore the resources created with a `for_each` loop on `local.security_hub_standards_arns` must be manually removed from the Terraform state to prevent unintended deletions. -4. Upgrade your mcaf-landing-zone module to v5.x.x. +4. Upgrade your mcaf-landing-zone module to v5.x.x. -5. Upgrade your [mcaf-account-baseline](https://github.com/schubergphilis/terraform-aws-mcaf-account-baseline) deployments to v2.0.0 or higher. +5. Upgrade your [mcaf-account-baseline](https://github.com/schubergphilis/terraform-aws-mcaf-account-baseline) deployments to v2.0.0 or higher. ### Troubleshooting @@ -90,15 +139,18 @@ The following variables have been removed: #### Resolution Steps 1. **Verify `regions.linked_regions`:** + - Ensure that `regions.linked_regions` matches the AWS Control Tower Landing Zone regions. - For guidance, refer to the [Specifying the correct regions section in the README](README.md). 2. **Check Organizational Units (OUs):** + - Log in to the **core-management account**. - Navigate to **AWS Control Tower** → **Organization**. - Confirm all OUs have the **Baseline state** set to `Succeeded`. 3. **Check Account Baseline States:** + - In **AWS Control Tower** → **Organization**, verify that all accounts show a **Baseline state** of `Succeeded`. - If any accounts display `Update available`: - Select the account. @@ -117,38 +169,40 @@ If all steps are completed and the issue persists, review AWS Control Tower sett **Workaround:** Suppress these findings or enable AWS Config yourself in the linked regions for the core-management account. - ## Upgrading to v4.0.0 -> [!WARNING] -> **Read the diagram in [PR 210](https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone/pull/210) and the guide below! If you currently have EKS Runtime Monitoring enabled, you need to perform MANUAL steps after you have migrated to this version.** +> [!WARNING] > **Read the diagram in [PR 210](https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone/pull/210) and the guide below! If you currently have EKS Runtime Monitoring enabled, you need to perform MANUAL steps after you have migrated to this version.** ### Behaviour Using the default `aws_guardduty` values: -* `EKS_RUNTIME_MONITORING` gets removed from the state (but not disabled) -* `RUNTIME_MONITORING` is enabled including `ECS_FARGATE_AGENT_MANAGEMENT`, `EC2_AGENT_MANAGEMENT`, and `EKS_ADDON_MANAGEMENT`. -* Minimum required AWS provider has been set to `v5.54.0`, and minimum required Terraform version has been set to `v1.6`. + +- `EKS_RUNTIME_MONITORING` gets removed from the state (but not disabled) +- `RUNTIME_MONITORING` is enabled including `ECS_FARGATE_AGENT_MANAGEMENT`, `EC2_AGENT_MANAGEMENT`, and `EKS_ADDON_MANAGEMENT`. +- Minimum required AWS provider has been set to `v5.54.0`, and minimum required Terraform version has been set to `v1.6`. ### Variables The following variables have been replaced: -* `aws_guardduty.eks_runtime_monitoring_status` -> `aws_guardduty.runtime_monitoring_status.enabled` -* `aws_guardduty.eks_addon_management_status` -> `aws_guardduty.runtime_monitoring_status.eks_addon_management_status` + +- `aws_guardduty.eks_runtime_monitoring_status` -> `aws_guardduty.runtime_monitoring_status.enabled` +- `aws_guardduty.eks_addon_management_status` -> `aws_guardduty.runtime_monitoring_status.eks_addon_management_status` The following variables have been introduced: -* `aws_guardduty.runtime_monitoring_status.ecs_fargate_agent_management_status` -* `aws_guardduty.runtime_monitoring_status.ec2_agent_management_status` + +- `aws_guardduty.runtime_monitoring_status.ecs_fargate_agent_management_status` +- `aws_guardduty.runtime_monitoring_status.ec2_agent_management_status` ### EKS Runtime Monitoring to Runtime Monitoring migration #### The issue -After you upgraded to this version. **RUNTIME_MONITORING is enabled. But EKS_RUNTIME_MONITORING is not disabled** as is written in the [guardduty_detector_feature documentation](https://registry.terraform.io/providers/hashicorp/aws/5.68.0/docs/resources/guardduty_detector_feature): _Deleting this resource does not disable the detector feature, the resource in simply removed from state instead._ + +After you upgraded to this version. **RUNTIME_MONITORING is enabled. But EKS_RUNTIME_MONITORING is not disabled** as is written in the [guardduty_detector_feature documentation](https://registry.terraform.io/providers/hashicorp/aws/5.68.0/docs/resources/guardduty_detector_feature): _Deleting this resource does not disable the detector feature, the resource in simply removed from state instead._ To prevent duplicated costs please **disable** EKS_RUNTIME_MONITORING manually after upgrading. > [!IMPORTANT] -> Run all the commands with valid credentials in the AWS account where guardduty is delegated administrator. By default this is the **control tower audit** account. +> Run all the commands with valid credentials in the AWS account where guardduty is delegated administrator. By default this is the **control tower audit** account. > It's not possible to execute these steps from the AWS Console as the EKS Runtime Monitoring protection plan has already been removed from the GUI. The only way to control this feature is via the CLI. #### Step 1: get the GuardDuty detector id @@ -168,9 +222,9 @@ Should display: ``` > [!IMPORTANT] -> Ensure you run this command in the right region! If GuardDuty is enabled in multiple regions then execute all steps for all enabled regions. +> Ensure you run this command in the right region! If GuardDuty is enabled in multiple regions then execute all steps for all enabled regions. -#### Step 2: update the GuardDuty detector +#### Step 2: update the GuardDuty detector _Replace 12abc34d567e8fa901bc2d34e56789f0 with your own regional detector-id. Execute these commands in the audit account:_ @@ -186,7 +240,6 @@ Replace the `<>` with your current configuration for auto-enabli aws guardduty update-organization-configuration --detector-id 12abc34d567e8fa901bc2d34e56789f0 --auto-enable-organization-members <> --features '[{"Name" : "EKS_RUNTIME_MONITORING", "AutoEnable": "NONE"}]' ``` - #### Step 4: update the GuardDuty member accounts Disable EKS Runtime Monitoring for **all** member accounts in your organization, for example: @@ -199,7 +252,7 @@ aws guardduty update-member-detectors --detector-id 12abc34d567e8fa901bc2d34e567 > An error occurred (BadRequestException) when calling the UpdateMemberDetectors operation: The request is rejected because a feature cannot be turned off for a member while organization has the feature flag set to 'All Accounts'. -Change these options on the AWS console by following the steps below: +Change these options on the AWS console by following the steps below: 1. Go to the GuardDuty Console. 2. On left navigation bar, under protection plans, select `Runtime Monitoring`. @@ -283,15 +336,17 @@ This version sets the minimum required aws provider version from v4 to v5. ### Variables The following variables have been replaced: -* `aws_guardduty.datasources.malware_protection` -> `aws_guardduty.ebs_malware_protection_status` -* `aws_guardduty.datasources.kubernetes` -> `aws_guardduty.eks_audit_logs_status` -* `aws_guardduty.datasources.s3_logs` -> `aws_guardduty.s3_data_events_status` + +- `aws_guardduty.datasources.malware_protection` -> `aws_guardduty.ebs_malware_protection_status` +- `aws_guardduty.datasources.kubernetes` -> `aws_guardduty.eks_audit_logs_status` +- `aws_guardduty.datasources.s3_logs` -> `aws_guardduty.s3_data_events_status` The following variables have been introduced: -* `aws_guardduty.eks_addon_management_status` -* `aws_guardduty.eks_runtime_monitoring_status` -* `aws_guardduty.lambda_network_logs_status` -* `aws_guardduty.rds_login_events_status` + +- `aws_guardduty.eks_addon_management_status` +- `aws_guardduty.eks_runtime_monitoring_status` +- `aws_guardduty.lambda_network_logs_status` +- `aws_guardduty.rds_login_events_status` ## Upgrading to v1.0.0 diff --git a/modules/permission-set/main.tf b/modules/permission-set/main.tf index a3be8b17..b7f45d08 100644 --- a/modules/permission-set/main.tf +++ b/modules/permission-set/main.tf @@ -1,12 +1,11 @@ locals { aws_sso_account_assignments = flatten([ for assignment in var.assignments : [ - for aws_account_id, sso_groups in assignment : [ - for sso_group in sso_groups : { - aws_account_id = aws_account_id - sso_group = sso_group - } - ] + for sso_group in assignment.sso_groups : { + aws_account_id = assignment.account_id + aws_account_name = assignment.account_name + sso_group = sso_group + } ] ]) } @@ -14,8 +13,8 @@ locals { data "aws_ssoadmin_instances" "default" {} data "aws_identitystore_group" "default" { - for_each = toset(distinct([ - for assignment in local.aws_sso_account_assignments : assignment.sso_group + for_each = toset(flatten([ + for assignment in var.assignments : assignment.sso_groups ])) identity_store_id = tolist(data.aws_ssoadmin_instances.default.identity_store_ids)[0] @@ -58,7 +57,7 @@ resource "aws_ssoadmin_permission_set" "default" { resource "aws_ssoadmin_account_assignment" "default" { for_each = { for assignment in local.aws_sso_account_assignments : - "${assignment.sso_group}:${assignment.aws_account_id}" => assignment + "${assignment.sso_group}:${assignment.aws_account_name}" => assignment } instance_arn = var.create ? aws_ssoadmin_permission_set.default[0].instance_arn : data.aws_ssoadmin_permission_set.default[0].instance_arn diff --git a/modules/permission-set/variables.tf b/modules/permission-set/variables.tf index ca34c448..76736382 100644 --- a/modules/permission-set/variables.tf +++ b/modules/permission-set/variables.tf @@ -4,9 +4,13 @@ variable "name" { } variable "assignments" { - type = list(map(list(string))) + type = list(object({ + account_id = string + account_name = string + sso_groups = list(string) + })) default = [] - description = "List of account IDs and Identity Center groups to assign to the permission set" + description = "List of account names and IDs and Identity Center groups to assign to the permission set" } variable "create" { diff --git a/variables.tf b/variables.tf index 691ec14b..26728bbd 100644 --- a/variables.tf +++ b/variables.tf @@ -214,7 +214,11 @@ variable "aws_service_control_policies" { variable "aws_sso_permission_sets" { type = map(object({ - assignments = list(map(list(string))) + assignments = list(object({ + account_id = string + account_name = string + sso_groups = list(string) + })) inline_policy = optional(string, null) managed_policy_arns = optional(list(string), []) session_duration = optional(string, "PT4H")