diff --git a/README.md b/README.md index 938501f..38f4c0e 100644 --- a/README.md +++ b/README.md @@ -8,55 +8,82 @@ Overview of Landing Zone tools & services: The SBP AWS Landing Zone consists of 3 repositories: -- [MCAF Landing Zone module (current repository)](https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone): the foundation of the Landing Zone and manages the 3 core accounts: audit, logging, master +- [MCAF Landing Zone module (current repository)](https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone): the foundation of the Landing Zone and manages the 3 core accounts: audit, logging, management - [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). + ## Basic configuration -```hcl -locals { - control_tower_account_ids = { - audit = "012345678902" - logging = "012345678903" - } -} +Refer to [examples/basic](examples/basic/main.tf) for an example of minimal setup. -provider "aws" {} +### Specifying the correct regions -provider "aws" { - alias = "audit" +**Home Region** - assume_role { - role_arn = "arn:aws:iam::${local.control_tower_account_ids.audit}:role/AWSControlTowerExecution" - } -} +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. -provider "aws" { - alias = "logging" +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**. - assume_role { - role_arn = "arn:aws:iam::${local.control_tower_account_ids.logging}:role/AWSControlTowerExecution" - } -} +**Linked Regions** -provider "datadog" { - validate = false -} +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. -provider "mcaf" { - aws {} -} +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**. -module "landing_zone" { - providers = { aws = aws, aws.audit = aws.audit, aws.logging = aws.logging } +*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. + +**Allowed Regions** + +The optional `regions.allowed_regions` variable defines the allowed regions within your AWS Organization. This triggers the deployment of a [Service Control Policy (SCP)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_examples.html#example-scp-deny-region), which is attached to the root of your AWS Organization. - source = "github.com/schubergphilis/terraform-aws-mcaf-landing-zone?ref=VERSION" +#### Configuration Scenarios - control_tower_account_ids = local.control_tower_account_ids - tags = { Terraform = true } +**Scenario 1: Home region only (no deployment in other regions)** + +- **Home region:** `eu-central-1` +- **Requirement:** Prevent deployment in all other regions. + +You need to configure the `regions` variable as follows: + +```hcl +regions = { + allowed_regions = ["eu-central-1"] + home_region = "eu-central-1" } +``` + +*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`. + +You need to configure the `regions` variable as follows: +```hcl +regions = { + allowed_regions = ["eu-central-1", "eu-west-1] + home_region = "eu-central-1" + linked_regions = ["eu-west-1", "us-east-1"] +} ``` ## Detailed configuration @@ -101,9 +128,9 @@ additional_auditing_trail = { ### AWS Config Rules -This module provisions by default a set of basic AWS Config Rules. In order to add extra rules, a list of [rule identifiers](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) can be passed via the variable `aws_config` using the attribute `rule_identifiers`. +This module provisions by default a set of basic AWS Config Rules. In order to add extra rules, a list of [rule identifiers](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html) can be passed via the variable `aws_config.rule_identifiers`. -If you would like to authorize other accounts to aggregate AWS Config data, the account IDs and regions can also be passed via the variable `aws_config` using the attributes `aggregator_account_ids` and `aggregator_regions` respectively. +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. @@ -112,7 +139,6 @@ Example: ```hcl aws_config = { aggregator_account_ids = ["123456789012"] - aggregator_regions = ["eu-west-1"] rule_identifiers = ["ACCESS_KEYS_ROTATED", "ALB_WAF_ENABLED"] } ``` @@ -121,13 +147,13 @@ aws_config = { This module supports enabling GuardDuty at the organization level which means that all new accounts that are created in, or added to, the organization are added as member accounts to the `audit` account GuardDuty detector. -The feature can be controlled via the `aws_guardduty` variable and is enabled by default. The finding publishing frequency has been reduced from 6 hours to every 15 minutes, and the Malware Protection, Kubernetes and S3 Logs data sources are enabled out of the box. +The feature can be controlled via the `aws_guardduty` variable and is enabled by default. -Note: In case you are migrating an existing AWS organization to this module, all existing accounts except for the `master` and `logging` accounts have to be enabled like explained [here](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html#guardduty_add_orgs_accounts). +Note: In case you are migrating an existing AWS organization to this module, all existing accounts except for the `management` and `logging` accounts have to be enabled like explained [here](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html#guardduty_add_orgs_accounts). ## AWS KMS -The module creates 3 AWS KMS keys, one for the master account, one for the audit account, and one for the log archive account. We recommend to further scope down the AWS KMS key policy in the master account by providing a secure policy using `kms_key_policy`. The default policy "Base Permissions" can be overwritten and should be limited to the root account only, for example by using the statement below: +The module creates 3 AWS KMS keys, one for the management account, one for the audit account, and one for the log archive account. We recommend to further scope down the AWS KMS key policy in the management account by providing a secure policy using `kms_key_policy`. The default policy "Base Permissions" can be overwritten and should be limited to the root account only, for example by using the statement below: ```hcl statement { @@ -320,15 +346,7 @@ aws_service_control_policies = { #### SCP: Restricting AWS Regions -If you would like to define which AWS Regions can be used in your AWS Organization, you can pass a list of region names to the variable `aws_service_control_policies` using the `allowed_regions` attribute. This will trigger this module to deploy a [Service Control Policy (SCP) designed by AWS](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_examples.html#example-scp-deny-region) and attach it to the root of your AWS Organization. - -Example: - -```hcl -aws_service_control_policies = { - allowed_regions = ["eu-west-1"] -} -``` +See the section `Specifying the correct regions`. #### SCP: Restricting Root User Access @@ -504,14 +522,13 @@ module "landing_zone" { | [aws_s3_account_public_access_block.master](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_account_public_access_block) | resource | | [aws_securityhub_account.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | | [aws_securityhub_account.management](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_account) | resource | +| [aws_securityhub_configuration_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_configuration_policy) | resource | +| [aws_securityhub_configuration_policy_association.root](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_configuration_policy_association) | resource | +| [aws_securityhub_finding_aggregator.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_finding_aggregator) | resource | | [aws_securityhub_member.logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_member) | resource | | [aws_securityhub_member.management](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_member) | resource | | [aws_securityhub_organization_admin_account.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_admin_account) | resource | | [aws_securityhub_organization_configuration.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_organization_configuration) | resource | -| [aws_securityhub_product_subscription.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_product_subscription) | resource | -| [aws_securityhub_standards_subscription.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | -| [aws_securityhub_standards_subscription.logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | -| [aws_securityhub_standards_subscription.management](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/securityhub_standards_subscription) | resource | | [aws_sns_topic.iam_activity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_sns_topic.security_hub_findings](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_sns_topic_policy.iam_activity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_policy) | resource | @@ -539,19 +556,19 @@ 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 | -| [tags](#input\_tags) | Map of tags | `map(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), [])
aggregator_regions = 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": [],
"aggregator_regions": [],
"delivery_channel_s3_bucket_name": null,
"delivery_channel_s3_key_prefix": null,
"delivery_frequency": "TwentyFour_Hours",
"rule_identifiers": []
}
| 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({
auto_enable_controls = optional(bool, true)
auto_enable_default_standards = optional(bool, false)
auto_enable_new_accounts = optional(bool, true)
control_finding_generator = optional(string, "SECURITY_CONTROL")
create_cis_metric_filters = optional(bool, true)
product_arns = optional(list(string), [])
standards_arns = optional(list(string), null)
})
|
{
"auto_enable_controls": true,
"auto_enable_default_standards": false,
"auto_enable_new_accounts": true,
"control_finding_generator": "SECURITY_CONTROL",
"create_cis_metric_filters": true,
"product_arns": [],
"standards_arns": 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({
allowed_regions = optional(list(string), [])
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\_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 | | [datadog\_excluded\_regions](#input\_datadog\_excluded\_regions) | List of regions where metrics collection will be disabled. | `list(string)` | `[]` | no | @@ -562,11 +579,13 @@ module "landing_zone" { | [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 | +| [tags](#input\_tags) | Map of tags | `map(string)` | `{}` | no | ## Outputs | Name | Description | |------|-------------| +| [aws\_config\_s3\_bucket\_arn](#output\_aws\_config\_s3\_bucket\_arn) | ARN of the AWS Config S3 bucket | | [kms\_key\_arn](#output\_kms\_key\_arn) | ARN of KMS key for master account | | [kms\_key\_audit\_arn](#output\_kms\_key\_audit\_arn) | ARN of KMS key for audit account | | [kms\_key\_audit\_id](#output\_kms\_key\_audit\_id) | ID of KMS key for audit account | diff --git a/UPGRADING.md b/UPGRADING.md index 79fa1f8..cd23b4a 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,112 @@ This document captures required refactoring on your part when upgrading to a module version that contains breaking changes. +## Upgrading to v5.0.0 + +### Key Changes + +#### Transition to Centralized Security Hub Configuration + +This version transitions Security Hub configuration from **Local** to **Central**. Learn more in the [AWS Security Hub Documentation](https://docs.aws.amazon.com/securityhub/latest/userguide/central-configuration-intro.html). + +**Default Behavior:** + +- Security Hub Findings Aggregation is enabled for regions defined in: + - `regions.home_region` + - `regions.linked_regions`. `us-east-1` is automatically included for global services. + +#### Dropping Support for Local Configuration + +**Local configurations are no longer supported.** Centralized configuration aligns with AWS best practices and reduces complexity. + +### 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` + +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. + +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. + +1. Verify Control Tower Governed Regions. + + 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**. + + 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. + +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. + + *Steps to Remove Resources:* + + 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 + ``` + + 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"]' + ... + ``` + + *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. + +4. Upgrade your mcaf-landing-zone module to v5.x.x. + +### Troubleshooting + +#### Issue: AWS Security Hub control "AWS Config should be enabled and use the service-linked role for resource recording" fails for multiple accounts after upgrade + +#### 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. + - Go to **Actions** → **Update**. + +4. **Allow Time for Changes to Propagate:** + - Wait up to **24 hours** for updates to propagate and resolve the Security Hub findings. + +If all steps are completed and the issue persists, review AWS Control Tower settings and logs for additional troubleshooting. + + ## Upgrading to v4.0.0 > [!WARNING] diff --git a/config.tf b/config.tf index 17f8171..c808f55 100644 --- a/config.tf +++ b/config.tf @@ -1,12 +1,13 @@ locals { aws_config_aggregators = flatten([ for account in toset(try(var.aws_config.aggregator_account_ids, [])) : [ - for region in toset(try(var.aws_config.aggregator_regions, [])) : { + for region in toset(try(local.all_organisation_regions, [])) : { account_id = account region = region } ] ]) + aws_config_rules = setunion( try(var.aws_config.rule_identifiers, []), [ @@ -16,6 +17,7 @@ locals { "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" ] ) + aws_config_s3_name = coalesce( var.aws_config.delivery_channel_s3_bucket_name, "aws-config-configuration-history-${var.control_tower_account_ids.logging}-${data.aws_region.current.name}" @@ -32,7 +34,7 @@ resource "aws_config_aggregate_authorization" "master" { } resource "aws_config_aggregate_authorization" "master_to_audit" { - for_each = toset(coalescelist(var.aws_config.aggregator_regions, [data.aws_region.current.name])) + for_each = local.all_organisation_regions account_id = var.control_tower_account_ids.audit region = each.value diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 3f33bca..6d9b7f4 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -6,12 +6,12 @@ locals { } provider "aws" { - region = "eu-west-1" + region = "eu-central-1" } provider "aws" { alias = "audit" - region = "eu-west-1" + region = "eu-central-1" assume_role { role_arn = "arn:aws:iam::${local.control_tower_account_ids.audit}:role/AWSControlTowerExecution" @@ -20,7 +20,7 @@ provider "aws" { provider "aws" { alias = "logging" - region = "eu-west-1" + region = "eu-central-1" assume_role { role_arn = "arn:aws:iam::${local.control_tower_account_ids.logging}:role/AWSControlTowerExecution" @@ -41,5 +41,9 @@ module "landing_zone" { source = "../../" control_tower_account_ids = local.control_tower_account_ids - tags = { Terraform = true } + + regions = { + allowed_regions = ["eu-central-1"] + home_region = "eu-central-1" + } } diff --git a/locals.tf b/locals.tf index 0727e22..08b6cd1 100644 --- a/locals.tf +++ b/locals.tf @@ -34,4 +34,5 @@ locals { security_hub_has_cis_aws_foundations_enabled = length(regexall( "cis-aws-foundations-benchmark/v", join(",", local.security_hub_standards_arns) )) > 0 ? true : false + all_organisation_regions = toset(distinct(concat([var.regions.home_region], var.regions.linked_regions, var.regions.allowed_regions, [data.aws_region.current.name]))) } diff --git a/organizations_policy.tf b/organizations_policy.tf index 3f2d3d3..92a0bba 100644 --- a/organizations_policy.tf +++ b/organizations_policy.tf @@ -1,9 +1,9 @@ locals { enabled_root_policies = { allowed_regions = { - enable = var.aws_service_control_policies.allowed_regions != null ? true : false - policy = var.aws_service_control_policies.allowed_regions != null ? templatefile("${path.module}/files/organizations/allowed_regions.json.tpl", { - allowed = var.aws_service_control_policies.allowed_regions != null ? var.aws_service_control_policies.allowed_regions : [] + enable = var.regions.allowed_regions != null ? true : false + policy = var.regions.allowed_regions != null ? templatefile("${path.module}/files/organizations/allowed_regions.json.tpl", { + allowed = var.regions.allowed_regions != null ? var.regions.allowed_regions : [] exceptions = local.aws_service_control_policies_principal_exceptions }) : null } diff --git a/outputs.tf b/outputs.tf index 82ab63a..cbbdc77 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,3 +1,8 @@ +output "aws_config_s3_bucket_arn" { + description = "ARN of the AWS Config S3 bucket" + value = module.aws_config_s3.arn +} + output "kms_key_arn" { description = "ARN of KMS key for master account" value = module.kms_key.arn diff --git a/security_hub.tf b/security_hub.tf index 5bbeb76..93fcf3b 100644 --- a/security_hub.tf +++ b/security_hub.tf @@ -23,14 +23,6 @@ resource "aws_securityhub_member" "management" { } } -resource "aws_securityhub_standards_subscription" "management" { - for_each = toset(local.security_hub_standards_arns) - - standards_arn = each.value - - depends_on = [aws_securityhub_account.default] -} - // AWS Security Hub - Audit account configuration and enrollment resource "aws_securityhub_account" "default" { provider = aws.audit @@ -38,31 +30,52 @@ resource "aws_securityhub_account" "default" { control_finding_generator = var.aws_security_hub.control_finding_generator } -resource "aws_securityhub_organization_configuration" "default" { +resource "aws_securityhub_finding_aggregator" "default" { provider = aws.audit - auto_enable = var.aws_security_hub.auto_enable_new_accounts - auto_enable_standards = var.aws_security_hub.auto_enable_default_standards ? "DEFAULT" : "NONE" + linking_mode = var.aws_security_hub.aggregator_linking_mode + specified_regions = var.aws_security_hub.aggregator_linking_mode == "SPECIFIED_REGIONS" ? var.regions.linked_regions : null - depends_on = [aws_securityhub_organization_admin_account.default] + depends_on = [aws_securityhub_account.default] } -resource "aws_securityhub_product_subscription" "default" { - for_each = toset(var.aws_security_hub.product_arns) +resource "aws_securityhub_organization_configuration" "default" { provider = aws.audit - product_arn = each.value + auto_enable = false + auto_enable_standards = "NONE" - depends_on = [aws_securityhub_account.default] + organization_configuration { + configuration_type = "CENTRAL" + } + + depends_on = [aws_securityhub_organization_admin_account.default, aws_securityhub_finding_aggregator.default] } -resource "aws_securityhub_standards_subscription" "default" { - for_each = toset(local.security_hub_standards_arns) +resource "aws_securityhub_configuration_policy" "default" { provider = aws.audit - standards_arn = each.value + name = "mcaf-lz" + description = "MCAF Landing Zone default configuration policy" - depends_on = [aws_securityhub_account.default] + configuration_policy { + service_enabled = true + enabled_standard_arns = local.security_hub_standards_arns + + security_controls_configuration { + disabled_control_identifiers = var.aws_security_hub.disabled_control_identifiers + enabled_control_identifiers = var.aws_security_hub.enabled_control_identifiers + } + } + + depends_on = [aws_securityhub_organization_configuration.default] +} + +resource "aws_securityhub_configuration_policy_association" "root" { + provider = aws.audit + + target_id = data.aws_organizations_organization.default.roots[0].id + policy_id = aws_securityhub_configuration_policy.default.id } resource "aws_cloudwatch_event_rule" "security_hub_findings" { @@ -125,11 +138,3 @@ resource "aws_securityhub_member" "logging" { depends_on = [aws_securityhub_organization_configuration.default] } - -resource "aws_securityhub_standards_subscription" "logging" { - for_each = toset(local.security_hub_standards_arns) - provider = aws.logging - - standards_arn = each.value - depends_on = [aws_securityhub_account.default] -} diff --git a/variables.tf b/variables.tf index 9bd2c79..691ec14 100644 --- a/variables.tf +++ b/variables.tf @@ -18,6 +18,20 @@ variable "additional_auditing_trail" { description = "CloudTrail configuration for additional auditing trail" } +variable "regions" { + type = object({ + allowed_regions = list(string) + home_region = string + linked_regions = optional(list(string), ["us-east-1"]) + }) + description = "Region configuration. See the README for more information on the configuration options." + + validation { + condition = length(var.regions.linked_regions) > 0 + error_message = "The 'linked_regions' list must include at least one region. By default, 'us-east-1' is specified to ensure the tracking of global resources. Please specify at least one region if overriding the default." + } +} + variable "aws_account_password_policy" { type = object({ allow_users_to_change = bool @@ -57,7 +71,6 @@ variable "aws_auditmanager" { variable "aws_config" { type = object({ aggregator_account_ids = optional(list(string), []) - aggregator_regions = 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") @@ -65,7 +78,6 @@ variable "aws_config" { }) default = { aggregator_account_ids = [] - aggregator_regions = [] delivery_channel_s3_bucket_name = null delivery_channel_s3_key_prefix = null delivery_frequency = "TwentyFour_Hours" @@ -151,29 +163,32 @@ variable "aws_required_tags" { variable "aws_security_hub" { type = object({ - auto_enable_controls = optional(bool, true) - auto_enable_default_standards = optional(bool, false) - auto_enable_new_accounts = optional(bool, true) - control_finding_generator = optional(string, "SECURITY_CONTROL") - create_cis_metric_filters = optional(bool, true) - product_arns = optional(list(string), []) - standards_arns = optional(list(string), null) + 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) }) - default = { - auto_enable_controls = true - auto_enable_default_standards = false - auto_enable_new_accounts = true - control_finding_generator = "SECURITY_CONTROL" - create_cis_metric_filters = true - product_arns = [] - standards_arns = null - } + default = {} description = "AWS Security Hub settings" validation { condition = contains(["SECURITY_CONTROL", "STANDARD_CONTROL"], var.aws_security_hub.control_finding_generator) error_message = "The \"control_finding_generator\" variable must be set to either \"SECURITY_CONTROL\" or \"STANDARD_CONTROL\"." } + + validation { + condition = contains(["SPECIFIED_REGIONS", "ALL_REGIONS"], var.aws_security_hub.aggregator_linking_mode) + error_message = "The \"aggregator_linking_mode\" variable must be set to either \"SPECIFIED_REGIONS\" or \"ALL_REGIONS\"." + } + + validation { + condition = try(length(var.aws_security_hub.enabled_control_identifiers), 0) == 0 || try(length(var.aws_security_hub.disabled_control_identifiers), 0) == 0 + error_message = "Only one of \"enabled_control_identifiers\" or \"disabled_control_identifiers\" variable can be set." + } } variable "aws_security_hub_sns_subscription" { @@ -187,7 +202,6 @@ variable "aws_security_hub_sns_subscription" { variable "aws_service_control_policies" { type = object({ - allowed_regions = optional(list(string), []) aws_deny_disabling_security_hub = optional(bool, true) aws_deny_leaving_org = optional(bool, true) aws_deny_root_user_ous = optional(list(string), []) @@ -297,5 +311,6 @@ variable "ses_root_accounts_mail_forward" { variable "tags" { type = map(string) + default = {} description = "Map of tags" }