diff --git a/modules/aws/alternate-contacts/backplane/README.md b/modules/aws/alternate-contacts/backplane/README.md new file mode 100644 index 00000000..a9cfdd91 --- /dev/null +++ b/modules/aws/alternate-contacts/backplane/README.md @@ -0,0 +1,71 @@ +--- +name: AWS Alternate Contacts Backplane +supportedPlatforms: +- aws +description: | + Backplane infrastructure for the AWS Alternate Contacts building block. +--- + +This module sets up the IAM user and StackSet-based role deployment needed to manage alternate contacts on AWS accounts in your organization. + +It creates: + +1. An **IAM User** in your backplane account with permission to assume a service role in target accounts. +2. A **CloudFormation StackSet** deployed to the specified OUs that creates a service role in each target account with the necessary `account:*AlternateContact` permissions. + +## Usage + +```hcl +module "alternate_contacts_backplane" { + source = "./modules/aws/alternate-contacts/backplane" + + building_block_target_ou_ids = ["ou-xxxx-xxxxxxxx"] + + providers = { + aws.management = aws.management + aws.backplane = aws.backplane + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | ~> 5.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudformation_stack_set.permissions_in_target_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set) | resource | +| [aws_cloudformation_stack_set_instance.permissions_in_target_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set_instance) | resource | +| [aws_iam_access_key.backplane](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | +| [aws_iam_user.backplane](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_iam_user_policy.assume_roles](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy) | resource | +| [aws_iam_policy_document.building_block_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [backplane\_user\_name](#input\_backplane\_user\_name) | n/a | `string` | `"building-block-alternate-contacts"` | no | +| [building\_block\_target\_account\_access\_role\_name](#input\_building\_block\_target\_account\_access\_role\_name) | Name of the role that the backplane user will assume in the target account | `string` | `"building-block-alternate-contacts"` | no | +| [building\_block\_target\_ou\_ids](#input\_building\_block\_target\_ou\_ids) | List of OUs that the building block can be deployed to. Accounts in these OUs will receive the building\_block\_backplane\_account\_access\_role | `set(string)` | n/a | yes | +| [stackset\_region](#input\_stackset\_region) | AWS region to deploy the StackSet instances in | `string` | `"eu-central-1"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aws\_access\_key\_id](#output\_aws\_access\_key\_id) | Access key for the IAM user that can set alternate contacts | +| [aws\_secret\_access\_key](#output\_aws\_secret\_access\_key) | Secret key for the IAM user that can set alternate contacts | +| [role\_name](#output\_role\_name) | Name of the IAM role assumed in target accounts to set alternate contacts | + diff --git a/modules/aws/alternate-contacts/backplane/main.tf b/modules/aws/alternate-contacts/backplane/main.tf new file mode 100644 index 00000000..52baedfc --- /dev/null +++ b/modules/aws/alternate-contacts/backplane/main.tf @@ -0,0 +1,112 @@ +# AWS Alternate Contacts Backplane +# This module creates necessary IAM Users and role setup so that we have an IAM user that can set alternate contacts +# on any account in the target OU. + +# user referenced in building block definition +resource "aws_iam_user" "backplane" { + provider = aws.backplane + name = var.backplane_user_name +} + +resource "aws_iam_access_key" "backplane" { + provider = aws.backplane + user = aws_iam_user.backplane.name +} + +data "aws_partition" "current" { + provider = aws.backplane +} + +# access building block service role in target accounts +data "aws_iam_policy_document" "building_block_service" { + provider = aws.backplane + version = "2012-10-17" + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = ["arn:${data.aws_partition.current.partition}:iam::*:role/${var.building_block_target_account_access_role_name}"] + } +} + +resource "aws_iam_user_policy" "assume_roles" { + provider = aws.backplane + name = "assume-roles" + user = aws_iam_user.backplane.name + policy = data.aws_iam_policy_document.building_block_service.json +} + + +# this stackset automatically deploys the building block backplane role to target accounts +resource "aws_cloudformation_stack_set" "permissions_in_target_accounts" { + provider = aws.management + name = var.building_block_target_account_access_role_name + permission_model = "SERVICE_MANAGED" + auto_deployment { + enabled = true + retain_stacks_on_account_removal = false + } + operation_preferences { + failure_tolerance_count = 50 + max_concurrent_count = 50 + } + + template_body = jsonencode({ + AWSTemplateFormatVersion = "2010-09-09", + Description = "Grants the building block backplane ${aws_iam_user.backplane.name} access to manage alternate contacts on a managed account.", + Resources = { + BuildingBlockServiceRolePermissions = { + Type = "AWS::IAM::Role", + Properties = { + RoleName = var.building_block_target_account_access_role_name, + AssumeRolePolicyDocument = { + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = aws_iam_user.backplane.arn + }, + Action = "sts:AssumeRole" + } + ] + }, + Policies = [ + { + PolicyName = var.building_block_target_account_access_role_name + PolicyDocument = { + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "account:PutAlternateContact", + "account:GetAlternateContact", + "account:DeleteAlternateContact", + ], + Resource = "*" + } + ] + } + } + ] + } + } + } + }) + + capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"] + + lifecycle { + ignore_changes = [administration_role_arn] + } +} + +resource "aws_cloudformation_stack_set_instance" "permissions_in_target_accounts" { + provider = aws.management + deployment_targets { + organizational_unit_ids = var.building_block_target_ou_ids + } + + region = var.stackset_region + stack_set_name = aws_cloudformation_stack_set.permissions_in_target_accounts.name +} diff --git a/modules/aws/alternate-contacts/backplane/outputs.tf b/modules/aws/alternate-contacts/backplane/outputs.tf new file mode 100644 index 00000000..daeff043 --- /dev/null +++ b/modules/aws/alternate-contacts/backplane/outputs.tf @@ -0,0 +1,15 @@ +output "aws_access_key_id" { + description = "Access key for the IAM user that can set alternate contacts" + value = aws_iam_access_key.backplane.id +} + +output "aws_secret_access_key" { + description = "Secret key for the IAM user that can set alternate contacts" + sensitive = true + value = aws_iam_access_key.backplane.secret +} + +output "role_name" { + description = "Name of the IAM role assumed in target accounts to set alternate contacts" + value = var.building_block_target_account_access_role_name +} diff --git a/modules/aws/alternate-contacts/backplane/variables.tf b/modules/aws/alternate-contacts/backplane/variables.tf new file mode 100644 index 00000000..34b43f0a --- /dev/null +++ b/modules/aws/alternate-contacts/backplane/variables.tf @@ -0,0 +1,22 @@ +variable "backplane_user_name" { + type = string + nullable = false + default = "building-block-alternate-contacts" +} + +variable "building_block_target_account_access_role_name" { + type = string + description = "Name of the role that the backplane user will assume in the target account" + default = "building-block-alternate-contacts" +} + +variable "stackset_region" { + type = string + description = "AWS region to deploy the StackSet instances in" + default = "eu-central-1" +} + +variable "building_block_target_ou_ids" { + type = set(string) + description = "List of OUs that the building block can be deployed to. Accounts in these OUs will receive the building_block_backplane_account_access_role" +} diff --git a/modules/aws/alternate-contacts/backplane/versions.tf b/modules/aws/alternate-contacts/backplane/versions.tf new file mode 100644 index 00000000..1b1be9e1 --- /dev/null +++ b/modules/aws/alternate-contacts/backplane/versions.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + + configuration_aliases = [ + # This provider needs to point at the account owning your AWS Organization + # or the delegated admin account that can deploy StackSets over your AWS Organization. + aws.management, + + # This provider needs to point at the account that will host the IAM User for the building block backplane. + # Typically this is a dedicated account only used for building block automation. + aws.backplane + ] + } + } +} + diff --git a/modules/aws/alternate-contacts/buildingblock/README.md b/modules/aws/alternate-contacts/buildingblock/README.md new file mode 100644 index 00000000..a300f5d0 --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/README.md @@ -0,0 +1,84 @@ +--- +name: AWS Alternate Contacts +supportedPlatforms: +- aws +description: | + Sets the alternate contact information (billing, operations, security) for an AWS account. +--- + +This Terraform module configures the alternate contacts for an AWS account. AWS alternate contacts are used to receive notifications for billing, operations, and security-related communications, ensuring the right people are contacted for each concern. + +Each contact type is optional -- set only the ones you need. AWS allows exactly one contact per type (billing, operations, security). Each contact requires a name, title, email, and phone number. + +## Usage Examples + +Set only a security contact: + +```hcl +security_contact = { + name = "Jane Doe" + title = "Security Officer" + email = "security@example.com" + phone = "+1-555-555-0100" +} +``` + +Set billing and security contacts, skip operations: + +```hcl +billing_contact = { + name = "Carlos Salazar" + title = "CFO" + email = "billing@example.com" + phone = "+1-555-555-0199" +} + +security_contact = { + name = "Jane Doe" + title = "Security Officer" + email = "security@example.com" + phone = "+1-555-555-0100" +} +``` + +## Permissions + +Please reference the [backplane implementation](../backplane/) for the required permissions to deploy this building block. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | ~> 5.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_account_alternate_contact.billing](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource | +| [aws_account_alternate_contact.operations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource | +| [aws_account_alternate_contact.security](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_id](#input\_account\_id) | Target account id where the alternate contacts should be set | `string` | n/a | yes | +| [assume\_role\_name](#input\_assume\_role\_name) | The name of the role to assume in target account identified by account\_id | `string` | n/a | yes | +| [aws\_partition](#input\_aws\_partition) | The AWS partition to use. e.g. aws, aws-cn, aws-us-gov | `string` | `"aws"` | no | +| [billing\_contact](#input\_billing\_contact) | Billing alternate contact. Set to null to skip. All fields are required when set. |
object({
name = string
title = string
email = string
phone = string
})
| `null` | no | +| [operations\_contact](#input\_operations\_contact) | Operations alternate contact. Set to null to skip. All fields are required when set. |
object({
name = string
title = string
email = string
phone = string
})
| `null` | no | +| [security\_contact](#input\_security\_contact) | Security alternate contact. Set to null to skip. All fields are required when set. |
object({
name = string
title = string
email = string
phone = string
})
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [operations\_contacts](#output\_operations\_contacts) | Map of configured alternate contact types to their email addresses | + diff --git a/modules/aws/alternate-contacts/buildingblock/logo.png b/modules/aws/alternate-contacts/buildingblock/logo.png new file mode 100644 index 00000000..9cfafbf8 Binary files /dev/null and b/modules/aws/alternate-contacts/buildingblock/logo.png differ diff --git a/modules/aws/alternate-contacts/buildingblock/main.tf b/modules/aws/alternate-contacts/buildingblock/main.tf new file mode 100644 index 00000000..40f4d197 --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/main.tf @@ -0,0 +1,26 @@ +resource "aws_account_alternate_contact" "operations" { + alternate_contact_type = "OPERATIONS" + + name = var.operations_contact.name + title = var.operations_contact.title + email_address = var.operations_contact.email + phone_number = var.operations_contact.phone +} + +resource "aws_account_alternate_contact" "billing" { + alternate_contact_type = "BILLING" + + name = var.billing_contact.name + title = var.billing_contact.title + email_address = var.billing_contact.email + phone_number = var.billing_contact.phone +} + +resource "aws_account_alternate_contact" "security" { + alternate_contact_type = "SECURITY" + + name = var.security_contact.name + title = var.security_contact.title + email_address = var.security_contact.email + phone_number = var.security_contact.phone +} diff --git a/modules/aws/alternate-contacts/buildingblock/outputs.tf b/modules/aws/alternate-contacts/buildingblock/outputs.tf new file mode 100644 index 00000000..52a2fd72 --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/outputs.tf @@ -0,0 +1,8 @@ +output "operations_contacts" { + description = "Map of configured alternate contact types to their email addresses" + value = { + "operations" = aws_account_alternate_contact.operations.email_address + "billing" = aws_account_alternate_contact.billing.email_address + "security" = aws_account_alternate_contact.security.email_address + } +} diff --git a/modules/aws/alternate-contacts/buildingblock/provider.tf b/modules/aws/alternate-contacts/buildingblock/provider.tf new file mode 100644 index 00000000..3fa6bd0f --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/provider.tf @@ -0,0 +1,8 @@ +provider "aws" { + region = "eu-central-1" + + assume_role { + role_arn = "arn:${var.aws_partition}:iam::${var.account_id}:role/${var.assume_role_name}" + session_name = "deploy-alternate-contacts" + } +} diff --git a/modules/aws/alternate-contacts/buildingblock/variables.tf b/modules/aws/alternate-contacts/buildingblock/variables.tf new file mode 100644 index 00000000..7b499d0c --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/variables.tf @@ -0,0 +1,50 @@ +variable "billing_contact" { + type = object({ + name = string + title = string + email = string + phone = string + }) + description = "Billing alternate contact. Set to null to skip. All fields are required when set." + default = null +} + +variable "operations_contact" { + type = object({ + name = string + title = string + email = string + phone = string + }) + description = "Operations alternate contact. Set to null to skip. All fields are required when set." + default = null +} + +variable "security_contact" { + type = object({ + name = string + title = string + email = string + phone = string + }) + description = "Security alternate contact. Set to null to skip. All fields are required when set." + default = null +} + +// env vars + +variable "account_id" { + type = string + description = "Target account id where the alternate contacts should be set" +} + +variable "assume_role_name" { + type = string + description = "The name of the role to assume in target account identified by account_id" +} + +variable "aws_partition" { + type = string + description = "The AWS partition to use. e.g. aws, aws-cn, aws-us-gov" + default = "aws" +} diff --git a/modules/aws/alternate-contacts/buildingblock/versions.tf b/modules/aws/alternate-contacts/buildingblock/versions.tf new file mode 100644 index 00000000..28b0cd98 --- /dev/null +++ b/modules/aws/alternate-contacts/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +}