Skip to content

Commit

Permalink
breaking: Central Security Hub configuration (#216)
Browse files Browse the repository at this point in the history
feature: support central Security Hub configuration
  • Loading branch information
sbkg0002 authored Dec 24, 2024
1 parent 2242725 commit e767916
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 111 deletions.
129 changes: 74 additions & 55 deletions README.md

Large diffs are not rendered by default.

106 changes: 106 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 4 additions & 2 deletions config.tf
Original file line number Diff line number Diff line change
@@ -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, []),
[
Expand All @@ -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}"
Expand All @@ -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
Expand Down
12 changes: 8 additions & 4 deletions examples/basic/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
}
}
1 change: 1 addition & 0 deletions locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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])))
}
6 changes: 3 additions & 3 deletions organizations_policy.tf
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
61 changes: 33 additions & 28 deletions security_hub.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,59 @@ 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

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" {
Expand Down Expand Up @@ -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]
}
Loading

0 comments on commit e767916

Please sign in to comment.