# Hub vaults =================================================================== # Central destination vaults for cross-account recovery point copies. # Created once, in the designated hub/backup account. Spoke accounts never # create resources here directly — they only reference these vault ARNs as # copy_action destinations in their own local backup plans. # # force_destroy and prevent_destroy are both fixed, non-configurable — this # vault is the org-wide compliance record and should never be destroyable # as a side effect of routine changes. Decommissioning is a deliberate, # reviewed action (temporarily remove the lifecycle block in its own PR), # not something a stale plan or a fat-fingered apply should be able to do. resource "aws_backup_vault" "this" { for_each = var.vaults name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}" kms_key_arn = each.value.kms_key_arn force_destroy = false tags = merge(var.tags, local.module_tags, { Name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}" resource-type = "backup-vault" }) lifecycle { prevent_destroy = true } } # Vault access policy --------------------------------------------------------- # Grants CopyIntoBackupVault to each vault's own trusted spoke role list. # This is the only mechanism by which a cross-account copy_action from a # spoke succeeds — without this statement, the destination vault refuses # the incoming copy regardless of what the spoke's own IAM role allows. resource "aws_backup_vault_policy" "this" { for_each = var.vaults backup_vault_name = aws_backup_vault.this[each.key].name policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AllowTrustedSpokeCopyIntoVault" Effect = "Allow" Principal = { AWS = each.value.trusted_spoke_role_arns } Action = ["backup:CopyIntoBackupVault"] Resource = aws_backup_vault.this[each.key].arn } ] }) } # Vault lock (WORM compliance mode) -------------------------------------------- # Optional per-vault. Once locked in compliance mode, retention cannot be # shortened or the vault deleted by anyone, including account root, until # the lock's changeable_for_days grace period has elapsed. resource "aws_backup_vault_lock_configuration" "this" { for_each = { for k, v in var.vaults : k => v if v.lock_enabled } backup_vault_name = aws_backup_vault.this[each.key].name changeable_for_days = each.value.lock_changeable_for_days min_retention_days = each.value.lock_min_retention_days max_retention_days = each.value.lock_max_retention_days } # Notifications ----------------------------------------------------------------- # Opt-in per vault. If notifications.create_sns_topic is true and no # sns_topic_arn is provided, the module provisions a topic. Otherwise the # supplied sns_topic_arn is used as-is (e.g. an existing ops/alerting topic). resource "aws_sns_topic" "backup_notifications" { for_each = { for k, v in var.vaults : k => v if v.notifications != null && v.notifications.create_sns_topic && v.notifications.sns_topic_arn == null } name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}-notifications" kms_master_key_id = each.value.notifications.kms_key_arn tags = merge(var.tags, local.module_tags, { Name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}-notifications" resource-type = "backup-notifications-topic" }) } resource "aws_backup_vault_notifications" "this" { for_each = { for k, v in var.vaults : k => v if v.notifications != null } backup_vault_name = aws_backup_vault.this[each.key].name sns_topic_arn = coalesce( each.value.notifications.sns_topic_arn, try(aws_sns_topic.backup_notifications[each.key].arn, null) ) backup_vault_events = each.value.notifications.backup_vault_events }