558 | ```
559 |
560 | ## Upgrading to v0.11.x
561 |
562 | Version `0.11.x` adds additional IAM activity monitors, these will be created automatically if you have the cis-aws-foundations-benchmark standard enabled. To disable the creation of these monitors set the variable `security_hub_create_cis_metric_filters` to false.
563 |
564 | ## Upgrading to v0.10.x
565 |
566 | Version `0.10.x` adds the possibility of assigning the same SSO Permission Set to different groups of accounts and SSO Groups. For example, the permission set `Administrator` can be assigned to group A for account 123 and for group B for account 456.
567 |
568 | This required changing the variable `aws_sso_permission_sets` where the `accounts` attribute was renamed to `assignments` and changed to a list.
569 |
570 | ## Upgrading to v0.9.x
571 |
572 | Removal of the local AVM module. Modify the source to the new [MCAF Account Vending Machine (AVM) module](https://github.com/schubergphilis/terraform-aws-mcaf-avm).
573 |
574 | The following variables have been renamed:
575 |
576 | - `sns_aws_config_subscription` -> `aws_config_sns_subscription`
577 | - `security_hub_product_arns` -> `aws_security_hub_product_arns`
578 | - `sns_aws_security_hub_subscription` -> `aws_security_hub_sns_subscription`
579 | - `sns_monitor_iam_activity_subscription` -> `monitor_iam_activity_sns_subscription`
580 |
581 | The following variable has been removed:
582 |
583 | - `aws_create_account_password_policy`, if you do not want to enable the password policy set the `aws_account_password_policy` variable to `null`
584 |
585 | The provider alias has changed. Change the following occurence for all accounts, as shown below for the `sandbox` AVM module instance.
586 |
587 | ```shell
588 | module.sandbox.provider[\"registry.terraform.io/hashicorp/aws\"].managed_by_inception => module.sandbox.provider[\"registry.terraform.io/hashicorp/aws\"].account
589 | ```
590 |
591 | Moreover, resources in the AVM module are now stored under `module.tfe_workspace[0]`, resulting in a plan wanting to destroy and recreate the existing Terraform Cloud workspace and IAM user used by the workspace which is undesirable.
592 |
593 | To prevent this happening, simply move the resources in the state to their new location as shown below for the `sandbox` AVM module instance:
594 |
595 | ```shell
596 | terraform state mv 'module.sandbox.module.workspace[0]' 'module.sandbox.module.tfe_workspace[0]'
597 | ```
598 |
599 | Finally, if you are migrating to the [MCAF Account Baseline module](https://github.com/schubergphilis/terraform-aws-mcaf-account-baseline) as well. Then remove the following resources from the state and let these resource be managed by the baseline workspaces. Command shown below for the `sandbox` AVM module instance
600 |
601 | ```shell
602 | terraform state mv -state-out=baseline-sandbox.tfstate 'module.sandbox.aws_cloudwatch_log_metric_filter.iam_activity' 'module.account_baseline.aws_cloudwatch_log_metric_filter.iam_activity'
603 | terraform state mv -state-out=baseline-sandbox.tfstate 'module.sandbox.aws_cloudwatch_metric_alarm.iam_activity' 'module.account_baseline.aws_cloudwatch_metric_alarm.iam_activity'
604 | terraform state mv -state-out=baseline-sandbox.tfstate 'module.sandbox.aws_iam_account_password_policy.default' 'module.account_baseline.aws_iam_account_password_policy.default'
605 | terraform state mv -state-out=baseline-sandbox.tfstate 'module.sandbox.aws_ebs_encryption_by_default.default' 'module.account_baseline.aws_ebs_encryption_by_default.default'
606 | ```
607 |
608 | ## Upgrading to v0.8.x
609 |
610 | Version `0.8.x` introduces the possibility of managing AWS SSO resources using this module. To avoid a race condition between Okta pushing groups to AWS SSO and Terraform trying to read them using data sources, the `okta_app_saml` resource has been removed from the module.
611 |
612 | With this change, all Okta configuration can be managed in the way that best suits the user. It also makes it possible to use this module with any other identity provider that is able to create groups on AWS SSO.
613 |
614 | ## Upgrading to v0.7.x
615 |
616 | From version `0.7.0`, the monitoring of IAM entities has changed from Event Bridge Rules to CloudWatch Alarms. This means that passing a list of IAM identities to the variable `monitor_iam_access` is no longer supported.
617 |
618 | The name of the SNS Topic used for notifications has also changed from `LandingZone-MonitorIAMAccess` to `LandingZone-IAMActivity`. Since this is a new Topic, all pre-existing SNS Subscriptions should be configured again using the variable `sns_monitor_iam_activity_subscription`.
619 |
620 | ## Upgrading to v0.5.x
621 |
622 | Since the `create_workspace` variable was added to the AVM module, resources in the included [terraform-aws-mcaf-workspace](https://github.com/schubergphilis/terraform-aws-mcaf-workspace) module are now stored under `module.workspace[0]`, resulting in a plan wanting to destroy and recreate the existing Terraform Cloud workspace and IAM user used by the workspace which is undesirable.
623 |
624 | To prevent this happening, simply move the resources in the state to their new location as shown below for the `sandbox` AVM module instance:
625 |
626 | ```shell
627 | terraform state mv 'module.sandbox.module.workspace' 'module.sandbox.module.workspace[0]'
628 | ```
629 |
630 | ## Upgrading from v0.1.x to v0.2.x
631 |
632 | This section describes changes to be aware of when upgrading from v0.1.x to v0.2.x.
633 |
634 | ### Enhancements
635 |
636 | #### AWS Config Aggregator Accounts
637 |
638 | Since version `0.2.x` supports multiple account IDs when configuring AWS Config Aggregator accounts, the identifier given to the multiple `aws_config_aggregate_authorization` resources had to change from `region_name` to `account_id-region_name`. This causes the authorizations created by version `0.1.x` to be destroyed and recreated with the new identifiers.
639 |
640 | #### AWS GuardDuty
641 |
642 | In order to enable GuardDuty for the entire organization, all existing accounts except for the `master` and `logging` accounts have to be add as members in the `audit` account like explained [here](https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_organizations.html#guardduty_add_orgs_accounts). If this step is not taken, only the core accounts will have GuardDuty enabled.
643 |
644 | #### TFE Workspaces
645 |
646 | TFE Workspaces use version [0.3.0 of the terraform-aws-mcaf-workspace](https://github.com/schubergphilis/terraform-aws-mcaf-workspace/tree/v0.3.0) module which by default creates a Terraform backend file in the repository associated with the workspace.
647 |
--------------------------------------------------------------------------------
/account_audit.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_account_password_policy" "audit" {
2 | count = var.aws_account_password_policy != null ? 1 : 0
3 | provider = aws.audit
4 |
5 | allow_users_to_change_password = var.aws_account_password_policy.allow_users_to_change
6 | max_password_age = var.aws_account_password_policy.max_age
7 | minimum_password_length = var.aws_account_password_policy.minimum_length
8 | password_reuse_prevention = var.aws_account_password_policy.reuse_prevention_history
9 | require_lowercase_characters = var.aws_account_password_policy.require_lowercase_characters
10 | require_numbers = var.aws_account_password_policy.require_numbers
11 | require_symbols = var.aws_account_password_policy.require_symbols
12 | require_uppercase_characters = var.aws_account_password_policy.require_uppercase_characters
13 | }
14 |
15 | resource "aws_ebs_encryption_by_default" "audit" {
16 | provider = aws.audit
17 |
18 | enabled = var.aws_ebs_encryption_by_default
19 | }
20 |
21 | resource "aws_s3_account_public_access_block" "audit" {
22 | provider = aws.audit
23 |
24 | block_public_acls = true
25 | block_public_policy = true
26 | ignore_public_acls = true
27 | restrict_public_buckets = true
28 | }
29 |
--------------------------------------------------------------------------------
/account_logging.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_account_password_policy" "logging" {
2 | count = var.aws_account_password_policy != null ? 1 : 0
3 | provider = aws.logging
4 |
5 | allow_users_to_change_password = var.aws_account_password_policy.allow_users_to_change
6 | max_password_age = var.aws_account_password_policy.max_age
7 | minimum_password_length = var.aws_account_password_policy.minimum_length
8 | password_reuse_prevention = var.aws_account_password_policy.reuse_prevention_history
9 | require_lowercase_characters = var.aws_account_password_policy.require_lowercase_characters
10 | require_numbers = var.aws_account_password_policy.require_numbers
11 | require_symbols = var.aws_account_password_policy.require_symbols
12 | require_uppercase_characters = var.aws_account_password_policy.require_uppercase_characters
13 | }
14 |
15 | resource "aws_ebs_encryption_by_default" "logging" {
16 | provider = aws.logging
17 |
18 | enabled = var.aws_ebs_encryption_by_default
19 | }
20 |
21 | resource "aws_s3_account_public_access_block" "logging" {
22 | provider = aws.logging
23 |
24 | block_public_acls = true
25 | block_public_policy = true
26 | ignore_public_acls = true
27 | restrict_public_buckets = true
28 | }
29 |
--------------------------------------------------------------------------------
/account_management.tf:
--------------------------------------------------------------------------------
1 | resource "aws_cloudwatch_log_metric_filter" "iam_activity_master" {
2 | for_each = var.monitor_iam_activity ? merge(local.iam_activity, local.cloudtrail_activity_cis_aws_foundations) : {}
3 |
4 | name = "LandingZone-IAMActivity-${each.key}"
5 | pattern = each.value
6 | log_group_name = data.aws_cloudwatch_log_group.cloudtrail_master[0].name
7 |
8 | metric_transformation {
9 | name = "LandingZone-IAMActivity-${each.key}"
10 | namespace = "LandingZone-IAMActivity"
11 | value = "1"
12 | }
13 | }
14 |
15 | resource "aws_cloudwatch_metric_alarm" "iam_activity_master" {
16 | for_each = aws_cloudwatch_log_metric_filter.iam_activity_master
17 |
18 | alarm_name = each.value.name
19 | comparison_operator = "GreaterThanOrEqualToThreshold"
20 | evaluation_periods = "1"
21 | metric_name = each.value.name
22 | namespace = each.value.metric_transformation[0].namespace
23 | period = "300"
24 | statistic = "Sum"
25 | threshold = "1"
26 | alarm_description = "Monitors IAM activity for ${each.key}"
27 | alarm_actions = [aws_sns_topic.iam_activity[0].arn]
28 | insufficient_data_actions = []
29 | tags = var.tags
30 | }
31 |
32 | resource "aws_iam_account_password_policy" "master" {
33 | count = var.aws_account_password_policy != null ? 1 : 0
34 |
35 | allow_users_to_change_password = var.aws_account_password_policy.allow_users_to_change
36 | max_password_age = var.aws_account_password_policy.max_age
37 | minimum_password_length = var.aws_account_password_policy.minimum_length
38 | password_reuse_prevention = var.aws_account_password_policy.reuse_prevention_history
39 | require_lowercase_characters = var.aws_account_password_policy.require_lowercase_characters
40 | require_numbers = var.aws_account_password_policy.require_numbers
41 | require_symbols = var.aws_account_password_policy.require_symbols
42 | require_uppercase_characters = var.aws_account_password_policy.require_uppercase_characters
43 | }
44 |
45 | resource "aws_ebs_encryption_by_default" "master" {
46 | enabled = var.aws_ebs_encryption_by_default
47 | }
48 |
49 | resource "aws_s3_account_public_access_block" "master" {
50 | block_public_acls = true
51 | block_public_policy = true
52 | ignore_public_acls = true
53 | restrict_public_buckets = true
54 | }
55 |
--------------------------------------------------------------------------------
/audit_manager.tf:
--------------------------------------------------------------------------------
1 | resource "aws_auditmanager_account_registration" "default" {
2 | count = var.aws_auditmanager.enabled == true ? 1 : 0
3 |
4 | delegated_admin_account = data.aws_caller_identity.audit.account_id
5 | deregister_on_destroy = true
6 | kms_key = module.kms_key_audit.arn
7 | }
8 |
9 | module "audit_manager_reports" {
10 | count = var.aws_auditmanager.enabled == true ? 1 : 0
11 | providers = { aws = aws.audit }
12 |
13 | source = "schubergphilis/mcaf-s3/aws"
14 | version = "~> 0.14.1"
15 |
16 | kms_key_arn = module.kms_key_audit.arn
17 | name_prefix = var.aws_auditmanager.reports_bucket_prefix
18 | versioning = true
19 |
20 | lifecycle_rule = [
21 | {
22 | id = "retention"
23 | enabled = true
24 |
25 | abort_incomplete_multipart_upload = {
26 | days_after_initiation = 7
27 | }
28 |
29 | noncurrent_version_expiration = {
30 | noncurrent_days = 90
31 | }
32 |
33 | noncurrent_version_transition = {
34 | noncurrent_days = 30
35 | storage_class = "ONEZONE_IA"
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/cloudtrail.tf:
--------------------------------------------------------------------------------
1 | #tfsec:ignore:AWS065
2 | resource "aws_cloudtrail" "additional_auditing_trail" {
3 | #checkov:skip=CKV_AWS_252: "Ensure CloudTrail defines an SNS Topic"
4 | #checkov:skip=CKV2_AWS_10: "Ensure CloudTrail trails are integrated with CloudWatch Logs"
5 | count = var.additional_auditing_trail != null ? 1 : 0
6 |
7 | name = var.additional_auditing_trail.name
8 | enable_log_file_validation = true
9 | is_multi_region_trail = true
10 | is_organization_trail = true
11 | s3_bucket_name = var.additional_auditing_trail.bucket
12 | kms_key_id = var.additional_auditing_trail.kms_key_id
13 | tags = var.tags
14 |
15 | dynamic "event_selector" {
16 | for_each = var.additional_auditing_trail.event_selector != null ? { create = true } : {}
17 |
18 | content {
19 | dynamic "data_resource" {
20 | for_each = var.additional_auditing_trail.event_selector.data_resource != null ? { create = true } : {}
21 |
22 | content {
23 | type = var.additional_auditing_trail.event_selector.data_resource.type
24 | values = var.additional_auditing_trail.event_selector.data_resource.values
25 | }
26 | }
27 |
28 | include_management_events = var.additional_auditing_trail.event_selector.include_management_events
29 | exclude_management_event_sources = var.additional_auditing_trail.event_selector.exclude_management_event_sources
30 | read_write_type = var.additional_auditing_trail.event_selector.read_write_type
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/config.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | aws_config_aggregators = flatten([
3 | for account in toset(try(var.aws_config.aggregator_account_ids, [])) : [
4 | for region in toset(try(local.all_organisation_regions, [])) : {
5 | account_id = account
6 | region = region
7 | }
8 | ]
9 | ])
10 |
11 | aws_config_rules = setunion(
12 | try(var.aws_config.rule_identifiers, []),
13 | [
14 | "CLOUD_TRAIL_ENABLED",
15 | "ENCRYPTED_VOLUMES",
16 | "ROOT_ACCOUNT_MFA_ENABLED",
17 | "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
18 | ]
19 | )
20 |
21 | aws_config_s3_name = coalesce(
22 | var.aws_config.delivery_channel_s3_bucket_name,
23 | "aws-config-configuration-history-${var.control_tower_account_ids.logging}-${data.aws_region.current.name}"
24 | )
25 | }
26 |
27 | // AWS Config - Management account configuration
28 | resource "aws_config_aggregate_authorization" "master" {
29 | for_each = { for aggregator in local.aws_config_aggregators : "${aggregator.account_id}-${aggregator.region}" => aggregator if aggregator.account_id != var.control_tower_account_ids.audit }
30 |
31 | account_id = each.value.account_id
32 | region = each.value.region
33 | tags = var.tags
34 | }
35 |
36 | resource "aws_config_aggregate_authorization" "master_to_audit" {
37 | for_each = local.all_organisation_regions
38 |
39 | account_id = var.control_tower_account_ids.audit
40 | region = each.value
41 | tags = var.tags
42 | }
43 |
44 | resource "aws_iam_service_linked_role" "config" {
45 | aws_service_name = "config.amazonaws.com"
46 | }
47 |
48 | resource "aws_config_organization_managed_rule" "default" {
49 | for_each = toset(local.aws_config_rules)
50 |
51 | name = each.value
52 | rule_identifier = each.value
53 | }
54 |
55 | // AWS Config - Audit account configuration
56 | resource "aws_config_configuration_aggregator" "audit" {
57 | provider = aws.audit
58 |
59 | name = "audit"
60 | tags = var.tags
61 |
62 | account_aggregation_source {
63 | account_ids = [
64 | for account in data.aws_organizations_organization.default.accounts : account.id if account.id != var.control_tower_account_ids.audit
65 | ]
66 | all_regions = true
67 | }
68 | }
69 |
70 | resource "aws_config_aggregate_authorization" "audit" {
71 | for_each = { for aggregator in local.aws_config_aggregators : "${aggregator.account_id}-${aggregator.region}" => aggregator if aggregator.account_id != var.control_tower_account_ids.audit }
72 | provider = aws.audit
73 |
74 | account_id = each.value.account_id
75 | region = each.value.region
76 | tags = var.tags
77 | }
78 |
79 | resource "aws_sns_topic_subscription" "aws_config" {
80 | for_each = var.aws_config_sns_subscription
81 | provider = aws.audit
82 |
83 | endpoint = each.value.endpoint
84 | endpoint_auto_confirms = length(regexall("http", each.value.protocol)) > 0
85 | protocol = each.value.protocol
86 | topic_arn = "arn:aws:sns:${data.aws_region.current.name}:${var.control_tower_account_ids.audit}:aws-controltower-AggregateSecurityNotifications"
87 | }
88 |
89 | // AWS Config - Logging account configuration
90 | resource "aws_config_aggregate_authorization" "logging" {
91 | for_each = { for aggregator in local.aws_config_aggregators : "${aggregator.account_id}-${aggregator.region}" => aggregator if aggregator.account_id != var.control_tower_account_ids.audit }
92 | provider = aws.logging
93 |
94 | account_id = each.value.account_id
95 | region = each.value.region
96 | tags = var.tags
97 | }
98 |
99 | data "aws_iam_policy_document" "aws_config_s3" {
100 | statement {
101 | sid = "AWSConfigBucketPermissionsCheck"
102 | actions = ["s3:GetBucketAcl"]
103 | resources = ["arn:aws:s3:::${local.aws_config_s3_name}"]
104 |
105 | principals {
106 | type = "Service"
107 | identifiers = ["config.amazonaws.com"]
108 | }
109 | }
110 |
111 | statement {
112 | sid = "AWSConfigBucketExistenceCheck"
113 | actions = ["s3:ListBucket"]
114 | resources = ["arn:aws:s3:::${local.aws_config_s3_name}"]
115 |
116 | principals {
117 | type = "Service"
118 | identifiers = ["config.amazonaws.com"]
119 | }
120 | }
121 |
122 | statement {
123 | sid = "AllowConfigWriteAccess"
124 | actions = ["s3:PutObject"]
125 | resources = ["arn:aws:s3:::${local.aws_config_s3_name}/*"]
126 |
127 | principals {
128 | type = "Service"
129 | identifiers = ["config.amazonaws.com"]
130 | }
131 |
132 | condition {
133 | test = "StringEquals"
134 | variable = "s3:x-amz-acl"
135 | values = ["bucket-owner-full-control"]
136 | }
137 | }
138 | }
139 |
140 | module "aws_config_s3" {
141 | #checkov:skip=CKV_AWS_19: False positive, KMS key is used by default https://github.com/bridgecrewio/checkov/issues/3847
142 | #checkov:skip=CKV_AWS_145: False positive, KMS key is used by default https://github.com/bridgecrewio/checkov/issues/3847
143 | providers = { aws = aws.logging }
144 |
145 | source = "schubergphilis/mcaf-s3/aws"
146 | version = "~> 0.14.1"
147 |
148 | name = local.aws_config_s3_name
149 | kms_key_arn = module.kms_key_logging.arn
150 | policy = data.aws_iam_policy_document.aws_config_s3.json
151 | versioning = true
152 | tags = var.tags
153 |
154 | lifecycle_rule = [
155 | {
156 | id = "retention"
157 | enabled = true
158 |
159 | abort_incomplete_multipart_upload = {
160 | days_after_initiation = 7
161 | }
162 |
163 | expiration = {
164 | days = 365
165 | }
166 |
167 | noncurrent_version_expiration = {
168 | noncurrent_days = 365
169 | }
170 |
171 | noncurrent_version_transition = {
172 | noncurrent_days = 90
173 | storage_class = "STANDARD_IA"
174 | }
175 |
176 | transition = {
177 | days = 90
178 | storage_class = "STANDARD_IA"
179 | }
180 | }
181 | ]
182 | }
183 |
184 | module "aws_config_recorder" {
185 | source = "./modules/aws-config-recorder"
186 |
187 | delivery_frequency = var.aws_config.delivery_frequency
188 | iam_service_linked_role_arn = aws_iam_service_linked_role.config.arn
189 | kms_key_arn = module.kms_key_logging.arn
190 | s3_bucket_name = module.aws_config_s3.name
191 | s3_key_prefix = var.aws_config.delivery_channel_s3_key_prefix
192 | sns_topic_arn = data.aws_sns_topic.all_config_notifications.arn
193 | }
194 |
--------------------------------------------------------------------------------
/data.tf:
--------------------------------------------------------------------------------
1 | data "aws_caller_identity" "audit" {
2 | provider = aws.audit
3 | }
4 |
5 | data "aws_caller_identity" "logging" {
6 | provider = aws.logging
7 | }
8 |
9 | data "aws_caller_identity" "management" {}
10 |
11 | data "aws_cloudwatch_log_group" "cloudtrail_master" {
12 | count = var.monitor_iam_activity ? 1 : 0
13 |
14 | name = "aws-controltower/CloudTrailLogs"
15 | }
16 |
17 | data "aws_organizations_organization" "default" {}
18 |
19 | data "aws_organizations_organizational_units" "default" {
20 | parent_id = data.aws_organizations_organization.default.roots[0].id
21 | }
22 |
23 | data "aws_region" "current" {}
24 |
25 | data "aws_sns_topic" "all_config_notifications" {
26 | provider = aws.audit
27 |
28 | name = "aws-controltower-AllConfigNotifications"
29 | }
30 |
31 | data "mcaf_aws_all_organizational_units" "default" {}
32 |
--------------------------------------------------------------------------------
/datadog.tf:
--------------------------------------------------------------------------------
1 | module "datadog_audit" {
2 | #checkov:skip=CKV_AWS_124: since this is managed by terraform, we reason that this already provides feedback and a seperate SNS topic is therefore not required
3 | count = try(var.datadog.enable_integration, false) == true ? 1 : 0
4 | providers = { aws = aws.audit }
5 |
6 | source = "schubergphilis/mcaf-datadog/aws"
7 | version = "~> 0.9.0"
8 |
9 | api_key = try(var.datadog.api_key, null)
10 | api_key_name = var.datadog.create_api_key ? "${var.datadog.api_key_name_prefix}${data.aws_caller_identity.audit.account_id}" : null
11 | create_api_key = var.datadog.create_api_key
12 | cspm_resource_collection_enabled = var.datadog.cspm_resource_collection_enabled
13 | excluded_regions = var.datadog_excluded_regions
14 | extended_resource_collection_enabled = var.datadog.extended_resource_collection_enabled
15 | install_log_forwarder = var.datadog.install_log_forwarder
16 | log_collection_services = var.datadog.log_collection_services
17 | log_forwarder_version = var.datadog.log_forwarder_version
18 | metric_tag_filters = var.datadog.metric_tag_filters
19 | namespace_rules = var.datadog.namespace_rules
20 | site_url = try(var.datadog.site_url, null)
21 | tags = var.tags
22 | }
23 |
24 | module "datadog_master" {
25 | #checkov:skip=CKV_AWS_124: since this is managed by terraform, we reason that this already provides feedback and a seperate SNS topic is therefore not required
26 | count = try(var.datadog.enable_integration, false) == true ? 1 : 0
27 |
28 | source = "schubergphilis/mcaf-datadog/aws"
29 | version = "~> 0.9.0"
30 |
31 | api_key = try(var.datadog.api_key, null)
32 | api_key_name = var.datadog.create_api_key ? "${var.datadog.api_key_name_prefix}${data.aws_caller_identity.management.account_id}" : null
33 | create_api_key = var.datadog.create_api_key
34 | cspm_resource_collection_enabled = var.datadog.cspm_resource_collection_enabled
35 | excluded_regions = var.datadog_excluded_regions
36 | extended_resource_collection_enabled = var.datadog.extended_resource_collection_enabled
37 | install_log_forwarder = var.datadog.install_log_forwarder
38 | log_collection_services = var.datadog.log_collection_services
39 | log_forwarder_version = var.datadog.log_forwarder_version
40 | metric_tag_filters = var.datadog.metric_tag_filters
41 | namespace_rules = var.datadog.namespace_rules
42 | site_url = try(var.datadog.site_url, null)
43 | tags = var.tags
44 | }
45 |
46 | module "datadog_logging" {
47 | #checkov:skip=CKV_AWS_124: since this is managed by terraform, we reason that this already provides feedback and a seperate SNS topic is therefore not required
48 | count = try(var.datadog.enable_integration, false) == true ? 1 : 0
49 | providers = { aws = aws.logging }
50 |
51 | source = "schubergphilis/mcaf-datadog/aws"
52 | version = "~> 0.9.0"
53 |
54 | api_key = try(var.datadog.api_key, null)
55 | api_key_name = var.datadog.create_api_key ? "${var.datadog.api_key_name_prefix}${data.aws_caller_identity.logging.account_id}" : null
56 | create_api_key = var.datadog.create_api_key
57 | cspm_resource_collection_enabled = var.datadog.cspm_resource_collection_enabled
58 | excluded_regions = var.datadog_excluded_regions
59 | extended_resource_collection_enabled = var.datadog.extended_resource_collection_enabled
60 | install_log_forwarder = var.datadog.install_log_forwarder
61 | log_collection_services = var.datadog.log_collection_services
62 | log_forwarder_version = var.datadog.log_forwarder_version
63 | metric_tag_filters = var.datadog.metric_tag_filters
64 | namespace_rules = var.datadog.namespace_rules
65 | site_url = try(var.datadog.site_url, null)
66 | tags = var.tags
67 | }
68 |
--------------------------------------------------------------------------------
/examples/basic/README.md:
--------------------------------------------------------------------------------
1 | # basic
2 |
3 | Terraform module to test basic functionality of `terraform-aws-mcaf-landing-zone` module.
4 |
5 |
6 | ## Requirements
7 |
8 | | Name | Version |
9 | |------|---------|
10 | | [terraform](#requirement\_terraform) | >= 1.3 |
11 | | [aws](#requirement\_aws) | >= 4.40.0 |
12 | | [datadog](#requirement\_datadog) | > 3.0.0 |
13 | | [mcaf](#requirement\_mcaf) | >= 0.4.2 |
14 |
15 | ## Providers
16 |
17 | No providers.
18 |
19 | ## Modules
20 |
21 | | Name | Source | Version |
22 | |------|--------|---------|
23 | | [landing\_zone](#module\_landing\_zone) | ../../ | n/a |
24 |
25 | ## Resources
26 |
27 | No resources.
28 |
29 | ## Inputs
30 |
31 | No inputs.
32 |
33 | ## Outputs
34 |
35 | No outputs.
36 |
37 |
--------------------------------------------------------------------------------
/examples/basic/main.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | control_tower_account_ids = {
3 | audit = "012345678902"
4 | logging = "012345678903"
5 | }
6 | }
7 |
8 | provider "aws" {
9 | region = "eu-central-1"
10 | }
11 |
12 | provider "aws" {
13 | alias = "audit"
14 | region = "eu-central-1"
15 |
16 | assume_role {
17 | role_arn = "arn:aws:iam::${local.control_tower_account_ids.audit}:role/AWSControlTowerExecution"
18 | }
19 | }
20 |
21 | provider "aws" {
22 | alias = "logging"
23 | region = "eu-central-1"
24 |
25 | assume_role {
26 | role_arn = "arn:aws:iam::${local.control_tower_account_ids.logging}:role/AWSControlTowerExecution"
27 | }
28 | }
29 |
30 | provider "datadog" {
31 | validate = false
32 | }
33 |
34 | provider "mcaf" {
35 | aws {}
36 | }
37 |
38 | module "landing_zone" {
39 | providers = { aws = aws, aws.audit = aws.audit, aws.logging = aws.logging }
40 |
41 | source = "../../"
42 |
43 | control_tower_account_ids = local.control_tower_account_ids
44 |
45 | regions = {
46 | allowed_regions = ["eu-central-1"]
47 | home_region = "eu-central-1"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/examples/basic/terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 5.54.0"
6 | configuration_aliases = [aws.audit, aws.logging]
7 | }
8 | datadog = {
9 | source = "datadog/datadog"
10 | version = ">= 3.39"
11 | }
12 | mcaf = {
13 | source = "schubergphilis/mcaf"
14 | version = ">= 0.4.2"
15 | }
16 | }
17 | required_version = ">= 1.9.0"
18 | }
19 |
--------------------------------------------------------------------------------
/files/event_bridge/security_hub_findings.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "detail-type": ["Security Hub Findings - Imported"],
3 | "source": ["aws.securityhub"],
4 | "detail": {
5 | "findings": {
6 | "Severity": {
7 | "Label": [
8 | "HIGH",
9 | "CRITICAL",
10 | "MEDIUM"
11 | ]
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/files/iam/monitor_iam_access_policy.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Action": "events:PutEvents",
7 | "Resource": "${event_bus_arn}"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/files/iam/service_assume_role.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Action": "sts:AssumeRole",
6 | "Principal": {
7 | "Service": "${service}"
8 | },
9 | "Effect": "Allow",
10 | "Sid": ""
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/files/organizations/cloudtrail_log_stream.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": {
4 | "Sid": "DenyDeletingCloudTrailLogStream",
5 | "Effect": "Deny",
6 | "Action": [
7 | "logs:DeleteLogStream"
8 | ],
9 | "Resource": [
10 | "arn:aws:logs:*:*:log-group:aws-controltower/CloudTrailLogs:*"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/files/organizations/deny_disabling_security_hub.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": {
4 | "Sid": "DenyDisablingSecurityHub",
5 | "Effect": "Deny",
6 | "Action": [
7 | "securityhub:DeleteInvitations",
8 | "securityhub:DisableSecurityHub",
9 | "securityhub:DisassociateFromMasterAccount",
10 | "securityhub:DeleteMembers",
11 | "securityhub:DisassociateMembers",
12 | "securityhub:BatchDisableStandards"
13 | ],
14 | "Resource": "*",
15 | "Condition": {
16 | %{ if length(exceptions) > 0 ~}
17 | "ArnNotLike": {
18 | "aws:PrincipalARN": ${jsonencode(exceptions)}
19 | }
20 | %{ endif ~}
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/files/organizations/deny_leaving_org.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": {
4 | "Sid": "DenyLeavingOrg",
5 | "Effect": "Deny",
6 | "Action": "organizations:LeaveOrganization",
7 | "Resource": "*",
8 | "Condition": {
9 | %{ if length(exceptions) > 0 ~}
10 | "ArnNotLike": {
11 | "aws:PrincipalARN": ${jsonencode(exceptions)}
12 | }
13 | %{ endif ~}
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/files/organizations/deny_root_user.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "DenyRootUser",
6 | "Effect": "Deny",
7 | "Action": "*",
8 | "Resource": "*",
9 | "Condition": {
10 | "StringLike": {
11 | "aws:PrincipalArn": "arn:aws:iam::*:root"
12 | }
13 | }
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/files/organizations/require_use_of_imdsv2.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "RequireAllEc2RolesToUseV2",
6 | "Effect": "Deny",
7 | "Action": "*",
8 | "Resource": "*",
9 | "Condition": {
10 | "NumericLessThan": {
11 | "ec2:RoleDelivery": "2.0"
12 | }
13 | }
14 | },
15 | {
16 | "Sid": "RequireImdsV2",
17 | "Effect": "Deny",
18 | "Action": "ec2:RunInstances",
19 | "Resource": "arn:aws:ec2:*:*:instance/*",
20 | "Condition": {
21 | "StringNotEquals": {
22 | "ec2:MetadataHttpTokens": "required"
23 | }
24 | }
25 | },
26 | {
27 | "Effect": "Deny",
28 | "Action": "ec2:ModifyInstanceMetadataOptions",
29 | "Resource": "*"
30 | },
31 | {
32 | "Sid": "MaxImdsHopLimit",
33 | "Effect": "Deny",
34 | "Action": "ec2:RunInstances",
35 | "Resource": "arn:aws:ec2:*:*:instance/*",
36 | "Condition": {
37 | "NumericGreaterThan": {
38 | "ec2:MetadataHttpPutResponseHopLimit": "3"
39 | }
40 | }
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/files/sns/iam_activity_topic_policy.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "__default_statement_ID",
6 | "Effect": "Allow",
7 | "Principal": {
8 | "AWS": "*"
9 | },
10 | "Action": [
11 | "SNS:Subscribe",
12 | "SNS:SetTopicAttributes",
13 | "SNS:RemovePermission",
14 | "SNS:Receive",
15 | "SNS:Publish",
16 | "SNS:ListSubscriptionsByTopic",
17 | "SNS:GetTopicAttributes",
18 | "SNS:DeleteTopic",
19 | "SNS:AddPermission"
20 | ],
21 | "Resource": "${sns_topic}",
22 | "Condition": {
23 | "StringEquals": {
24 | "AWS:SourceAccount": "${audit_account_id}"
25 | }
26 | }
27 | },
28 | {
29 | "Sid": "AllowServicesToPublishFromMgmtAccount",
30 | "Effect": "Allow",
31 | "Principal": {
32 | "Service": ${services_allowed_publish}
33 | },
34 | "Action": "sns:Publish",
35 | "Resource": "${sns_topic}",
36 | "Condition": {
37 | "StringEquals": {
38 | "AWS:SourceAccount": "${mgmt_account_id}"
39 | }
40 | }
41 | },
42 | {
43 | "Sid": "AllowMgmtMasterToListSubcriptions",
44 | "Effect": "Allow",
45 | "Principal": {
46 | "AWS": "arn:aws:iam::${mgmt_account_id}:root"
47 | },
48 | "Action": "sns:ListSubscriptionsByTopic",
49 | "Resource": "${sns_topic}"
50 | }
51 | %{ if length(security_hub_roles) > 0 ~}
52 | ,
53 | {
54 | "Sid": "AllowListSubscribersBySecurityHub",
55 | "Effect": "Allow",
56 | "Principal": {
57 | "AWS": [ ${join(", ", security_hub_roles)} ]
58 | },
59 | "Action": "sns:ListSubscriptionsByTopic",
60 | "Resource": "${sns_topic}"
61 | }
62 | %{ endif ~}
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/files/sns/security_hub_topic_policy.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "__default_statement_ID",
6 | "Effect": "Allow",
7 | "Principal": {
8 | "AWS": "*"
9 | },
10 | "Action": [
11 | "SNS:Subscribe",
12 | "SNS:SetTopicAttributes",
13 | "SNS:RemovePermission",
14 | "SNS:Receive",
15 | "SNS:Publish",
16 | "SNS:ListSubscriptionsByTopic",
17 | "SNS:GetTopicAttributes",
18 | "SNS:DeleteTopic",
19 | "SNS:AddPermission"
20 | ],
21 | "Resource": "${sns_topic}",
22 | "Condition": {
23 | "StringEquals": {
24 | "AWS:SourceAccount": "${account_id}"
25 | }
26 | }
27 | },
28 | {
29 | "Sid": "__services_allowed_publish",
30 | "Effect": "Allow",
31 | "Principal": {
32 | "Service": ${services_allowed_publish}
33 | },
34 | "Action": "sns:Publish",
35 | "Resource": "${sns_topic}"
36 | }
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/guardduty.tf:
--------------------------------------------------------------------------------
1 | // AWS GuardDuty - Management account configuration
2 | resource "aws_guardduty_organization_admin_account" "audit" {
3 | count = var.aws_guardduty.enabled == true ? 1 : 0
4 |
5 | admin_account_id = var.control_tower_account_ids.audit
6 | }
7 |
8 | // AWS GuardDuty - Audit account configuration
9 | resource "aws_guardduty_detector" "audit" {
10 | #checkov:skip=CKV_AWS_238: "Ensure that GuardDuty detector is enabled" - False positive, GuardDuty is enabled by default.
11 | #checkov:skip=CKV2_AWS_3: "Ensure GuardDuty is enabled to specific org/region" - False positive, GuardDuty is enabled by default.
12 | provider = aws.audit
13 |
14 | enable = var.aws_guardduty.enabled
15 | finding_publishing_frequency = var.aws_guardduty.finding_publishing_frequency
16 | tags = var.tags
17 | }
18 |
19 | resource "aws_guardduty_organization_configuration" "default" {
20 | count = var.aws_guardduty.enabled == true ? 1 : 0
21 | provider = aws.audit
22 |
23 | auto_enable_organization_members = var.aws_guardduty.enabled ? "ALL" : "NONE"
24 | detector_id = aws_guardduty_detector.audit.id
25 |
26 | depends_on = [aws_guardduty_organization_admin_account.audit]
27 | }
28 |
29 | resource "aws_guardduty_organization_configuration_feature" "ebs_malware_protection" {
30 | provider = aws.audit
31 |
32 | detector_id = aws_guardduty_detector.audit.id
33 | name = "EBS_MALWARE_PROTECTION"
34 | auto_enable = var.aws_guardduty.ebs_malware_protection_status == true ? "ALL" : "NONE"
35 | }
36 |
37 | resource "aws_guardduty_organization_configuration_feature" "eks_audit_logs" {
38 | provider = aws.audit
39 |
40 | detector_id = aws_guardduty_detector.audit.id
41 | name = "EKS_AUDIT_LOGS"
42 | auto_enable = var.aws_guardduty.eks_audit_logs_status == true ? "ALL" : "NONE"
43 | }
44 |
45 | resource "aws_guardduty_organization_configuration_feature" "lambda_network_logs" {
46 | provider = aws.audit
47 |
48 | detector_id = aws_guardduty_detector.audit.id
49 | name = "LAMBDA_NETWORK_LOGS"
50 | auto_enable = var.aws_guardduty.lambda_network_logs_status == true ? "ALL" : "NONE"
51 | }
52 |
53 | resource "aws_guardduty_organization_configuration_feature" "rds_login_events" {
54 | provider = aws.audit
55 |
56 | detector_id = aws_guardduty_detector.audit.id
57 | name = "RDS_LOGIN_EVENTS"
58 | auto_enable = var.aws_guardduty.rds_login_events_status == true ? "ALL" : "NONE"
59 | }
60 |
61 | resource "aws_guardduty_organization_configuration_feature" "s3_data_events" {
62 | provider = aws.audit
63 |
64 | detector_id = aws_guardduty_detector.audit.id
65 | name = "S3_DATA_EVENTS"
66 | auto_enable = var.aws_guardduty.s3_data_events_status == true ? "ALL" : "NONE"
67 | }
68 |
69 | resource "aws_guardduty_organization_configuration_feature" "runtime_monitoring" {
70 | provider = aws.audit
71 |
72 | detector_id = aws_guardduty_detector.audit.id
73 | name = "RUNTIME_MONITORING"
74 | auto_enable = var.aws_guardduty.runtime_monitoring_status.enabled == true ? "ALL" : "NONE"
75 |
76 | additional_configuration {
77 | name = "ECS_FARGATE_AGENT_MANAGEMENT"
78 | auto_enable = var.aws_guardduty.runtime_monitoring_status.ecs_fargate_agent_management_status == true ? "ALL" : "NONE"
79 | }
80 |
81 | additional_configuration {
82 | name = "EC2_AGENT_MANAGEMENT"
83 | auto_enable = var.aws_guardduty.runtime_monitoring_status.ec2_agent_management_status == true ? "ALL" : "NONE"
84 | }
85 |
86 | additional_configuration {
87 | name = "EKS_ADDON_MANAGEMENT"
88 | auto_enable = var.aws_guardduty.runtime_monitoring_status.eks_addon_management_status == true ? "ALL" : "NONE"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/iam_activity_logging.tf:
--------------------------------------------------------------------------------
1 | resource "aws_sns_topic" "iam_activity" {
2 | count = var.monitor_iam_activity ? 1 : 0
3 | provider = aws.audit
4 |
5 | name = "LandingZone-IAMActivity"
6 | http_success_feedback_role_arn = aws_iam_role.sns_feedback.arn
7 | http_failure_feedback_role_arn = aws_iam_role.sns_feedback.arn
8 | kms_master_key_id = module.kms_key_audit.id
9 | tags = var.tags
10 | }
11 |
12 | resource "aws_sns_topic_policy" "iam_activity" {
13 | count = var.monitor_iam_activity ? 1 : 0
14 | provider = aws.audit
15 |
16 | arn = aws_sns_topic.iam_activity[0].arn
17 |
18 | policy = templatefile("${path.module}/files/sns/iam_activity_topic_policy.json.tpl", {
19 | audit_account_id = data.aws_caller_identity.audit.account_id
20 | mgmt_account_id = data.aws_caller_identity.management.account_id
21 | services_allowed_publish = jsonencode("cloudwatch.amazonaws.com")
22 | sns_topic = aws_sns_topic.iam_activity[0].arn
23 |
24 | security_hub_roles = local.security_hub_has_cis_aws_foundations_enabled ? sort([
25 | for account_id, _ in local.aws_account_emails : "\"arn:aws:sts::${account_id}:assumed-role/AWSServiceRoleForSecurityHub/securityhub\""
26 | if account_id != var.control_tower_account_ids.audit
27 | ]) : []
28 | })
29 | }
30 |
31 | resource "aws_sns_topic_subscription" "iam_activity" {
32 | for_each = var.monitor_iam_activity ? var.monitor_iam_activity_sns_subscription : {}
33 | provider = aws.audit
34 |
35 | endpoint = each.value.endpoint
36 | endpoint_auto_confirms = length(regexall("http", each.value.protocol)) > 0
37 | protocol = each.value.protocol
38 | topic_arn = aws_sns_topic.iam_activity[0].arn
39 | }
40 |
41 | resource "aws_iam_role" "sns_feedback" {
42 | provider = aws.audit
43 |
44 | name = "LandingZone-SNSFeedback"
45 | path = var.path
46 | tags = var.tags
47 |
48 | assume_role_policy = templatefile("${path.module}/files/iam/service_assume_role.json.tpl", {
49 | service = "sns.amazonaws.com"
50 | })
51 | }
52 |
53 | data "aws_iam_policy_document" "sns_feedback" {
54 | statement {
55 | sid = "SNSFeedbackPolicy"
56 |
57 | actions = [
58 | "logs:CreateLogGroup",
59 | "logs:CreateLogStream",
60 | "logs:PutLogEvents",
61 | "logs:PutMetricFilter",
62 | "logs:PutRetentionPolicy"
63 | ]
64 |
65 | resources = compact([aws_sns_topic.security_hub_findings.arn, var.monitor_iam_activity ? aws_sns_topic.iam_activity[0].arn : null])
66 |
67 | condition {
68 | test = "StringEquals"
69 | variable = "AWS:SourceAccount"
70 | values = [data.aws_caller_identity.audit.account_id]
71 | }
72 | }
73 | }
74 |
75 | resource "aws_iam_role_policy" "sns_feedback_policy" {
76 | provider = aws.audit
77 |
78 | name = "LandingZone-SNSFeedbackPolicy"
79 | policy = data.aws_iam_policy_document.sns_feedback.json
80 | role = aws_iam_role.sns_feedback.id
81 | }
82 |
--------------------------------------------------------------------------------
/images/MCAF_landing_zone_tools_and_services_v040.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schubergphilis/terraform-aws-mcaf-landing-zone/d19518fcd8f94576aef0008f60f17f1a40d28f8f/images/MCAF_landing_zone_tools_and_services_v040.png
--------------------------------------------------------------------------------
/inspector.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | inspector_members_account_ids = var.aws_inspector.enabled ? [
3 | for account in data.aws_organizations_organization.default.accounts : account.id
4 | if account.id != var.control_tower_account_ids.audit
5 | && !contains(var.aws_inspector.excluded_member_account_ids, account.id)
6 | ] : []
7 |
8 | inspector_enabled_resource_types = var.aws_inspector.enabled ? compact([
9 | var.aws_inspector.enable_scan_ec2 ? "EC2" : "",
10 | var.aws_inspector.enable_scan_ecr ? "ECR" : "",
11 | var.aws_inspector.enable_scan_lambda ? "LAMBDA" : "",
12 | var.aws_inspector.enable_scan_lambda_code ? "LAMBDA_CODE" : "",
13 | ]) : []
14 | }
15 |
16 | // Delegate the admin account to the audit account
17 | resource "aws_inspector2_delegated_admin_account" "default" {
18 | count = var.aws_inspector.enabled == true ? 1 : 0
19 |
20 | account_id = var.control_tower_account_ids.audit
21 | }
22 |
23 | // Activate Inspector in the audit account
24 | resource "aws_inspector2_enabler" "audit_account" {
25 | count = var.aws_inspector.enabled == true ? 1 : 0
26 | provider = aws.audit
27 |
28 | account_ids = [var.control_tower_account_ids.audit]
29 | resource_types = local.inspector_enabled_resource_types
30 |
31 | depends_on = [aws_inspector2_delegated_admin_account.default]
32 | }
33 |
34 | // Associate the member accounts with the audit account
35 | resource "aws_inspector2_member_association" "default" {
36 | for_each = toset(local.inspector_members_account_ids)
37 | provider = aws.audit
38 |
39 | account_id = each.value
40 |
41 | depends_on = [aws_inspector2_enabler.audit_account]
42 | }
43 |
44 | // Activate Inspector in the member accounts
45 | resource "aws_inspector2_enabler" "member_accounts" {
46 | count = var.aws_inspector.enabled == true ? 1 : 0
47 | provider = aws.audit
48 |
49 | account_ids = toset(local.inspector_members_account_ids)
50 | resource_types = local.inspector_enabled_resource_types
51 |
52 | timeouts {
53 | create = var.aws_inspector.resource_create_timeout
54 | }
55 |
56 | depends_on = [aws_inspector2_member_association.default]
57 | }
58 |
59 | // Auto-enable Inspector in the new member accounts
60 | resource "aws_inspector2_organization_configuration" "default" {
61 | count = var.aws_inspector.enabled == true ? 1 : 0
62 | provider = aws.audit
63 |
64 | auto_enable {
65 | ec2 = var.aws_inspector.enable_scan_ec2
66 | ecr = var.aws_inspector.enable_scan_ecr
67 | lambda = var.aws_inspector.enable_scan_lambda
68 | lambda_code = var.aws_inspector.enable_scan_lambda_code
69 | }
70 |
71 | depends_on = [aws_inspector2_enabler.member_accounts]
72 | }
73 |
--------------------------------------------------------------------------------
/kms.tf:
--------------------------------------------------------------------------------
1 | # Management Account
2 | module "kms_key" {
3 | source = "schubergphilis/mcaf-kms/aws"
4 | version = "~> 0.3.0"
5 |
6 | name = "inception"
7 | description = "KMS key used in the master account"
8 | enable_key_rotation = true
9 | policy = data.aws_iam_policy_document.kms_key.json
10 | tags = var.tags
11 | }
12 |
13 | data "aws_iam_policy_document" "kms_key" {
14 | override_policy_documents = var.kms_key_policy
15 |
16 | statement {
17 | sid = "Base Permissions"
18 | actions = ["kms:*"]
19 | effect = "Allow"
20 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.management.account_id}:key/*"]
21 |
22 | principals {
23 | type = "AWS"
24 | identifiers = [
25 | "arn:aws:iam::${data.aws_caller_identity.management.account_id}:root"
26 | ]
27 | }
28 | }
29 |
30 | dynamic "statement" {
31 | for_each = var.ses_root_accounts_mail_forward != null ? ["allow_ses"] : []
32 | content {
33 | sid = "Allow SES Decrypt"
34 | effect = "Allow"
35 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.management.account_id}:key/*"]
36 |
37 | actions = [
38 | "kms:Decrypt",
39 | "kms:GenerateDataKey*"
40 | ]
41 |
42 | principals {
43 | type = "Service"
44 | identifiers = [
45 | "ses.amazonaws.com"
46 | ]
47 | }
48 | }
49 | }
50 |
51 | dynamic "statement" {
52 | for_each = var.ses_root_accounts_mail_forward != null ? ["allow_cw_loggroup_email_forwarder"] : []
53 | content {
54 | sid = "Allow EmailForwarder CloudWatch Log Group"
55 | effect = "Allow"
56 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.management.account_id}:key/*"]
57 |
58 | actions = [
59 | "kms:Decrypt",
60 | "kms:Describe*",
61 | "kms:Encrypt",
62 | "kms:GenerateDataKey*",
63 | "kms:ReEncrypt*"
64 | ]
65 |
66 | condition {
67 | test = "ArnLike"
68 | variable = "kms:EncryptionContext:aws:logs:arn"
69 |
70 | values = [
71 | "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.management.account_id}:log-group:/aws/lambda/EmailForwarder"
72 | ]
73 | }
74 |
75 | principals {
76 | type = "Service"
77 | identifiers = [
78 | "logs.${data.aws_region.current.name}.amazonaws.com"
79 | ]
80 | }
81 | }
82 | }
83 | }
84 |
85 | # Audit Account
86 | module "kms_key_audit" {
87 | providers = { aws = aws.audit }
88 |
89 | source = "schubergphilis/mcaf-kms/aws"
90 | version = "~> 0.3.0"
91 |
92 | name = "audit"
93 | description = "KMS key used for encrypting audit-related data"
94 | enable_key_rotation = true
95 | policy = data.aws_iam_policy_document.kms_key_audit.json
96 | tags = var.tags
97 | }
98 |
99 | data "aws_iam_policy_document" "kms_key_audit" {
100 | source_policy_documents = var.kms_key_policy_audit
101 |
102 | statement {
103 | sid = "Full permissions for the root user only"
104 | actions = ["kms:*"]
105 | effect = "Allow"
106 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
107 |
108 | condition {
109 | test = "StringEquals"
110 | variable = "aws:PrincipalType"
111 | values = ["Account"]
112 | }
113 |
114 | principals {
115 | type = "AWS"
116 | identifiers = [
117 | "arn:aws:iam::${data.aws_caller_identity.audit.account_id}:root"
118 | ]
119 | }
120 | }
121 |
122 | statement {
123 | sid = "Administrative permissions for pipeline"
124 | effect = "Allow"
125 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
126 |
127 | actions = [
128 | "kms:Create*",
129 | "kms:Describe*",
130 | "kms:Enable*",
131 | "kms:GenerateDataKey*",
132 | "kms:Get*",
133 | "kms:List*",
134 | "kms:Put*",
135 | "kms:Revoke*",
136 | "kms:TagResource",
137 | "kms:UntagResource",
138 | "kms:Update*"
139 | ]
140 |
141 | principals {
142 | type = "AWS"
143 | identifiers = [
144 | "arn:aws:iam::${data.aws_caller_identity.audit.account_id}:role/AWSControlTowerExecution"
145 | ]
146 | }
147 | }
148 |
149 | statement {
150 | sid = "List KMS keys permissions for all IAM users"
151 | effect = "Allow"
152 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
153 |
154 | actions = [
155 | "kms:Describe*",
156 | "kms:Get*",
157 | "kms:List*"
158 | ]
159 |
160 | principals {
161 | type = "AWS"
162 | identifiers = [
163 | "arn:aws:iam::${data.aws_caller_identity.audit.account_id}:root"
164 | ]
165 | }
166 | }
167 |
168 | statement {
169 | sid = "Allow CloudWatch Decrypt"
170 | effect = "Allow"
171 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
172 |
173 | actions = [
174 | "kms:Decrypt",
175 | "kms:GenerateDataKey"
176 | ]
177 |
178 | principals {
179 | type = "Service"
180 | identifiers = [
181 | "cloudwatch.amazonaws.com",
182 | "events.amazonaws.com"
183 | ]
184 | }
185 | }
186 |
187 | statement {
188 | sid = "Allow SNS Decrypt"
189 | effect = "Allow"
190 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
191 |
192 | actions = [
193 | "kms:Decrypt",
194 | "kms:GenerateDataKey"
195 | ]
196 |
197 | principals {
198 | type = "Service"
199 | identifiers = [
200 | "sns.amazonaws.com"
201 | ]
202 | }
203 | }
204 |
205 | dynamic "statement" {
206 | for_each = var.aws_auditmanager.enabled ? ["allow_audit_manager"] : []
207 |
208 | content {
209 | sid = "Allow Audit Manager from management to describe and grant"
210 | effect = "Allow"
211 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.audit.account_id}:key/*"]
212 |
213 | actions = [
214 | "kms:CreateGrant",
215 | "kms:DescribeKey"
216 | ]
217 |
218 | principals {
219 | type = "AWS"
220 | identifiers = [
221 | "arn:aws:iam::${data.aws_caller_identity.management.account_id}:root"
222 | ]
223 | }
224 |
225 | condition {
226 | test = "Bool"
227 | variable = "kms:ViaService"
228 |
229 | values = [
230 | "auditmanager.amazonaws.com"
231 | ]
232 | }
233 | }
234 | }
235 |
236 | dynamic "statement" {
237 | for_each = var.aws_auditmanager.enabled ? ["allow_audit_manager"] : []
238 | content {
239 | sid = "Encrypt and Decrypt permissions for S3"
240 | effect = "Allow"
241 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.management.account_id}:key/*"]
242 |
243 | actions = [
244 | "kms:Encrypt",
245 | "kms:Decrypt",
246 | "kms:ReEncrypt*",
247 | "kms:GenerateDataKey*"
248 | ]
249 |
250 | principals {
251 | type = "AWS"
252 | identifiers = [
253 | "arn:aws:iam::${data.aws_caller_identity.management.account_id}:root"
254 | ]
255 | }
256 |
257 | condition {
258 | test = "StringLike"
259 | variable = "kms:ViaService"
260 | values = [
261 | "s3.${data.aws_region.current.name}.amazonaws.com",
262 | ]
263 | }
264 | }
265 | }
266 | }
267 |
268 | # Logging Account
269 | module "kms_key_logging" {
270 | providers = { aws = aws.logging }
271 |
272 | source = "schubergphilis/mcaf-kms/aws"
273 | version = "~> 0.3.0"
274 |
275 | name = "logging"
276 | description = "KMS key to use with logging account"
277 | enable_key_rotation = true
278 | policy = data.aws_iam_policy_document.kms_key_logging.json
279 | tags = var.tags
280 | }
281 |
282 | data "aws_iam_policy_document" "kms_key_logging" {
283 | source_policy_documents = var.kms_key_policy_logging
284 |
285 | statement {
286 | sid = "Full permissions for the root user only"
287 | actions = ["kms:*"]
288 | effect = "Allow"
289 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.logging.account_id}:key/*"]
290 |
291 | condition {
292 | test = "StringEquals"
293 | variable = "aws:PrincipalType"
294 | values = ["Account"]
295 | }
296 |
297 | principals {
298 | type = "AWS"
299 | identifiers = [
300 | "arn:aws:iam::${data.aws_caller_identity.logging.account_id}:root"
301 | ]
302 | }
303 | }
304 |
305 | statement {
306 | sid = "Administrative permissions for pipeline"
307 | effect = "Allow"
308 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.logging.account_id}:key/*"]
309 |
310 | actions = [
311 | "kms:Create*",
312 | "kms:Describe*",
313 | "kms:Enable*",
314 | "kms:Get*",
315 | "kms:List*",
316 | "kms:Put*",
317 | "kms:Revoke*",
318 | "kms:TagResource",
319 | "kms:UntagResource",
320 | "kms:Update*"
321 | ]
322 |
323 | principals {
324 | type = "AWS"
325 | identifiers = [
326 | "arn:aws:iam::${data.aws_caller_identity.logging.account_id}:role/AWSControlTowerExecution"
327 | ]
328 | }
329 | }
330 |
331 | statement {
332 | sid = "List KMS keys permissions for all IAM users"
333 | effect = "Allow"
334 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.logging.account_id}:key/*"]
335 |
336 | actions = [
337 | "kms:Describe*",
338 | "kms:Get*",
339 | "kms:List*"
340 | ]
341 |
342 | principals {
343 | type = "AWS"
344 | identifiers = [
345 | "arn:aws:iam::${data.aws_caller_identity.logging.account_id}:root"
346 | ]
347 | }
348 | }
349 |
350 | statement {
351 | sid = "KMS permissions for AWS logs service"
352 | effect = "Allow"
353 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.logging.account_id}:key/*"]
354 |
355 | actions = [
356 | "kms:Encrypt",
357 | "kms:Decrypt",
358 | "kms:ReEncrypt*",
359 | "kms:GenerateDataKey*",
360 | "kms:DescribeKey",
361 | ]
362 |
363 | principals {
364 | type = "Service"
365 | identifiers = ["logs.${data.aws_region.current.name}.amazonaws.com"]
366 | }
367 | }
368 |
369 | statement {
370 | sid = "AllowAWSConfigToEncryptDecryptLogs"
371 | effect = "Allow"
372 | resources = ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.logging.account_id}:key/*"]
373 |
374 | actions = [
375 | "kms:Decrypt",
376 | "kms:GenerateDataKey*"
377 | ]
378 |
379 | principals {
380 | type = "Service"
381 | identifiers = [
382 | "config.amazonaws.com"
383 | ]
384 | }
385 | }
386 | }
387 |
--------------------------------------------------------------------------------
/locals.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | aws_account_emails = { for account in data.aws_organizations_organization.default.accounts : account.id => account.email }
3 |
4 | iam_activity = {
5 | SSO = "{$.readOnly IS FALSE && $.userIdentity.sessionContext.sessionIssuer.userName = \"AWSReservedSSO_*\" && $.eventName != \"ConsoleLogin\"}"
6 | }
7 |
8 | cloudtrail_activity_cis_aws_foundations = (local.security_hub_has_cis_aws_foundations_enabled && var.aws_security_hub.create_cis_metric_filters) ? {
9 | RootActivity = "{$.userIdentity.type=\"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\"}"
10 | UnauthorizedApiCalls = "{($.errorCode=\"*UnauthorizedOperation\") || ($.errorCode=\"AccessDenied*\")}"
11 | IamPolicyChanges = "{($.eventName=DeleteGroupPolicy) || ($.eventName=DeleteRolePolicy) || ($.eventName=DeleteUserPolicy) || ($.eventName=PutGroupPolicy) || ($.eventName=PutRolePolicy) || ($.eventName=PutUserPolicy) || ($.eventName=CreatePolicy) || ($.eventName=DeletePolicy) || ($.eventName=CreatePolicyVersion) || ($.eventName=DeletePolicyVersion) || ($.eventName=AttachRolePolicy) || ($.eventName=DetachRolePolicy) || ($.eventName=AttachUserPolicy) || ($.eventName=DetachUserPolicy) || ($.eventName=AttachGroupPolicy) || ($.eventName=DetachGroupPolicy)}"
12 | CloudTrailConfigChange = "{($.eventName=CreateTrail) || ($.eventName=UpdateTrail) || ($.eventName=DeleteTrail) || ($.eventName=StartLogging) || ($.eventName=StopLogging)}"
13 | ManagementConsoleAuthFailure = "{($.eventName=ConsoleLogin) && ($.errorMessage=\"Failed authentication\")}"
14 | KmsKeyDisableOrDeletion = "{($.eventSource=kms.amazonaws.com) && (($.eventName=DisableKey) || ($.eventName=ScheduleKeyDeletion))}"
15 | S3BucketPolicyChange = "{($.eventSource=s3.amazonaws.com) && (($.eventName=PutBucketAcl) || ($.eventName=PutBucketPolicy) || ($.eventName=PutBucketCors) || ($.eventName=PutBucketLifecycle) || ($.eventName=PutBucketReplication) || ($.eventName=DeleteBucketPolicy) || ($.eventName=DeleteBucketCors) || ($.eventName=DeleteBucketLifecycle) || ($.eventName=DeleteBucketReplication))}"
16 | AwsConfigConfigChange = "{($.eventSource=config.amazonaws.com) && (($.eventName=StopConfigurationRecorder) || ($.eventName=DeleteDeliveryChannel) || ($.eventName=PutDeliveryChannel) || ($.eventName=PutConfigurationRecorder))}"
17 | SecurityGroupChange = "{($.eventName=AuthorizeSecurityGroupIngress) || ($.eventName=AuthorizeSecurityGroupEgress) || ($.eventName=RevokeSecurityGroupIngress) || ($.eventName=RevokeSecurityGroupEgress) || ($.eventName=CreateSecurityGroup) || ($.eventName=DeleteSecurityGroup)}"
18 | NaclChange = "{($.eventName=CreateNetworkAcl) || ($.eventName=CreateNetworkAclEntry) || ($.eventName=DeleteNetworkAcl) || ($.eventName=DeleteNetworkAclEntry) || ($.eventName=ReplaceNetworkAclEntry) || ($.eventName=ReplaceNetworkAclAssociation)}"
19 | NetworkGatewayChange = "{($.eventName=CreateCustomerGateway) || ($.eventName=DeleteCustomerGateway) || ($.eventName=AttachInternetGateway) || ($.eventName=CreateInternetGateway) || ($.eventName=DeleteInternetGateway) || ($.eventName=DetachInternetGateway)}"
20 | RouteTableChange = "{($.eventName=CreateRoute) || ($.eventName=CreateRouteTable) || ($.eventName=ReplaceRoute) || ($.eventName=ReplaceRouteTableAssociation) || ($.eventName=DeleteRouteTable) || ($.eventName=DeleteRoute) || ($.eventName=DisassociateRouteTable)}"
21 | VpcChange = "{($.eventName=CreateVpc) || ($.eventName=DeleteVpc) || ($.eventName=ModifyVpcAttribute) || ($.eventName=AcceptVpcPeeringConnection) || ($.eventName=CreateVpcPeeringConnection) || ($.eventName=DeleteVpcPeeringConnection) || ($.eventName=RejectVpcPeeringConnection) || ($.eventName=AttachClassicLinkVpc) || ($.eventName=DetachClassicLinkVpc) || ($.eventName=DisableVpcClassicLink) || ($.eventName=EnableVpcClassicLink)}"
22 | } : {}
23 |
24 | aws_service_control_policies_principal_exceptions = distinct(concat(var.aws_service_control_policies.principal_exceptions, ["arn:aws:iam::*:role/AWSControlTowerExecution"]))
25 |
26 | security_hub_standards_arns_default = [
27 | "arn:aws:securityhub:${data.aws_region.current.name}::standards/aws-foundational-security-best-practices/v/1.0.0",
28 | "arn:aws:securityhub:${data.aws_region.current.name}::standards/cis-aws-foundations-benchmark/v/1.4.0",
29 | "arn:aws:securityhub:${data.aws_region.current.name}::standards/pci-dss/v/3.2.1"
30 | ]
31 |
32 | security_hub_standards_arns = var.aws_security_hub.standards_arns != null ? var.aws_security_hub.standards_arns : local.security_hub_standards_arns_default
33 |
34 | security_hub_has_cis_aws_foundations_enabled = length(regexall(
35 | "cis-aws-foundations-benchmark/v", join(",", local.security_hub_standards_arns)
36 | )) > 0 ? true : false
37 | all_organisation_regions = toset(distinct(concat([var.regions.home_region], var.regions.linked_regions, var.regions.allowed_regions, [data.aws_region.current.name])))
38 | }
39 |
--------------------------------------------------------------------------------
/modules/aws-config-recorder/README.md:
--------------------------------------------------------------------------------
1 | # AWS Config Recorder
2 |
3 | Terraform module to create an AWS Config Recorder.
4 |
5 |
6 | ## Requirements
7 |
8 | | Name | Version |
9 | |------|---------|
10 | | [terraform](#requirement\_terraform) | >= 1.3 |
11 | | [aws](#requirement\_aws) | >= 4.9.0 |
12 |
13 | ## Providers
14 |
15 | | Name | Version |
16 | |------|---------|
17 | | [aws](#provider\_aws) | >= 4.9.0 |
18 |
19 | ## Modules
20 |
21 | No modules.
22 |
23 | ## Resources
24 |
25 | | Name | Type |
26 | |------|------|
27 | | [aws_config_configuration_recorder.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/config_configuration_recorder) | resource |
28 | | [aws_config_configuration_recorder_status.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/config_configuration_recorder_status) | resource |
29 | | [aws_config_delivery_channel.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/config_delivery_channel) | resource |
30 |
31 | ## Inputs
32 |
33 | | Name | Description | Type | Default | Required |
34 | |------|-------------|------|---------|:--------:|
35 | | [iam\_service\_linked\_role\_arn](#input\_iam\_service\_linked\_role\_arn) | The ARN for the AWS Config IAM Service Linked Role | `string` | n/a | yes |
36 | | [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN of the KMS key for AWS Config | `string` | n/a | yes |
37 | | [s3\_bucket\_name](#input\_s3\_bucket\_name) | The name of the S3 bucket for AWS Config | `string` | n/a | yes |
38 | | [s3\_key\_prefix](#input\_s3\_key\_prefix) | The S3 key prefix for AWS Config | `string` | n/a | yes |
39 | | [sns\_topic\_arn](#input\_sns\_topic\_arn) | The ARN of the SNS topic that AWS Config delivers notifications to. | `string` | n/a | yes |
40 | | [delivery\_frequency](#input\_delivery\_frequency) | The frequency with which AWS Config recurringly delivers configuration snapshots | `string` | `"TwentyFour_Hours"` | no |
41 |
42 | ## Outputs
43 |
44 | No outputs.
45 |
--------------------------------------------------------------------------------
/modules/aws-config-recorder/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_config_configuration_recorder" "default" {
2 | name = "default"
3 | role_arn = var.iam_service_linked_role_arn
4 |
5 | recording_group {
6 | all_supported = true
7 | include_global_resource_types = true
8 | }
9 | }
10 |
11 | resource "aws_config_delivery_channel" "default" {
12 | name = "default"
13 | s3_bucket_name = var.s3_bucket_name
14 | s3_key_prefix = var.s3_key_prefix
15 | s3_kms_key_arn = var.kms_key_arn
16 | sns_topic_arn = var.sns_topic_arn
17 |
18 | snapshot_delivery_properties {
19 | delivery_frequency = var.delivery_frequency
20 | }
21 |
22 | depends_on = [aws_config_configuration_recorder.default]
23 | }
24 |
25 | resource "aws_config_configuration_recorder_status" "default" {
26 | name = aws_config_configuration_recorder.default.name
27 | is_enabled = true
28 | depends_on = [aws_config_delivery_channel.default]
29 | }
30 |
--------------------------------------------------------------------------------
/modules/aws-config-recorder/terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 4.9.0"
6 | }
7 | }
8 | required_version = ">= 1.3"
9 | }
10 |
--------------------------------------------------------------------------------
/modules/aws-config-recorder/variables.tf:
--------------------------------------------------------------------------------
1 | variable "delivery_frequency" {
2 | type = string
3 | description = "The frequency with which AWS Config recurringly delivers configuration snapshots"
4 | default = "TwentyFour_Hours"
5 | }
6 |
7 | variable "iam_service_linked_role_arn" {
8 | type = string
9 | description = "The ARN for the AWS Config IAM Service Linked Role"
10 | }
11 |
12 | variable "kms_key_arn" {
13 | type = string
14 | description = "The ARN of the KMS key for AWS Config"
15 | }
16 |
17 | variable "s3_bucket_name" {
18 | type = string
19 | description = "The name of the S3 bucket for AWS Config"
20 | }
21 |
22 | variable "s3_key_prefix" {
23 | type = string
24 | description = "The S3 key prefix for AWS Config"
25 | }
26 |
27 | variable "sns_topic_arn" {
28 | type = string
29 | description = "The ARN of the SNS topic that AWS Config delivers notifications to."
30 | }
31 |
--------------------------------------------------------------------------------
/modules/permission-set/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/modules/permission-set/README.md:
--------------------------------------------------------------------------------
1 | # AWS IAM Identity Center Permission Set
2 |
3 | Terraform module to create and/or manage a permission set in AWS IAM Identity Center (previously known as AWS SSO).
4 |
5 | ## Usage
6 |
7 | This module creates a permission set, or uses an existing permission set, and manages its account and group assignments.
8 |
9 | To just create a permissions set for it to be managed elsewhere:
10 |
11 | ```hcl
12 | module "permission_set" {
13 | source = "https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone//modules/permission-set"
14 |
15 | name = "PlatformAdmin"
16 | managed_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
17 | }
18 | ```
19 |
20 | Use `assignments` to assign this accounts and groups combinations to the created permission set:
21 |
22 | ```hcl
23 | module "permission_set" {
24 | source = "https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone//modules/permission-set"
25 |
26 | name = "PlatformAdmin"
27 | managed_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
28 |
29 | assignments = [
30 | {
31 | for account in ["11111111111", "22222222222", "33333333333"] :
32 | account => ["SSOgroup1"]
33 | },
34 | ]
35 | }
36 | ```
37 |
38 | To manage an existing permission set, set `create` to `false`:
39 |
40 | ```hcl
41 | module "permission_set" {
42 | source = "https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone//modules/permission-set"
43 |
44 | name = "PlatformAdmin"
45 | create = false
46 | managed_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
47 |
48 | assignments = [
49 | {
50 | for account in ["11111111111", "22222222222", "33333333333"] :
51 | account => ["SSOgroup1"]
52 | },
53 | ]
54 | }
55 | ```
56 |
57 | Add additional assignments to reuse the permission set across different account and SSO group combinations:
58 |
59 | ```hcl
60 | module "permission_set" {
61 | source = "https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone//modules/permission-set"
62 |
63 | name = "PlatformAdmin"
64 | managed_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
65 |
66 | assignments = [
67 | {
68 | for account in ["11111111111", "22222222222", "33333333333"] :
69 | account => ["SSOgroup1"]
70 | },
71 | {
72 | for account in ["33333333333", "44444444444"] :
73 | account => ["SSOgroup2"]
74 | },
75 | ]
76 | }
77 | ```
78 |
79 | It's also possible to create your own inline policy instead if more fine grained access is required:
80 |
81 | ```hcl
82 | module "permission_set" {
83 | source = "https://github.com/schubergphilis/terraform-aws-mcaf-landing-zone//modules/permission-set"
84 |
85 | name = "PlatformAdmin"
86 | managed_arns = ["arn:aws:iam::aws:policy/AdministratorAccess"]
87 |
88 | assignments = [
89 | {
90 | for account in ["11111111111", "22222222222", "33333333333"] :
91 | account => ["SSOgroup1"]
92 | },
93 | ]
94 |
95 | inline_policy = jsonencode({
96 | "Version" = "2012-10-17"
97 | "Statement" = jsondecode(file("${path.module}/templates/sso_platform_admin.json.tftpl"))
98 | })
99 | }
100 | ```
101 |
102 |
103 | ## Requirements
104 |
105 | | Name | Version |
106 | |------|---------|
107 | | terraform | >= 1.3 |
108 | | aws | >= 4.9.0 |
109 |
110 | ## Providers
111 |
112 | | Name | Version |
113 | |------|---------|
114 | | aws | >= 4.9.0 |
115 |
116 | ## Inputs
117 |
118 | | Name | Description | Type | Default | Required |
119 | |------|-------------|------|---------|:--------:|
120 | | name | Name of the permission set | `string` | n/a | yes |
121 | | assignments | List of account IDs and Identity Center groups to assign to the permission set | `list(map(list(string)))` | `[]` | no |
122 | | create | Set to false to only manage assignments when the permission set already exists | `bool` | `true` | no |
123 | | inline\_policy | The IAM inline policy to attach to a permission set | `string` | `null` | no |
124 | | managed\_policy\_arns | List of IAM managed policy ARNs to be attached to the permission set | `list(string)` | `[]` | no |
125 | | module\_depends\_on | A list of external resources the module depends\_on | `any` | `[]` | no |
126 | | session\_duration | The length of time that the application user sessions are valid in the ISO-8601 standard | `string` | `"PT4H"` | no |
127 |
128 | ## Outputs
129 |
130 | No output.
131 |
132 |
133 |
134 | ## License
135 |
136 | **Copyright:** Schuberg Philis
137 |
138 | ```text
139 | Licensed under the Apache License, Version 2.0 (the "License");
140 | you may not use this file except in compliance with the License.
141 | You may obtain a copy of the License at
142 | http://www.apache.org/licenses/LICENSE-2.0
143 | Unless required by applicable law or agreed to in writing, software
144 | distributed under the License is distributed on an "AS IS" BASIS,
145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
146 | See the License for the specific language governing permissions and
147 | limitations under the License.
148 | ```
149 |
150 |
151 | ## Requirements
152 |
153 | | Name | Version |
154 | |------|---------|
155 | | [terraform](#requirement\_terraform) | >= 1.3 |
156 | | [aws](#requirement\_aws) | >= 4.9.0 |
157 |
158 | ## Providers
159 |
160 | | Name | Version |
161 | |------|---------|
162 | | [aws](#provider\_aws) | >= 4.9.0 |
163 |
164 | ## Modules
165 |
166 | No modules.
167 |
168 | ## Resources
169 |
170 | | Name | Type |
171 | |------|------|
172 | | [aws_ssoadmin_account_assignment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_account_assignment) | resource |
173 | | [aws_ssoadmin_managed_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_managed_policy_attachment) | resource |
174 | | [aws_ssoadmin_permission_set.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_permission_set) | resource |
175 | | [aws_ssoadmin_permission_set_inline_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_permission_set_inline_policy) | resource |
176 | | [aws_identitystore_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/identitystore_group) | data source |
177 | | [aws_ssoadmin_instances.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssoadmin_instances) | data source |
178 | | [aws_ssoadmin_permission_set.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssoadmin_permission_set) | data source |
179 |
180 | ## Inputs
181 |
182 | | Name | Description | Type | Default | Required |
183 | |------|-------------|------|---------|:--------:|
184 | | [name](#input\_name) | Name of the permission set | `string` | n/a | yes |
185 | | [assignments](#input\_assignments) | List of account names and IDs and Identity Center groups to assign to the permission set | list(object({
account_id = string
account_name = string
sso_groups = list(string)
}))
| `[]` | no |
186 | | [create](#input\_create) | Set to false to only manage assignments when the permission set already exists | `bool` | `true` | no |
187 | | [inline\_policy](#input\_inline\_policy) | The IAM inline policy to attach to a permission set | `string` | `null` | no |
188 | | [managed\_policy\_arns](#input\_managed\_policy\_arns) | List of IAM managed policy ARNs to be attached to the permission set | `list(string)` | `[]` | no |
189 | | [module\_depends\_on](#input\_module\_depends\_on) | A list of external resources the module depends\_on | `any` | `[]` | no |
190 | | [session\_duration](#input\_session\_duration) | The length of time that the application user sessions are valid in the ISO-8601 standard | `string` | `"PT4H"` | no |
191 |
192 | ## Outputs
193 |
194 | No outputs.
195 |
--------------------------------------------------------------------------------
/modules/permission-set/main.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | aws_sso_account_assignments = flatten([
3 | for assignment in var.assignments : [
4 | for sso_group in assignment.sso_groups : {
5 | aws_account_id = assignment.account_id
6 | aws_account_name = assignment.account_name
7 | sso_group = sso_group
8 | }
9 | ]
10 | ])
11 | }
12 |
13 | data "aws_ssoadmin_instances" "default" {}
14 |
15 | data "aws_identitystore_group" "default" {
16 | for_each = toset(flatten([
17 | for assignment in var.assignments : assignment.sso_groups
18 | ]))
19 |
20 | identity_store_id = tolist(data.aws_ssoadmin_instances.default.identity_store_ids)[0]
21 |
22 | alternate_identifier {
23 | unique_attribute {
24 | attribute_path = "DisplayName"
25 | attribute_value = each.value
26 | }
27 | }
28 |
29 | depends_on = [
30 | var.module_depends_on
31 | ]
32 | }
33 |
34 | data "aws_ssoadmin_permission_set" "default" {
35 | count = var.create ? 0 : 1
36 |
37 | instance_arn = tolist(data.aws_ssoadmin_instances.default.arns)[0]
38 | name = var.name
39 |
40 | depends_on = [
41 | var.module_depends_on
42 | ]
43 | }
44 |
45 | resource "aws_ssoadmin_permission_set" "default" {
46 | count = var.create ? 1 : 0
47 |
48 | name = var.name
49 | instance_arn = tolist(data.aws_ssoadmin_instances.default.arns)[0]
50 | session_duration = var.session_duration
51 |
52 | depends_on = [
53 | var.module_depends_on
54 | ]
55 | }
56 |
57 | resource "aws_ssoadmin_account_assignment" "default" {
58 | for_each = {
59 | for assignment in local.aws_sso_account_assignments :
60 | "${assignment.sso_group}:${assignment.aws_account_name}" => assignment
61 | }
62 |
63 | instance_arn = var.create ? aws_ssoadmin_permission_set.default[0].instance_arn : data.aws_ssoadmin_permission_set.default[0].instance_arn
64 | permission_set_arn = var.create ? aws_ssoadmin_permission_set.default[0].arn : data.aws_ssoadmin_permission_set.default[0].arn
65 | principal_id = data.aws_identitystore_group.default[each.value.sso_group].group_id
66 | principal_type = "GROUP"
67 | target_id = each.value.aws_account_id
68 | target_type = "AWS_ACCOUNT"
69 |
70 | depends_on = [
71 | var.module_depends_on
72 | ]
73 | }
74 |
75 | resource "aws_ssoadmin_permission_set_inline_policy" "default" {
76 | count = var.inline_policy != null ? 1 : 0
77 |
78 | inline_policy = var.inline_policy
79 | instance_arn = aws_ssoadmin_permission_set.default[0].instance_arn
80 | permission_set_arn = aws_ssoadmin_permission_set.default[0].arn
81 |
82 | depends_on = [
83 | var.module_depends_on
84 | ]
85 | }
86 |
87 | resource "aws_ssoadmin_managed_policy_attachment" "default" {
88 | for_each = toset(var.managed_policy_arns)
89 |
90 | instance_arn = aws_ssoadmin_permission_set.default[0].instance_arn
91 | managed_policy_arn = each.value
92 | permission_set_arn = aws_ssoadmin_permission_set.default[0].arn
93 |
94 | depends_on = [
95 | var.module_depends_on
96 | ]
97 | }
98 |
--------------------------------------------------------------------------------
/modules/permission-set/terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 4.9.0"
6 | }
7 | }
8 | required_version = ">= 1.3"
9 | }
10 |
--------------------------------------------------------------------------------
/modules/permission-set/variables.tf:
--------------------------------------------------------------------------------
1 | variable "name" {
2 | type = string
3 | description = "Name of the permission set"
4 | }
5 |
6 | variable "assignments" {
7 | type = list(object({
8 | account_id = string
9 | account_name = string
10 | sso_groups = list(string)
11 | }))
12 | default = []
13 | description = "List of account names and IDs and Identity Center groups to assign to the permission set"
14 | }
15 |
16 | variable "create" {
17 | type = bool
18 | default = true
19 | description = "Set to false to only manage assignments when the permission set already exists"
20 | }
21 |
22 | variable "inline_policy" {
23 | type = string
24 | default = null
25 | description = "The IAM inline policy to attach to a permission set"
26 | }
27 |
28 | variable "module_depends_on" {
29 | type = any
30 | description = "A list of external resources the module depends_on"
31 | default = []
32 | }
33 |
34 | variable "managed_policy_arns" {
35 | type = list(string)
36 | default = []
37 | description = "List of IAM managed policy ARNs to be attached to the permission set"
38 | }
39 |
40 | variable "session_duration" {
41 | type = string
42 | default = "PT4H"
43 | description = "The length of time that the application user sessions are valid in the ISO-8601 standard"
44 | }
45 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/README.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | ## Requirements
4 |
5 | | Name | Version |
6 | |------|---------|
7 | | terraform | >= 1.3 |
8 | | aws | >= 4.9.0 |
9 |
10 | ## Providers
11 |
12 | | Name | Version |
13 | |------|---------|
14 | | aws | >= 4.9.0 |
15 |
16 | ## Inputs
17 |
18 | | Name | Description | Type | Default | Required |
19 | |------|-------------|------|---------|:--------:|
20 | | aws\_ou\_tags | Map of AWS OU names and their tag policies | map(object({
values = optional(list(string))
enforced_for = optional(list(string))
}))
| n/a | yes |
21 | | ou\_path | Path of the organizational unit (OU) | `string` | n/a | yes |
22 | | target\_id | The unique identifier (ID) organizational unit (OU) that you want to attach the policy to. | `string` | n/a | yes |
23 | | tags | Map of AWS resource tags | `map(string)` | `{}` | no |
24 |
25 | ## Outputs
26 |
27 | No output.
28 |
29 |
30 |
31 |
32 | ## Requirements
33 |
34 | | Name | Version |
35 | |------|---------|
36 | | [terraform](#requirement\_terraform) | >= 1.3 |
37 | | [aws](#requirement\_aws) | >= 4.9.0 |
38 |
39 | ## Providers
40 |
41 | | Name | Version |
42 | |------|---------|
43 | | [aws](#provider\_aws) | >= 4.9.0 |
44 |
45 | ## Modules
46 |
47 | No modules.
48 |
49 | ## Resources
50 |
51 | | Name | Type |
52 | |------|------|
53 | | [aws_organizations_policy.required_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_policy) | resource |
54 | | [aws_organizations_policy_attachment.required_tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/organizations_policy_attachment) | resource |
55 |
56 | ## Inputs
57 |
58 | | Name | Description | Type | Default | Required |
59 | |------|-------------|------|---------|:--------:|
60 | | [aws\_ou\_tags](#input\_aws\_ou\_tags) | Map of AWS OU names and their tag policies | map(object({
values = optional(list(string))
enforced_for = optional(list(string))
}))
| n/a | yes |
61 | | [ou\_path](#input\_ou\_path) | Path of the organizational unit (OU) | `string` | n/a | yes |
62 | | [target\_id](#input\_target\_id) | The unique identifier (ID) organizational unit (OU) that you want to attach the policy to. | `string` | n/a | yes |
63 | | [tags](#input\_tags) | Map of AWS resource tags | `map(string)` | `{}` | no |
64 |
65 | ## Outputs
66 |
67 | No outputs.
68 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/UPDATING.md:
--------------------------------------------------------------------------------
1 | # Updating
2 |
3 | To update the `all_enforced_services` local variable, copy all services from the [services and resource types that support enforcement](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_supported-resources-enforcement.html) page into a file called `all-services-page.txt` and run the following:
4 |
5 | ```shell
6 | grep '^\"' all-services-page.txt | awk -F':' '{ print $1":*\"\," }' | sort -n | uniq
7 | ```
8 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/locals.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | all_enforced_services = [
3 | "acm-pca:certificate-authority",
4 | "acm:*",
5 | "amplifyuibuilder:app/environment/components",
6 | "amplifyuibuilder:app/environment/themes",
7 | "aoss:collection",
8 | "apigateway:apikeys",
9 | "apigateway:domainnames",
10 | "apigateway:restapis",
11 | "apigateway:restapis/stages",
12 | "appconfig:application",
13 | "appconfig:application/configurationprofile",
14 | "appconfig:application/environment",
15 | "appconfig:application/environment/deployment",
16 | "appconfig:deploymentstrategy",
17 | "appmesh:*",
18 | "athena:*",
19 | "auditmanager:assessment",
20 | "auditmanager:assessmentFramework",
21 | "auditmanager:control",
22 | "backup-gateway:gateway",
23 | "backup-gateway:hypervisor",
24 | "backup-gateway:vm",
25 | "backup:backup-plan",
26 | "backup:backup-vault",
27 | "batch:job",
28 | "batch:job-definition",
29 | "batch:job-queue",
30 | "bugbust:event",
31 | "catalog:portfolio",
32 | "catalog:product",
33 | "chime:app-instance",
34 | "chime:app-instance/channel",
35 | "chime:app-instance/user",
36 | "chime:media-pipeline",
37 | "chime:meeting",
38 | "cleanrooms:collaboration",
39 | "cleanrooms:configuredtable",
40 | "cleanrooms:membership",
41 | "cleanrooms:membership/configuredtableassociation",
42 | "cloud9:environment",
43 | "cloudfront:*",
44 | "cloudtrail:*",
45 | "cloudwatch:*",
46 | "codebuild:*",
47 | "codecatalyst:connections",
48 | "codecommit:*",
49 | "codeguru-reviewer:association",
50 | "codepipeline:*",
51 | "codestar-connections:connection",
52 | "codestar-connections:host",
53 | "cognito-identity:*",
54 | "cognito-idp:*",
55 | "comprehend:*",
56 | "config:*",
57 | "connect:instance/agent",
58 | "connect:instance/contact-flow",
59 | "connect:instance/integration-association",
60 | "connect:instance/queue",
61 | "connect:instance/routing-profile",
62 | "connect:instance/transfer-destination",
63 | "diode-messaging:mapping",
64 | "directconnect:*",
65 | "dlm:policy",
66 | "dms:*",
67 | "dynamodb:*",
68 | "ec2:capacity-reservation",
69 | "ec2:client-vpn-endpoint",
70 | "ec2:customer-gateway",
71 | "ec2:dhcp-options",
72 | "ec2:elastic-ip",
73 | "ec2:fleet",
74 | "ec2:fpga-image",
75 | "ec2:host-reservation",
76 | "ec2:image",
77 | "ec2:instance",
78 | "ec2:internet-gateway",
79 | "ec2:launch-template",
80 | "ec2:natgateway",
81 | "ec2:network-acl",
82 | "ec2:network-interface",
83 | "ec2:reserved-instances",
84 | "ec2:route-table",
85 | "ec2:security-group",
86 | "ec2:snapshot",
87 | "ec2:spot-instances-request",
88 | "ec2:subnet",
89 | "ec2:traffic-mirror-filter",
90 | "ec2:traffic-mirror-session",
91 | "ec2:traffic-mirror-target",
92 | "ec2:volume",
93 | "ec2:vpc",
94 | "ec2:vpc-endpoint",
95 | "ec2:vpc-endpoint-service",
96 | "ec2:vpc-peering-connection",
97 | "ec2:vpn-connection",
98 | "ec2:vpn-gateway",
99 | "ecr:repository",
100 | "ecs:cluster",
101 | "ecs:service",
102 | "ecs:task-set",
103 | "eks:cluster",
104 | "elastic-inference:elastic-inference-accelerator",
105 | "elasticache:cluster",
106 | "elasticbeanstalk:application",
107 | "elasticbeanstalk:applicationversion",
108 | "elasticbeanstalk:configurationtemplate",
109 | "elasticbeanstalk:platform",
110 | "elasticfilesystem:*",
111 | "elasticloadbalancing:*",
112 | "elasticmapreduce:cluster",
113 | "elasticmapreduce:editor",
114 | "emr-serverless:applications",
115 | "es:domain",
116 | "events:*",
117 | "firehose:*",
118 | "frauddetector:detector",
119 | "frauddetector:detector-version",
120 | "frauddetector:model",
121 | "frauddetector:rule",
122 | "frauddetector:variable",
123 | "fsx:*",
124 | "globalaccelerator:accelerator",
125 | "greengrass:bulk",
126 | "greengrass:connectorsDefinition",
127 | "greengrass:coresDefinition",
128 | "greengrass:devicesDefinition",
129 | "greengrass:functionsDefinition",
130 | "greengrass:loggersDefinition",
131 | "greengrass:resourcesDefinition",
132 | "greengrass:subscriptionsDefinition",
133 | "guardduty:detector",
134 | "guardduty:detector/filter",
135 | "guardduty:detector/ipset",
136 | "guardduty:detector/threatintelset",
137 | "healthlake:datastore",
138 | "iam:instance-profile",
139 | "iam:mfa",
140 | "iam:oidc-provider",
141 | "iam:policy",
142 | "iam:saml-provider",
143 | "iam:server-certificate",
144 | "inspector2:filter",
145 | "internetmonitor:monitor",
146 | "iotanalytics:*",
147 | "iotevents:*",
148 | "iotfleethub:application",
149 | "iotroborunner:site",
150 | "iotroborunner:site/destination",
151 | "iotroborunner:site/worker-fleet",
152 | "iotroborunner:site/worker-fleet/worker",
153 | "iotsitewise:asset",
154 | "iotsitewise:asset-model",
155 | "kinesisanalytics:*",
156 | "kms:*",
157 | "lambda:*",
158 | "logs:log-group",
159 | "macie2:custom-data-identifier",
160 | "mediastore:container",
161 | "mq:broker",
162 | "mq:configuration",
163 | "network-firewall:firewall",
164 | "network-firewall:firewall-policy",
165 | "network-firewall:stateful-rulegroup",
166 | "network-firewall:stateless-rulegroup",
167 | "oam:link",
168 | "oam:sink",
169 | "omics:annotationStore",
170 | "omics:referenceStore",
171 | "omics:referenceStore/reference",
172 | "omics:run",
173 | "omics:runGroup",
174 | "omics:sequenceStore",
175 | "omics:sequenceStore/readSet",
176 | "omics:variantStore",
177 | "omics:workflow",
178 | "organizations:account",
179 | "organizations:ou",
180 | "organizations:policy",
181 | "organizations:root",
182 | "pipes:pipe",
183 | "ram:*",
184 | "rbin:rule",
185 | "rds:cluster-endpoint",
186 | "rds:cluster-pg",
187 | "rds:db-proxy",
188 | "rds:db-proxy-endpoint",
189 | "rds:es",
190 | "rds:og",
191 | "rds:pg",
192 | "rds:ri",
193 | "rds:secgrp",
194 | "rds:subgrp",
195 | "rds:target-group",
196 | "redshift-serverless:namespace",
197 | "redshift-serverless:workgroup",
198 | "redshift:*",
199 | "resource-groups:*",
200 | "route53:hostedzone",
201 | "route53resolver:*",
202 | "s3:bucket",
203 | "sagemaker:action",
204 | "sagemaker:app-image-config",
205 | "sagemaker:artifact",
206 | "sagemaker:context",
207 | "sagemaker:experiment",
208 | "sagemaker:flow-definition",
209 | "sagemaker:human-task-ui",
210 | "sagemaker:model-package",
211 | "sagemaker:model-package-group",
212 | "sagemaker:pipeline",
213 | "sagemaker:processing-job",
214 | "sagemaker:project",
215 | "sagemaker:training-job",
216 | "scheduler:schedule-group",
217 | "secretsmanager:*",
218 | "servicecatalog:applications",
219 | "servicecatalog:attribute-groups",
220 | "sms-voice:configuration-set",
221 | "sms-voice:opt-out-list",
222 | "sms-voice:phone-number",
223 | "sms-voice:pool",
224 | "sms-voice:sender-id",
225 | "sns:topic",
226 | "sqs:queue",
227 | "ssm-contacts:contact",
228 | "ssm:automation-execution",
229 | "ssm:document",
230 | "ssm:managed-instance",
231 | "ssm:opsitem",
232 | "ssm:patchbaseline",
233 | "ssm:session",
234 | "states:*",
235 | "storagegateway:*",
236 | "transfer:server",
237 | "transfer:user",
238 | "transfer:workflow",
239 | "wellarchitected:workload",
240 | "wickr:network",
241 | "wisdom:assistant",
242 | "wisdom:association",
243 | "wisdom:content",
244 | "wisdom:knowledge-base",
245 | "wisdom:session",
246 | "worklink:fleet",
247 | "workspaces:*"
248 | ]
249 | }
250 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/main.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | ou_path = replace(var.ou_path, "/", "-")
3 | }
4 |
5 | // https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_supported-resources-enforcement.html
6 | resource "aws_organizations_policy" "required_tags" {
7 | for_each = var.aws_ou_tags
8 |
9 | name = "LandingZone-RequiredTags-${local.ou_path}-${each.key}"
10 | type = "TAG_POLICY"
11 | tags = var.tags
12 |
13 | content = jsonencode(
14 | {
15 | tags = {
16 | (each.key) = merge(
17 | {
18 | tag_key = {
19 | "@@assign" = each.key,
20 | "@@operators_allowed_for_child_policies" = ["@@none"]
21 | }
22 | },
23 | try(each.value["values"] != null, false) ?
24 | {
25 | tag_value = { "@@assign" = each.value["values"] }
26 | } : {},
27 | try(each.value["enforced_for"] != null, false) ?
28 | {
29 | enforced_for = {
30 | "@@assign" = (each.value["enforced_for"][0] == "all" ?
31 | local.all_enforced_services : each.value["enforced_for"])
32 | }
33 | } : {},
34 | )
35 | }
36 | }
37 | )
38 | }
39 |
40 | resource "aws_organizations_policy_attachment" "required_tags" {
41 | for_each = var.aws_ou_tags
42 |
43 | policy_id = aws_organizations_policy.required_tags[each.key].id
44 | target_id = var.target_id
45 | }
46 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/variables.tf:
--------------------------------------------------------------------------------
1 | variable "aws_ou_tags" {
2 | type = map(object({
3 | values = optional(list(string))
4 | enforced_for = optional(list(string))
5 | }))
6 | description = "Map of AWS OU names and their tag policies"
7 | }
8 |
9 | variable "tags" {
10 | type = map(string)
11 | description = "Map of AWS resource tags"
12 | default = {}
13 | }
14 |
15 | variable "target_id" {
16 | type = string
17 | description = "The unique identifier (ID) organizational unit (OU) that you want to attach the policy to."
18 | }
19 |
20 | variable "ou_path" {
21 | type = string
22 | description = "Path of the organizational unit (OU)"
23 | }
24 |
--------------------------------------------------------------------------------
/modules/tag-policy-assignment/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 4.9.0"
6 | }
7 | }
8 | required_version = ">= 1.3"
9 | }
10 |
--------------------------------------------------------------------------------
/moved.tf:
--------------------------------------------------------------------------------
1 | moved {
2 | from = aws_config_configuration_recorder.default
3 | to = module.aws_config_recorder.aws_config_configuration_recorder.default
4 | }
5 |
6 | moved {
7 | from = aws_config_delivery_channel.default
8 | to = module.aws_config_recorder.aws_config_delivery_channel.default
9 | }
10 |
11 | moved {
12 | from = aws_config_configuration_recorder_status.default
13 | to = module.aws_config_recorder.aws_config_configuration_recorder_status.default
14 | }
15 |
--------------------------------------------------------------------------------
/organizations_policy.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | enabled_root_policies = {
3 | cloudtrail_log_stream = {
4 | enable = true // This is not configurable and will be applied all the time.
5 | policy = file("${path.module}/files/organizations/cloudtrail_log_stream.json")
6 | }
7 | deny_disabling_security_hub = {
8 | enable = var.aws_service_control_policies.aws_deny_disabling_security_hub
9 | policy = var.aws_service_control_policies.aws_deny_disabling_security_hub != false ? templatefile("${path.module}/files/organizations/deny_disabling_security_hub.json.tpl", {
10 | exceptions = local.aws_service_control_policies_principal_exceptions
11 | }) : null
12 | }
13 | deny_leaving_org = {
14 | enable = var.aws_service_control_policies.aws_deny_leaving_org
15 | policy = var.aws_service_control_policies.aws_deny_leaving_org != false ? templatefile("${path.module}/files/organizations/deny_leaving_org.json.tpl", {
16 | exceptions = local.aws_service_control_policies_principal_exceptions
17 | }) : null
18 | }
19 | // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ExamplePolicies_EC2.html#iam-example-instance-metadata-requireIMDSv2
20 | require_use_of_imdsv2 = {
21 | enable = var.aws_service_control_policies.aws_require_imdsv2
22 | policy = file("${path.module}/files/organizations/require_use_of_imdsv2.json")
23 | }
24 | }
25 |
26 | root_policies_to_merge = [for key, value in local.enabled_root_policies : jsondecode(
27 | value.enable == true ? value.policy : "{\"Statement\": []}"
28 | )]
29 |
30 | root_policies_merged = flatten([
31 | for policy in local.root_policies_to_merge : policy.Statement
32 | ])
33 | }
34 |
35 | resource "aws_organizations_policy" "lz_root_policies" {
36 | name = "LandingZone-RootPolicies"
37 | content = jsonencode({
38 | Version = "2012-10-17"
39 | Statement = local.root_policies_merged
40 | })
41 | description = "LandingZone enabled Root OU policies"
42 | tags = var.tags
43 | }
44 |
45 | resource "aws_organizations_policy_attachment" "lz_root_policies" {
46 | policy_id = aws_organizations_policy.lz_root_policies.id
47 | target_id = data.aws_organizations_organization.default.roots[0].id
48 | }
49 |
50 | // https://summitroute.com/blog/2020/03/25/aws_scp_best_practices/#deny-ability-to-leave-organization
51 | resource "aws_organizations_policy" "deny_root_user" {
52 | count = length(var.aws_service_control_policies.aws_deny_root_user_ous) > 0 ? 1 : 0
53 |
54 | name = "LandingZone-DenyRootUser"
55 | content = file("${path.module}/files/organizations/deny_root_user.json")
56 | tags = var.tags
57 | }
58 |
59 | resource "aws_organizations_policy_attachment" "deny_root_user" {
60 | for_each = {
61 | for ou in data.aws_organizations_organizational_units.default.children : ou.name => ou if contains(var.aws_service_control_policies.aws_deny_root_user_ous, ou.name)
62 | }
63 |
64 | policy_id = aws_organizations_policy.deny_root_user[0].id
65 | target_id = each.value.id
66 | }
67 |
68 | module "tag_policy_assignment" {
69 | for_each = {
70 | for ou in data.mcaf_aws_all_organizational_units.default.organizational_units : ou.path => ou if contains(keys(coalesce(var.aws_required_tags, {})), ou.path)
71 | }
72 |
73 | source = "./modules/tag-policy-assignment"
74 | aws_ou_tags = { for k, v in var.aws_required_tags[each.key] : v.name => v }
75 | target_id = each.value.id
76 | ou_path = each.key
77 | tags = var.tags
78 | }
79 |
--------------------------------------------------------------------------------
/organizations_policy_allowed_regions.tf:
--------------------------------------------------------------------------------
1 | locals {
2 | ################################################################################
3 | # 1) Core service lists
4 | ################################################################################
5 |
6 | # AWS services that need to be allowed in the global (us-east-1) region.
7 | # These services are typically used for account management, billing, and other global operations.
8 | global_service_actions = [
9 | "a4b:*",
10 | "access-analyzer:*",
11 | "account:*",
12 | "acm:*",
13 | "activate:*",
14 | "artifact:*",
15 | "aws-marketplace-management:*",
16 | "aws-marketplace:*",
17 | "aws-portal:*",
18 | "billing:*",
19 | "billingconductor:*",
20 | "budgets:*",
21 | "ce:*",
22 | "chatbot:*",
23 | "chime:*",
24 | "cloudfront:*",
25 | "cloudtrail:Describe*",
26 | "cloudtrail:Get*",
27 | "cloudtrail:List*",
28 | "cloudtrail:LookupEvents",
29 | "cloudwatch:Describe*",
30 | "cloudwatch:Get*",
31 | "cloudwatch:List*",
32 | "compute-optimizer:*",
33 | "config:*",
34 | "consoleapp:*",
35 | "consolidatedbilling:*",
36 | "cur:*",
37 | "datapipeline:GetAccountLimits",
38 | "devicefarm:*",
39 | "directconnect:*",
40 | "ec2:DescribeRegions",
41 | "ec2:DescribeTransitGateways",
42 | "ec2:DescribeVpnGateways",
43 | "ecr-public:*",
44 | "fms:*",
45 | "freetier:*",
46 | "globalaccelerator:*",
47 | "health:*",
48 | "iam:*",
49 | "importexport:*",
50 | "invoicing:*",
51 | "iq:*",
52 | "kms:*",
53 | "license-manager:ListReceivedLicenses",
54 | "lightsail:Get*",
55 | "logs:*",
56 | "mobileanalytics:*",
57 | "networkmanager:*",
58 | "notifications-contacts:*",
59 | "notifications:*",
60 | "organizations:*",
61 | "payments:*",
62 | "pricing:*",
63 | "quicksight:DescribeAccountSubscription",
64 | "quicksight:DescribeTemplate",
65 | "resource-explorer-2:*",
66 | "route53-recovery-cluster:*",
67 | "route53-recovery-control-config:*",
68 | "route53-recovery-readiness:*",
69 | "route53:*",
70 | "route53domains:*",
71 | "s3:CreateMultiRegionAccessPoint",
72 | "s3:DeleteMultiRegionAccessPoint",
73 | "s3:DescribeMultiRegionAccessPointOperation",
74 | "s3:GetAccountPublicAccessBlock",
75 | "s3:GetBucketLocation",
76 | "s3:GetBucketPolicy",
77 | "s3:GetBucketPolicyStatus",
78 | "s3:GetBucketPublicAccessBlock",
79 | "s3:GetMultiRegionAccessPoint",
80 | "s3:GetMultiRegionAccessPointPolicy",
81 | "s3:GetMultiRegionAccessPointPolicyStatus",
82 | "s3:GetStorageLensConfiguration",
83 | "s3:GetStorageLensDashboard",
84 | "s3:ListAllMyBuckets",
85 | "s3:ListMultiRegionAccessPoints",
86 | "s3:ListStorageLensConfigurations",
87 | "s3:PutAccountPublicAccessBlock",
88 | "s3:PutMultiRegionAccessPointPolicy",
89 | "savingsplans:*",
90 | "servicequotas:*",
91 | "shield:*",
92 | "sso:*",
93 | "sts:*",
94 | "support:*",
95 | "supportapp:*",
96 | "supportplans:*",
97 | "sustainability:*",
98 | "tag:GetResources",
99 | "tax:*",
100 | "trustedadvisor:*",
101 | "vendor-insights:ListEntitledSecurityProfiles",
102 | "waf-regional:*",
103 | "waf:*",
104 | "wafv2:*",
105 | "wellarchitected:*",
106 | ]
107 |
108 | # AWS services that are inherently multi-region, meaning they can operate across multiple regions.
109 | multi_region_service_actions = [
110 | "supportplans:*"
111 | ]
112 |
113 | ################################################################################
114 | # 2) Build the regions & exemption sets used in the SCP Statements
115 | ################################################################################
116 |
117 | # List of regions that have extra whitelisted actions
118 | regions_with_whitelist_exceptions = keys(var.regions.additional_allowed_service_actions_per_region)
119 |
120 | # Statement #1:
121 | allowed_plus_exception_regions = var.regions.allowed_regions != null ? distinct(concat(
122 | var.regions.allowed_regions,
123 | local.regions_with_whitelist_exceptions
124 | )) : []
125 |
126 | exempted_actions_global = distinct(concat(
127 | local.global_service_actions,
128 | local.multi_region_service_actions,
129 | ))
130 |
131 | # Statement #2:
132 | exempted_actions_per_region = {
133 | for region, service_action in var.regions.additional_allowed_service_actions_per_region :
134 | region => distinct(concat(
135 | local.multi_region_service_actions,
136 | service_action
137 | ))
138 | }
139 |
140 | # To keep the SCP as small as possible and avoid duplication, regions with exactly the same allowed service actions are grouped together.
141 | exempted_actions_per_region_grouped = [
142 | for actions in distinct(values(local.exempted_actions_per_region)) : {
143 | actions = actions
144 | regions = [
145 | for region, service_action in local.exempted_actions_per_region :
146 | region if service_action == actions
147 | ]
148 | }
149 | ]
150 |
151 | # Statement #3:
152 | allowed_plus_linked_plus_exception_plus_global_regions = var.regions.allowed_regions != null ? distinct(concat(
153 | var.regions.allowed_regions,
154 | var.regions.linked_regions,
155 | local.regions_with_whitelist_exceptions,
156 | ["us-east-1"] # (us-east-1 is the default region for the global services, so we need to allow it)
157 | )) : []
158 |
159 | ################################################################################
160 | # 3) Assemble the 3 SCP statements
161 | ################################################################################
162 |
163 | allowed_regions_policy_statements = concat(
164 | # Statement (1) explanation:
165 | # Allow all services in your `allowed_regions` & regions listed in the `additional_allowed_service_actions_per_region`,
166 | # For all other regions, every service action is denied except for global & multi-region service actions.
167 | [
168 | {
169 | Sid = "DenyAllRegionsOutsideAllowedList"
170 | Effect = "Deny"
171 | NotAction = local.exempted_actions_global
172 | Resource = "*"
173 | Condition = {
174 | StringNotEquals = {
175 | "aws:RequestedRegion" = local.allowed_plus_exception_regions
176 | }
177 | ArnNotLike = {
178 | "aws:PrincipalARN" = local.aws_service_control_policies_principal_exceptions
179 | }
180 | }
181 | }
182 | ],
183 |
184 | # Statement (2) explanation:
185 | # In each `additional_allowed_service_actions_per_region` region,
186 | # only allow the actions listed in the `additional_allowed_service_actions_per_region` map & the `multi_region_service_actions`.
187 | [
188 | for group in local.exempted_actions_per_region_grouped : {
189 | Sid = "DenyOutsideAllowedList${replace(replace(join("_", group.regions), "-", ""), "_", "")}"
190 | Effect = "Deny"
191 | NotAction = group.actions
192 | Resource = "*"
193 | Condition = {
194 | StringEquals = {
195 | "aws:RequestedRegion" = group.regions
196 | }
197 | ArnNotLike = {
198 | "aws:PrincipalARN" = local.aws_service_control_policies_principal_exceptions
199 | }
200 | }
201 | }
202 | ],
203 |
204 | # Statement (3) explanation:
205 | # Deny all service actions except for the `multi_region_service_actions` in any region not in your allowed + linked + exception + [us-east-1] set.
206 | # This statement is for leak prevention: It explicitly denies any per-region-only services within your core allowed regions so they can’t slip in where you don’t want them;
207 | # as some services like acm & logs are both global and regional specific services.
208 | [
209 | {
210 | Sid = "DenyAllOtherRegions"
211 | Effect = "Deny"
212 | NotAction = local.multi_region_service_actions
213 | Resource = "*"
214 | Condition = {
215 | StringNotEquals = {
216 | "aws:RequestedRegion" = local.allowed_plus_linked_plus_exception_plus_global_regions
217 | }
218 | ArnNotLike = {
219 | "aws:PrincipalARN" = local.aws_service_control_policies_principal_exceptions
220 | }
221 | }
222 | }
223 | ]
224 | )
225 |
226 | allowed_regions_policy = jsonencode({
227 | Version = "2012-10-17"
228 | Statement = local.allowed_regions_policy_statements
229 | })
230 | }
231 |
232 | resource "aws_organizations_policy" "allowed_regions" {
233 | count = var.regions.allowed_regions != null ? 1 : 0
234 |
235 | name = "LandingZone-AllowedRegions"
236 | content = local.allowed_regions_policy
237 | description = "LandingZone allowed regions"
238 | tags = var.tags
239 | }
240 |
241 | resource "aws_organizations_policy_attachment" "allowed_regions" {
242 | count = var.regions.allowed_regions != null ? 1 : 0
243 |
244 | policy_id = aws_organizations_policy.allowed_regions[0].id
245 | target_id = data.aws_organizations_organization.default.roots[0].id
246 | }
247 |
--------------------------------------------------------------------------------
/outputs.tf:
--------------------------------------------------------------------------------
1 | output "aws_config_s3_bucket_arn" {
2 | description = "ARN of the AWS Config S3 bucket in the logging account"
3 | value = module.aws_config_s3.arn
4 | }
5 |
6 | output "aws_config_iam_service_linked_role_arn" {
7 | description = "IAM Service Linked Role ARN for AWS Config in the management account"
8 | value = aws_iam_service_linked_role.config.arn
9 | }
10 |
11 | output "kms_key_arn" {
12 | description = "ARN of KMS key for the management account"
13 | value = module.kms_key.arn
14 | }
15 |
16 | output "kms_key_id" {
17 | description = "ID of KMS key for the management account"
18 | value = module.kms_key.id
19 | }
20 |
21 | output "kms_key_audit_arn" {
22 | description = "ARN of KMS key for the audit account"
23 | value = module.kms_key_audit.arn
24 | }
25 |
26 | output "kms_key_audit_id" {
27 | description = "ID of KMS key for the audit account"
28 | value = module.kms_key_audit.id
29 | }
30 |
31 | output "kms_key_logging_arn" {
32 | description = "ARN of KMS key for the logging account"
33 | value = module.kms_key_logging.arn
34 | }
35 |
36 | output "kms_key_logging_id" {
37 | description = "ID of KMS key for the logging account"
38 | value = module.kms_key_logging.id
39 | }
40 |
41 | output "monitor_iam_activity_sns_topic_arn" {
42 | description = "ARN of the SNS Topic in the audit account for IAM activity monitoring notifications"
43 | value = var.monitor_iam_activity ? aws_sns_topic.iam_activity[0].arn : ""
44 | }
45 |
--------------------------------------------------------------------------------
/security_hub.tf:
--------------------------------------------------------------------------------
1 | // AWS Security Hub - Management account configuration and enrollment
2 | resource "aws_securityhub_organization_admin_account" "default" {
3 | admin_account_id = data.aws_caller_identity.audit.account_id
4 |
5 | depends_on = [aws_securityhub_account.default]
6 | }
7 |
8 | resource "aws_securityhub_account" "management" {
9 | control_finding_generator = var.aws_security_hub.control_finding_generator
10 |
11 | depends_on = [aws_securityhub_organization_configuration.default]
12 | }
13 |
14 | resource "aws_securityhub_member" "management" {
15 | provider = aws.audit
16 |
17 | account_id = data.aws_caller_identity.management.account_id
18 |
19 | depends_on = [aws_securityhub_account.management]
20 |
21 | lifecycle {
22 | ignore_changes = [invite]
23 | }
24 | }
25 |
26 | // AWS Security Hub - Audit account configuration and enrollment
27 | resource "aws_securityhub_account" "default" {
28 | provider = aws.audit
29 |
30 | control_finding_generator = var.aws_security_hub.control_finding_generator
31 | }
32 |
33 | resource "aws_securityhub_finding_aggregator" "default" {
34 | provider = aws.audit
35 |
36 | linking_mode = var.aws_security_hub.aggregator_linking_mode
37 | specified_regions = var.aws_security_hub.aggregator_linking_mode == "SPECIFIED_REGIONS" ? var.regions.linked_regions : null
38 |
39 | depends_on = [aws_securityhub_account.default]
40 | }
41 |
42 | resource "aws_securityhub_organization_configuration" "default" {
43 | provider = aws.audit
44 |
45 | auto_enable = false
46 | auto_enable_standards = "NONE"
47 |
48 | organization_configuration {
49 | configuration_type = "CENTRAL"
50 | }
51 |
52 | depends_on = [aws_securityhub_organization_admin_account.default, aws_securityhub_finding_aggregator.default]
53 | }
54 |
55 | resource "aws_securityhub_configuration_policy" "default" {
56 | provider = aws.audit
57 |
58 | name = "mcaf-lz"
59 | description = "MCAF Landing Zone default configuration policy"
60 |
61 | configuration_policy {
62 | service_enabled = true
63 | enabled_standard_arns = local.security_hub_standards_arns
64 |
65 | security_controls_configuration {
66 | disabled_control_identifiers = var.aws_security_hub.disabled_control_identifiers
67 | enabled_control_identifiers = var.aws_security_hub.enabled_control_identifiers
68 | }
69 | }
70 |
71 | depends_on = [aws_securityhub_organization_configuration.default]
72 | }
73 |
74 | resource "aws_securityhub_configuration_policy_association" "root" {
75 | provider = aws.audit
76 |
77 | target_id = data.aws_organizations_organization.default.roots[0].id
78 | policy_id = aws_securityhub_configuration_policy.default.id
79 | }
80 |
81 | resource "aws_cloudwatch_event_rule" "security_hub_findings" {
82 | provider = aws.audit
83 |
84 | name = "LandingZone-SecurityHubFindings"
85 | description = "Rule for getting SecurityHub findings"
86 | event_pattern = file("${path.module}/files/event_bridge/security_hub_findings.json.tpl")
87 | tags = var.tags
88 | }
89 |
90 | resource "aws_cloudwatch_event_target" "security_hub_findings" {
91 | provider = aws.audit
92 |
93 | arn = aws_sns_topic.security_hub_findings.arn
94 | rule = aws_cloudwatch_event_rule.security_hub_findings.name
95 | target_id = "SendToSNS"
96 | }
97 |
98 | resource "aws_sns_topic" "security_hub_findings" {
99 | provider = aws.audit
100 |
101 | name = "LandingZone-SecurityHubFindings"
102 | http_success_feedback_role_arn = aws_iam_role.sns_feedback.arn
103 | http_failure_feedback_role_arn = aws_iam_role.sns_feedback.arn
104 | kms_master_key_id = module.kms_key_audit.id
105 | tags = var.tags
106 | }
107 |
108 | resource "aws_sns_topic_policy" "security_hub_findings" {
109 | provider = aws.audit
110 |
111 | arn = aws_sns_topic.security_hub_findings.arn
112 | policy = templatefile("${path.module}/files/sns/security_hub_topic_policy.json.tpl", {
113 | account_id = data.aws_caller_identity.audit.account_id
114 | services_allowed_publish = jsonencode("events.amazonaws.com")
115 | sns_topic = aws_sns_topic.security_hub_findings.arn
116 | })
117 | }
118 |
119 | resource "aws_sns_topic_subscription" "security_hub_findings" {
120 | for_each = var.aws_security_hub_sns_subscription
121 | provider = aws.audit
122 |
123 | endpoint = each.value.endpoint
124 | endpoint_auto_confirms = length(regexall("http", each.value.protocol)) > 0
125 | protocol = each.value.protocol
126 | topic_arn = aws_sns_topic.security_hub_findings.arn
127 | }
128 |
129 | // AWS Security Hub - Logging account enrollment
130 | resource "aws_securityhub_member" "logging" {
131 | provider = aws.audit
132 |
133 | account_id = data.aws_caller_identity.logging.account_id
134 |
135 | lifecycle {
136 | ignore_changes = [invite]
137 | }
138 |
139 | depends_on = [aws_securityhub_organization_configuration.default]
140 | }
141 |
--------------------------------------------------------------------------------
/ses_accounts_mail_alias.tf:
--------------------------------------------------------------------------------
1 | module "ses-root-accounts-mail-alias" {
2 | # checkov:skip=CKV_AWS_273: IAM user is the only option for SMTP auth
3 |
4 | count = var.ses_root_accounts_mail_forward != null ? 1 : 0
5 | providers = { aws = aws, aws.route53 = aws }
6 |
7 | source = "schubergphilis/mcaf-ses/aws"
8 | version = "~> 0.1.4"
9 |
10 | dmarc = var.ses_root_accounts_mail_forward.dmarc
11 | domain = var.ses_root_accounts_mail_forward.domain
12 | kms_key_id = module.kms_key.id
13 | tags = var.tags
14 | }
15 |
16 | module "ses-root-accounts-mail-forward" {
17 | # checkov:skip=CKV_AWS_19: False positive: https://github.com/bridgecrewio/checkov/issues/3847. The S3 bucket created by this module is encrypted with KMS.
18 | # checkov:skip=CKV_AWS_145: False positive: https://github.com/bridgecrewio/checkov/issues/3847. The S3 bucket created by this module is encrypted with KMS.
19 | # checkov:skip=CKV_AWS_272: This module does not support lambda code signing at the moment
20 |
21 | count = var.ses_root_accounts_mail_forward != null ? 1 : 0
22 |
23 | source = "schubergphilis/mcaf-ses-forwarder/aws"
24 | version = "~> 0.3.0"
25 |
26 | bucket_name = "ses-forwarder-${replace(var.ses_root_accounts_mail_forward.domain, ".", "-")}"
27 | from_email = var.ses_root_accounts_mail_forward.from_email
28 | kms_key_arn = module.kms_key.arn
29 | recipient_mapping = var.ses_root_accounts_mail_forward.recipient_mapping
30 | tags = var.tags
31 | }
32 |
--------------------------------------------------------------------------------
/sso.tf:
--------------------------------------------------------------------------------
1 | module "aws_sso_permission_sets" {
2 | for_each = var.aws_sso_permission_sets
3 |
4 | source = "./modules/permission-set"
5 | name = each.key
6 | session_duration = each.value.session_duration
7 | assignments = each.value.assignments
8 | inline_policy = each.value.inline_policy
9 | managed_policy_arns = each.value.managed_policy_arns
10 | }
11 |
--------------------------------------------------------------------------------
/terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 5.54.0"
6 | configuration_aliases = [aws.audit, aws.logging]
7 | }
8 | datadog = {
9 | source = "datadog/datadog"
10 | version = ">= 3.39"
11 | }
12 | mcaf = {
13 | source = "schubergphilis/mcaf"
14 | version = ">= 0.4.2"
15 | }
16 | }
17 | required_version = ">= 1.9.0"
18 | }
19 |
--------------------------------------------------------------------------------
/tests/datadog.tftest.hcl:
--------------------------------------------------------------------------------
1 | variables {
2 |
3 | control_tower_account_ids = {
4 | audit = "012345678902"
5 | logging = "012345678903"
6 | }
7 |
8 | regions = {
9 | allowed_regions = ["eu-central-1"]
10 | home_region = "eu-central-1"
11 | }
12 | }
13 |
14 | run "setup" {
15 | module {
16 | source = "./tests/setup"
17 | }
18 | }
19 |
20 | mock_provider "datadog" {}
21 |
22 | mock_provider "aws" {
23 | mock_data "aws_region" {
24 | defaults = {
25 | name = "eu-central-1"
26 | }
27 | }
28 |
29 | mock_data "aws_caller_identity" {
30 | defaults = {
31 | account_id = "012345678901"
32 | }
33 | }
34 |
35 | mock_data "aws_organizations_organization" {
36 | defaults = {
37 | accounts = [
38 | {
39 | "arn" : "arn:aws:organizations::012345678901:account/o-ab1234cdef/012345678901",
40 | "email" : "core-master@example.com",
41 | "id" : "012345678901",
42 | "name" : "core-master",
43 | "status" : "ACTIVE"
44 | },
45 | {
46 | "arn" : "arn:aws:organizations::012345678901:account/o-ab1234cdef/012345678902",
47 | "email" : "core-audit@example.com",
48 | "id" : "012345678902",
49 | "name" : "core-audit",
50 | "status" : "ACTIVE"
51 | },
52 | {
53 | "arn" : "arn:aws:organizations::012345678901:account/o-ab1234cdef/012345678903",
54 | "email" : "core-logging@example.com",
55 | "id" : "012345678903",
56 | "name" : "core-logging",
57 | "status" : "ACTIVE"
58 | }
59 | ]
60 |
61 | roots = [
62 | {
63 | "arn" : "arn:aws:organizations::012345678901:root/o-ab1234cdef/r-12ac",
64 | "id" : "r-12ac",
65 | "name" : "Root",
66 | "policy_types" : [
67 | {
68 | "status" : "ENABLED",
69 | "type" : "SERVICE_CONTROL_POLICY"
70 | },
71 | {
72 | "status" : "ENABLED",
73 | "type" : "TAG_POLICY"
74 | }
75 | ]
76 | }
77 | ]
78 | }
79 | }
80 |
81 | mock_data "aws_iam_policy_document" {
82 | defaults = {
83 | json = "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Sid\": \"Base Permissions\",\"Effect\": \"Allow\",\"Action\": \"kms:*\",\"Resource\": \"*\",\"Principal\": {\"AWS\": \"arn:aws:iam::012345678901:root\"}}]}"
84 | }
85 | }
86 | }
87 |
88 | mock_provider "aws" {
89 | alias = "audit"
90 |
91 | mock_data "aws_region" {
92 | defaults = {
93 | name = "eu-central-1"
94 | }
95 | }
96 |
97 | mock_data "aws_caller_identity" {
98 | defaults = {
99 | account_id = "012345678902"
100 | }
101 | }
102 |
103 | mock_data "aws_iam_policy_document" {
104 | defaults = {
105 | json = "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Sid\": \"Base Permissions\",\"Effect\": \"Allow\",\"Action\": \"kms:*\",\"Resource\": \"*\",\"Principal\": {\"AWS\": \"arn:aws:iam::012345678902:root\"}}]}"
106 | }
107 | }
108 |
109 | mock_data "aws_sns_topic" {
110 | defaults = {
111 | arn = "arn:aws:sns:eu-central-1:012345678902:aws-controltower-AllConfigNotifications"
112 | }
113 | }
114 | }
115 |
116 | mock_provider "aws" {
117 | alias = "logging"
118 |
119 | mock_data "aws_region" {
120 | defaults = {
121 | name = "eu-central-1"
122 | }
123 | }
124 |
125 | mock_data "aws_caller_identity" {
126 | defaults = {
127 | account_id = "012345678903"
128 | }
129 | }
130 |
131 | mock_data "aws_iam_policy_document" {
132 | defaults = {
133 | json = "{\"Version\": \"2012-10-17\",\"Statement\": [{\"Sid\": \"Base Permissions\",\"Effect\": \"Allow\",\"Action\": \"kms:*\",\"Resource\": \"*\",\"Principal\": {\"AWS\": \"arn:aws:iam::012345678903:root\"}}]}"
134 | }
135 | }
136 | }
137 |
138 | mock_provider "mcaf" {
139 | mock_data "mcaf_aws_all_organizational_units" {
140 | defaults = {
141 | organizational_units = [
142 | {
143 | "id" : "ou-1234",
144 | "name" : "OU1"
145 | },
146 | {
147 | "id" : "ou-5678",
148 | "name" : "OU2"
149 | }
150 | ]
151 | }
152 | }
153 | }
154 |
155 | run "datadog_integration_disabled" {
156 | module {
157 | source = "./"
158 | }
159 |
160 | command = plan
161 |
162 | assert {
163 | condition = length(module.datadog_master) == 0
164 | error_message = "The Datadog integration should be disabled"
165 | }
166 |
167 | assert {
168 | condition = length(module.datadog_audit) == 0
169 | error_message = "The Datadog integration should be disabled"
170 | }
171 |
172 | assert {
173 | condition = length(module.datadog_logging) == 0
174 | error_message = "The Datadog integration should be disabled"
175 | }
176 | }
177 |
178 | run "datadog_integration_disabled_by_boolean" {
179 | module {
180 | source = "./"
181 | }
182 |
183 | variables {
184 | datadog = {
185 | enable_integration = false
186 | site_url = "datadoghq.eu"
187 | }
188 | }
189 |
190 | command = plan
191 |
192 | assert {
193 | condition = length(module.datadog_master) == 0
194 | error_message = "The Datadog integration should be disabled"
195 | }
196 |
197 | assert {
198 | condition = length(module.datadog_audit) == 0
199 | error_message = "The Datadog integration should be disabled"
200 | }
201 |
202 | assert {
203 | condition = length(module.datadog_logging) == 0
204 | error_message = "The Datadog integration should be disabled"
205 | }
206 | }
207 |
208 | run "datadog_integration_enabled_api_key" {
209 | module {
210 | source = "./"
211 | }
212 |
213 | variables {
214 | datadog = {
215 | api_key = "12345678901234567890123456789012"
216 | enable_integration = true
217 | install_log_forwarder = true
218 | log_forwarder_version = "latest"
219 | site_url = "datadoghq.eu"
220 | }
221 | }
222 |
223 | command = plan
224 |
225 | assert {
226 | condition = length(module.datadog_master) == 1
227 | error_message = "The Datadog integration should be enabled"
228 | }
229 |
230 | assert {
231 | condition = length(module.datadog_audit) == 1
232 | error_message = "The Datadog integration should be enabled"
233 | }
234 |
235 | assert {
236 | condition = length(module.datadog_logging) == 1
237 | error_message = "The Datadog integration should be enabled"
238 | }
239 | }
240 |
241 | run "datadog_integration_enabled_create_api_key" {
242 | module {
243 | source = "./"
244 | }
245 |
246 | variables {
247 | datadog = {
248 | api_key = null
249 | create_api_key = true
250 | enable_integration = true
251 | install_log_forwarder = true
252 | log_forwarder_version = "latest"
253 | site_url = "datadoghq.eu"
254 | }
255 | }
256 |
257 | command = plan
258 |
259 | assert {
260 | condition = length(module.datadog_master) == 1
261 | error_message = "The Datadog integration should be enabled"
262 | }
263 |
264 | assert {
265 | condition = length(module.datadog_audit) == 1
266 | error_message = "The Datadog integration should be enabled"
267 | }
268 |
269 | assert {
270 | condition = length(module.datadog_logging) == 1
271 | error_message = "The Datadog integration should be enabled"
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/tests/setup/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 5.54.0"
6 | configuration_aliases = [aws.audit, aws.logging]
7 | }
8 | datadog = {
9 | source = "datadog/datadog"
10 | version = ">= 3.39"
11 | }
12 | mcaf = {
13 | source = "schubergphilis/mcaf"
14 | version = ">= 0.4.2"
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/variables.tf:
--------------------------------------------------------------------------------
1 | variable "additional_auditing_trail" {
2 | type = object({
3 | name = string
4 | bucket = string
5 | kms_key_id = string
6 |
7 | event_selector = optional(object({
8 | data_resource = optional(object({
9 | type = string
10 | values = list(string)
11 | }))
12 | exclude_management_event_sources = optional(set(string), null)
13 | include_management_events = optional(bool, true)
14 | read_write_type = optional(string, "All")
15 | }))
16 | })
17 | default = null
18 | description = "CloudTrail configuration for additional auditing trail"
19 | }
20 |
21 | variable "aws_account_password_policy" {
22 | type = object({
23 | allow_users_to_change = bool
24 | max_age = number
25 | minimum_length = number
26 | require_lowercase_characters = bool
27 | require_numbers = bool
28 | require_symbols = bool
29 | require_uppercase_characters = bool
30 | reuse_prevention_history = number
31 | })
32 | default = {
33 | allow_users_to_change = true
34 | max_age = 90
35 | minimum_length = 14
36 | require_lowercase_characters = true
37 | require_numbers = true
38 | require_symbols = true
39 | require_uppercase_characters = true
40 | reuse_prevention_history = 24
41 | }
42 | description = "AWS account password policy parameters for the audit, logging and master account"
43 | }
44 |
45 | variable "aws_auditmanager" {
46 | type = object({
47 | enabled = bool
48 | reports_bucket_prefix = string
49 | })
50 | default = {
51 | enabled = true
52 | reports_bucket_prefix = "audit-manager-reports"
53 | }
54 | description = "AWS Audit Manager config settings"
55 | }
56 |
57 | variable "aws_config" {
58 | type = object({
59 | aggregator_account_ids = optional(list(string), [])
60 | delivery_channel_s3_bucket_name = optional(string, null)
61 | delivery_channel_s3_key_prefix = optional(string, null)
62 | delivery_frequency = optional(string, "TwentyFour_Hours")
63 | rule_identifiers = optional(list(string), [])
64 | })
65 | default = {
66 | aggregator_account_ids = []
67 | delivery_channel_s3_bucket_name = null
68 | delivery_channel_s3_key_prefix = null
69 | delivery_frequency = "TwentyFour_Hours"
70 | rule_identifiers = []
71 | }
72 | description = "AWS Config settings"
73 |
74 | validation {
75 | condition = contains(["One_Hour", "Three_Hours", "Six_Hours", "Twelve_Hours", "TwentyFour_Hours"], var.aws_config.delivery_frequency)
76 | error_message = "The delivery frequency must be set to \"One_Hour\", \"Three_Hours\", \"Six_Hours\", \"Twelve_Hours\", or \"TwentyFour_Hours\"."
77 | }
78 | }
79 |
80 | variable "aws_config_sns_subscription" {
81 | type = map(object({
82 | endpoint = string
83 | protocol = string
84 | }))
85 | default = {}
86 | description = "Subscription options for the aws-controltower-AggregateSecurityNotifications (AWS Config) SNS topic"
87 | }
88 |
89 | variable "aws_ebs_encryption_by_default" {
90 | type = bool
91 | default = true
92 | description = "Set to true to enable AWS Elastic Block Store encryption by default"
93 | }
94 |
95 | variable "aws_guardduty" {
96 | type = object({
97 | enabled = optional(bool, true)
98 | finding_publishing_frequency = optional(string, "FIFTEEN_MINUTES")
99 | ebs_malware_protection_status = optional(bool, true)
100 | eks_audit_logs_status = optional(bool, true)
101 | lambda_network_logs_status = optional(bool, true)
102 | rds_login_events_status = optional(bool, true)
103 | s3_data_events_status = optional(bool, true)
104 | runtime_monitoring_status = optional(object({
105 | enabled = optional(bool, true)
106 | eks_addon_management_status = optional(bool, true)
107 | ecs_fargate_agent_management_status = optional(bool, true)
108 | ec2_agent_management_status = optional(bool, true)
109 | }), {})
110 | })
111 | default = {}
112 | description = "AWS GuardDuty settings"
113 | }
114 |
115 | variable "aws_inspector" {
116 | type = object({
117 | enabled = optional(bool, false)
118 | enable_scan_ec2 = optional(bool, true)
119 | enable_scan_ecr = optional(bool, true)
120 | enable_scan_lambda = optional(bool, true)
121 | enable_scan_lambda_code = optional(bool, true)
122 | excluded_member_account_ids = optional(list(string), [])
123 | resource_create_timeout = optional(string, "15m")
124 | })
125 | default = {}
126 | description = "AWS Inspector settings, at least one of the scan options must be enabled"
127 | }
128 |
129 | variable "aws_required_tags" {
130 | type = map(list(object({
131 | name = string
132 | values = optional(list(string))
133 | enforced_for = optional(list(string))
134 | })))
135 | default = null
136 | description = "AWS Required tags settings"
137 |
138 | validation {
139 | condition = var.aws_required_tags != null ? alltrue([for taglist in var.aws_required_tags : length(taglist) <= 10]) : true
140 | error_message = "A maximum of 10 tag keys can be supplied to stay within the maximum policy length."
141 | }
142 | }
143 |
144 | variable "aws_security_hub" {
145 | type = object({
146 | aggregator_linking_mode = optional(string, "SPECIFIED_REGIONS")
147 | auto_enable_controls = optional(bool, true)
148 | control_finding_generator = optional(string, "SECURITY_CONTROL")
149 | create_cis_metric_filters = optional(bool, true)
150 | disabled_control_identifiers = optional(list(string), null)
151 | enabled_control_identifiers = optional(list(string), null)
152 | product_arns = optional(list(string), [])
153 | standards_arns = optional(list(string), null)
154 | })
155 | default = {}
156 | description = "AWS Security Hub settings"
157 |
158 | validation {
159 | condition = contains(["SECURITY_CONTROL", "STANDARD_CONTROL"], var.aws_security_hub.control_finding_generator)
160 | error_message = "The \"control_finding_generator\" variable must be set to either \"SECURITY_CONTROL\" or \"STANDARD_CONTROL\"."
161 | }
162 |
163 | validation {
164 | condition = contains(["SPECIFIED_REGIONS", "ALL_REGIONS"], var.aws_security_hub.aggregator_linking_mode)
165 | error_message = "The \"aggregator_linking_mode\" variable must be set to either \"SPECIFIED_REGIONS\" or \"ALL_REGIONS\"."
166 | }
167 |
168 | validation {
169 | condition = try(length(var.aws_security_hub.enabled_control_identifiers), 0) == 0 || try(length(var.aws_security_hub.disabled_control_identifiers), 0) == 0
170 | error_message = "Only one of \"enabled_control_identifiers\" or \"disabled_control_identifiers\" variable can be set."
171 | }
172 | }
173 |
174 | variable "aws_security_hub_sns_subscription" {
175 | type = map(object({
176 | endpoint = string
177 | protocol = string
178 | }))
179 | default = {}
180 | description = "Subscription options for the LandingZone-SecurityHubFindings SNS topic"
181 | }
182 |
183 | variable "aws_service_control_policies" {
184 | type = object({
185 | aws_deny_disabling_security_hub = optional(bool, true)
186 | aws_deny_leaving_org = optional(bool, true)
187 | aws_deny_root_user_ous = optional(list(string), [])
188 | aws_require_imdsv2 = optional(bool, true)
189 | principal_exceptions = optional(list(string), [])
190 | })
191 | default = {}
192 | description = "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"
193 | }
194 |
195 | variable "aws_sso_permission_sets" {
196 | type = map(object({
197 | assignments = list(object({
198 | account_id = string
199 | account_name = string
200 | sso_groups = list(string)
201 | }))
202 | inline_policy = optional(string, null)
203 | managed_policy_arns = optional(list(string), [])
204 | session_duration = optional(string, "PT4H")
205 | }))
206 | default = {}
207 | description = "Map of AWS IAM Identity Center permission sets with AWS accounts and group names that should be granted access to each account"
208 | }
209 |
210 | variable "control_tower_account_ids" {
211 | type = object({
212 | audit = string
213 | logging = string
214 | })
215 | description = "Control Tower core account IDs"
216 | }
217 |
218 | variable "datadog" {
219 | type = object({
220 | api_key = optional(string, null)
221 | api_key_name_prefix = optional(string, "aws-landing-zone-")
222 | create_api_key = optional(bool, false)
223 | cspm_resource_collection_enabled = optional(bool, false)
224 | enable_integration = bool
225 | extended_resource_collection_enabled = optional(bool, false)
226 | install_log_forwarder = optional(bool, false)
227 | log_collection_services = optional(list(string), [])
228 | log_forwarder_version = optional(string)
229 | metric_tag_filters = optional(map(string), {})
230 | namespace_rules = optional(list(string), [])
231 | site_url = string
232 | })
233 | default = null
234 | description = "Datadog integration options for the core accounts"
235 |
236 | validation {
237 | condition = (
238 | # Either Datadog integration config is not supplied = disabled
239 | var.datadog == null ||
240 | # Or it's directly disabled
241 | try(var.datadog.enable_integration, false) == false ||
242 | # Or it's enabled but the log forwarder is disabled (API key not needed)
243 | (try(var.datadog.enable_integration, false) && try(var.datadog.install_log_forwarder, false) == false) ||
244 | # Or the API key is supplied
245 | try(length(var.datadog.api_key), 0) > 0 ||
246 | # Or the API key will be created
247 | try(var.datadog.create_api_key, false)
248 | )
249 |
250 | error_message = "If Datadog integration is enabled, either an API key must be provided or the 'create_api_key' option must be set to true."
251 | }
252 | }
253 |
254 | variable "datadog_excluded_regions" {
255 | type = list(string)
256 | description = "List of regions where metrics collection will be disabled."
257 | default = []
258 | }
259 |
260 | variable "kms_key_policy" {
261 | type = list(string)
262 | default = []
263 | description = "A list of valid KMS key policy JSON documents"
264 | }
265 |
266 | variable "kms_key_policy_audit" {
267 | type = list(string)
268 | default = []
269 | description = "A list of valid KMS key policy JSON document for use with audit KMS key"
270 | }
271 |
272 | variable "kms_key_policy_logging" {
273 | type = list(string)
274 | default = []
275 | description = "A list of valid KMS key policy JSON document for use with logging KMS key"
276 | }
277 |
278 | variable "monitor_iam_activity" {
279 | type = bool
280 | default = true
281 | description = "Whether IAM activity should be monitored"
282 | }
283 |
284 | variable "monitor_iam_activity_sns_subscription" {
285 | type = map(object({
286 | endpoint = string
287 | protocol = string
288 | }))
289 | default = {}
290 | description = "Subscription options for the LandingZone-IAMActivity SNS topic"
291 | }
292 |
293 | variable "regions" {
294 | type = object({
295 | additional_allowed_service_actions_per_region = optional(map(list(string)), {})
296 | allowed_regions = list(string)
297 | home_region = string
298 | linked_regions = optional(list(string), ["us-east-1"])
299 | })
300 | description = "Region configuration, plus global and per-region service SCP exceptions. See the README for more information on the configuration options."
301 |
302 | validation {
303 | condition = length(var.regions.linked_regions) > 0
304 | 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."
305 | }
306 | }
307 |
308 | variable "path" {
309 | type = string
310 | default = "/"
311 | description = "Optional path for all IAM users, user groups, roles, and customer managed policies created by this module"
312 | }
313 |
314 | variable "ses_root_accounts_mail_forward" {
315 | type = object({
316 | domain = string
317 | from_email = string
318 | recipient_mapping = map(any)
319 |
320 | dmarc = object({
321 | policy = optional(string)
322 | rua = optional(string)
323 | ruf = optional(string)
324 | })
325 | })
326 | default = null
327 | description = "SES config to receive and forward root account emails"
328 | }
329 |
330 | variable "tags" {
331 | type = map(string)
332 | default = {}
333 | description = "Map of tags"
334 | }
335 |
--------------------------------------------------------------------------------