diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aa70e2e..b9e4b8e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -29,17 +29,17 @@ updates: interval: weekly - directory: / - # ignore: - # # Managed by cisagov/skeleton-tf-module - # - dependency-name: hashicorp/aws + ignore: + # Managed by cisagov/skeleton-tf-module + - dependency-name: hashicorp/aws package-ecosystem: terraform schedule: interval: weekly - directory: /examples/basic_usage - # ignore: - # # Managed by cisagov/skeleton-tf-module - # - dependency-name: hashicorp/aws + ignore: + # Managed by cisagov/skeleton-tf-module + - dependency-name: hashicorp/aws package-ecosystem: terraform schedule: interval: weekly diff --git a/README.md b/README.md index 96c6c1d..b700e88 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,87 @@ [![GitHub Build Status](https://github.com/cisagov/cool-accounts-cyhy/workflows/build/badge.svg)](https://github.com/cisagov/cool-accounts-cyhy/actions) -This is a generic skeleton project that can be used to quickly get a -new [cisagov](https://github.com/cisagov) [Terraform -module](https://www.terraform.io/docs/modules/index.html) GitHub -repository started. This skeleton project contains [licensing -information](LICENSE), as well as [pre-commit -hooks](https://pre-commit.com) and -[GitHub Actions](https://github.com/features/actions) configurations -appropriate for the major languages that we use. - -See [here](https://www.terraform.io/docs/modules/index.html) for more -details on Terraform modules and the standard module structure. - -## Usage ## - -```hcl -module "example" { - source = "github.com/cisagov/cool-accounts-cyhy" - - aws_region = "us-west-1" - aws_availability_zone = "b" - subnet_id = "subnet-0123456789abcdef0" -} -``` - -## Examples ## - -- [Basic usage](https://github.com/cisagov/cool-accounts-cyhy/tree/develop/examples/basic_usage) +This project contains Terraform code to perform the initial configuration of a +COOL Cyber Hygiene (CyHy) account. This Terraform code creates and configures +the most basic resources needed to build out services and environments. + +It creates an IAM role that allows sufficient permissions to provision all AWS +resources in this account. This role has a trust relationship with the COOL +users account. + +## Pre-requisites ## + +- [Terraform](https://www.terraform.io/) installed on your system. +- An accessible AWS S3 bucket to store Terraform state (specified in + [backend.tf](backend.tf)). +- An accessible AWS DynamoDB database to store the Terraform state lock + (specified in [backend.tf](backend.tf)). + +We recommend creating the S3 bucket and DynamoDB table above by applying the +Terraform code in the "terraform" subdirectory of +[`cisagov/cool-accounts`](https://github.com/cisagov/cool-accounts). + +## Bootstrapping this account ## + +Note that the COOL Cyber Hygiene account must be bootstrapped. This is because +initially there is no IAM role that can be assumed to build out these resources. +Therefore you must first apply the Terraform code using programmatic credentials +for AWSAdministratorAccess as obtained for the COOL Cyber Hygiene account from +the COOL AWS SSO page. + +After this initial apply your desired IAM role will exist, and it will be +assumable from your IAM user that exists in the COOL users account. Therefore +you can apply future changes using your IAM user credentials. + +To do this bootstrapping, follow these steps: + +1. Comment out the `profile = "cool-cyhy-provisionaccount"` line for the + "default" provider in `providers.tf` and directly below that uncomment the + line `profile = "cool-cyhy-account-admin"`. +1. Create a new AWS profile called `cool-cyhy-account-admin` in your local + configuration using the "AWSAdministratorAccess" credentials (access key ID, + secret access key, and session token) as obtained from the COOL Cyber Hygiene + account: + + ```ini + [cool-cyhy-account-admin] + aws_access_key_id = + aws_secret_access_key = + aws_session_token = + ``` + +1. Create a Terraform workspace (if you haven't already done so) by running + `terraform workspace new ` +1. Create a `.tfvars` file with any optional variables + that you wish to override (see [Inputs](#inputs) below for + details): + + ```hcl + tags = { + Team = "VM - Development" + Application = "COOL - Cyber Hygiene" + Workspace = "production" + } + ``` + +1. Run the command `terraform init`. +1. Run the command `terraform apply -var-file=.tfvars`. +1. Revert the changes you made to `providers.tf` in step 1. +1. Create a new AWS profile called `cool-cyhy-provisionaccount` in your local + configuration that includes the `provisionaccount_role` ARN output from the + previous step, for example: + + ```ini + [cool-cyhy-provisionaccount] + role_arn = arn:aws:iam::111111111111:role/ProvisionAccount + role_session_name = your.session.name + source_profile = cool-user-base-profile + ``` + +1. Run the command `terraform apply -var-file=.tfvars`. + +At this point the account has been bootstrapped, and you can apply future +changes by simply running `terraform apply -var-file=.tfvars`. ## Requirements ## @@ -43,51 +97,72 @@ module "example" { | Name | Version | |------|---------| | aws | ~> 4.9 | +| aws.organizationsreadonly | ~> 4.9 | ## Modules ## -No modules. +| Name | Source | Version | +|------|--------|---------| +| cw\_alarm\_sns | github.com/cisagov/sns-send-to-account-email-tf-module | n/a | +| disable-inactive-iam-users | github.com/cisagov/disable-inactive-iam-users-tf-module | n/a | +| provisionaccount | github.com/cisagov/provisionaccount-role-tf-module | n/a | +| session\_manager | github.com/cisagov/session-manager-tf-module | n/a | +| user\_group\_mod\_event | github.com/cisagov/user-group-mod-alert-tf-module | n/a | +| user\_group\_mod\_sns | github.com/cisagov/sns-send-to-account-email-tf-module | n/a | ## Resources ## | Name | Type | |------|------| -| [aws_instance.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | -| [aws_ami.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | -| [aws_default_tags.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags) | data source | +| [aws_iam_policy.provisionlambdabucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.provisionssmsessionmanager_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.read_cool_lambda_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role_policy_attachment.provisionlambdabucket_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.provisionssmsessionmanager_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.read_cool_lambda_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_s3_bucket.lambda_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_ownership_controls.lambda_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | +| [aws_s3_bucket_public_access_block.lambda_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.lambda_artifacts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_caller_identity.cyhy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.provisionlambdabucket_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.provisionssmsessionmanager_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.read_cool_lambda_bucket_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.sns_topic_access_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_organizations_organization.cool](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/organizations_organization) | data source | ## Inputs ## | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| ami\_owner\_account\_id | The ID of the AWS account that owns the Example AMI, or "self" if the AMI is owned by the same account as the provisioner. | `string` | `"self"` | no | -| aws\_availability\_zone | The AWS availability zone to deploy into (e.g. a, b, c, etc.). | `string` | `"a"` | no | -| aws\_region | The AWS region to deploy into (e.g. us-east-1). | `string` | `"us-east-1"` | no | -| subnet\_id | The ID of the AWS subnet to deploy into (e.g. subnet-0123456789abcdef0). | `string` | n/a | yes | +| aws\_region | The AWS region where the non-global resources for the Cyber Hygiene account are to be provisioned (e.g. "us-east-1"). | `string` | `"us-east-1"` | no | +| cool\_lambda\_artifacts\_s3\_bucket | The name of the bucket where COOL Lambda deployment packages are to be stored. | `string` | n/a | yes | +| cyhy\_lambda\_artifacts\_s3\_bucket\_prefix | The prefix of the name of the bucket in the Cyber Hygiene account where any Lambda deployment artifacts for a CyHy environment will be stored. A unique bucket name beginning with the specified prefix will be created. | `string` | `"cool-cyhy-lambda-deployment-artifacts"` | no | +| disable\_inactive\_users\_lambda\_key | The S3 key associated with the Lambda function deployment package to disable inactive IAM users. | `string` | n/a | yes | +| provisionaccount\_role\_description | The description to associate with the IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account. | `string` | `"Allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account."` | no | +| provisionaccount\_role\_name | The name to assign the IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account. | `string` | `"ProvisionAccount"` | no | +| provisionlambdabucket\_policy\_description | The description to associate with the IAM policy that allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account. | `string` | `"Allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account."` | no | +| provisionlambdabucket\_policy\_name | The name to assign the IAM policy that allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account. | `string` | `"ProvisionLambdaArtifactsBucket"` | no | +| provisionssmsessionmanager\_policy\_description | The description to associate with the IAM policy that allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account. | `string` | `"Allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account."` | no | +| provisionssmsessionmanager\_policy\_name | The name to assign the IAM policy that allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account. | `string` | `"ProvisionSSMSessionManager"` | no | +| read\_cool\_lambda\_bucket\_policy\_description | The description to associate with the IAM role that allows read-only access to the bucket in the Terraform account containing Lambda deployments. | `string` | `"Allows read-only access to the bucket in the Terraform account containing Lambda deployments."` | no | +| read\_cool\_lambda\_bucket\_policy\_name | The name to assign the IAM policy that allows read-only access to the bucket in the Terraform account containing Lambda deployments. | `string` | `"LambdaBucketReadOnly"` | no | +| tags | Tags to apply to all AWS resources provisioned. | `map(string)` | `{}` | no | ## Outputs ## | Name | Description | |------|-------------| -| arn | The EC2 instance ARN. | -| availability\_zone | The AZ where the EC2 instance is deployed. | -| id | The EC2 instance ID. | -| private\_ip | The private IP of the EC2 instance. | -| subnet\_id | The ID of the subnet where the EC2 instance is deployed. | +| cw\_alarm\_sns\_topic | The SNS topic to which a message is sent when a CloudWatch alarm is triggered. | +| lambda\_artifacts\_bucket | The S3 bucket in the Cyber Hygiene account where Lambda artifacts are stored. | +| provisionaccount\_role | The IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account. | +| ssm\_session\_role | An IAM role that allows creation of SSM SessionManager sessions to any EC2 instance in this account. | ## Notes ## Running `pre-commit` requires running `terraform init` in every directory that -contains Terraform code. In this repository, these are the main directory and -every directory under `examples/`. - -## New Repositories from a Skeleton ## - -Please see our [Project Setup guide](https://github.com/cisagov/development-guide/tree/develop/project_setup) -for step-by-step instructions on how to start a new repository from -a skeleton. This will save you time and effort when configuring a -new repository! +contains Terraform code. In this repository, this is just the main directory. ## Contributing ## diff --git a/backend.tf b/backend.tf new file mode 100644 index 0000000..1c4814c --- /dev/null +++ b/backend.tf @@ -0,0 +1,10 @@ +terraform { + backend "s3" { + bucket = "cisa-cool-terraform-state" + dynamodb_table = "terraform-state-lock" + encrypt = true + key = "cool-accounts-cyhy/terraform.tfstate" + profile = "cool-terraform-backend" + region = "us-east-1" + } +} diff --git a/disable-inactive-iam-users.tf b/disable-inactive-iam-users.tf new file mode 100644 index 0000000..0f5e15b --- /dev/null +++ b/disable-inactive-iam-users.tf @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------------ +# Create the EventBridge event rule that triggers at a fixed cadence, kicking +# off a Lambda function that disables inactive IAM users. +# ------------------------------------------------------------------------------ +module "disable-inactive-iam-users" { + depends_on = [ + aws_iam_role_policy_attachment.read_cool_lambda_bucket, + ] + providers = { + aws = aws + } + + source = "github.com/cisagov/disable-inactive-iam-users-tf-module" + + lambda_bucket_name = var.cool_lambda_artifacts_s3_bucket + lambda_key = var.disable_inactive_users_lambda_key +} diff --git a/examples/basic_usage/.terraform-docs.yml b/examples/basic_usage/.terraform-docs.yml deleted file mode 120000 index 2afdcf8..0000000 --- a/examples/basic_usage/.terraform-docs.yml +++ /dev/null @@ -1 +0,0 @@ -../../.terraform-docs.yml \ No newline at end of file diff --git a/examples/basic_usage/README.md b/examples/basic_usage/README.md deleted file mode 100644 index d120104..0000000 --- a/examples/basic_usage/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Launch an example EC2 instance in a new VPC # - -## Usage ## - -To run this example you need to execute the `terraform init` command -followed by the `terraform apply` command. - -Note that this example may create resources which cost money. Run -`terraform destroy` when you no longer need these resources. - - -## Requirements ## - -| Name | Version | -|------|---------| -| terraform | ~> 1.0 | -| aws | ~> 4.9 | - -## Providers ## - -| Name | Version | -|------|---------| -| aws | ~> 4.9 | - -## Modules ## - -| Name | Source | Version | -|------|--------|---------| -| example | ../../ | n/a | - -## Resources ## - -| Name | Type | -|------|------| -| [aws_subnet.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | -| [aws_vpc.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource | - -## Inputs ## - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| ami\_owner\_account\_id | The ID of the AWS account that owns the AMI, or "self" if the AMI is owned by the same account as the provisioner. | `string` | `"self"` | no | -| aws\_availability\_zone | The AWS availability zone to deploy into (e.g. a, b, c, etc.). | `string` | `"a"` | no | -| aws\_region | The AWS region to deploy into (e.g. us-east-1). | `string` | `"us-east-1"` | no | -| tags | Tags to apply to all AWS resources created. | `map(string)` | ```{ "Testing": true }``` | no | -| tf\_role\_arn | The ARN of the role that can terraform non-specialized resources. | `string` | n/a | yes | - -## Outputs ## - -| Name | Description | -|------|-------------| -| arn | The EC2 instance ARN. | -| availability\_zone | The AZ where the EC2 instance is deployed. | -| id | The EC2 instance ID. | -| private\_ip | The private IP of the EC2 instance. | -| subnet\_id | The ID of the subnet where the EC2 instance is deployed. | - diff --git a/examples/basic_usage/main.tf b/examples/basic_usage/main.tf deleted file mode 100644 index efdc5bc..0000000 --- a/examples/basic_usage/main.tf +++ /dev/null @@ -1,26 +0,0 @@ -provider "aws" { - # Our primary provider uses our terraform role - assume_role { - role_arn = var.tf_role_arn - session_name = "terraform-example" - } - default_tags { - tags = var.tags - } - region = var.aws_region -} - -#------------------------------------------------------------------------------- -# Configure the example module. -#------------------------------------------------------------------------------- -module "example" { - source = "../../" - providers = { - aws = aws - } - - ami_owner_account_id = var.ami_owner_account_id - aws_availability_zone = var.aws_availability_zone - aws_region = var.aws_region - subnet_id = aws_subnet.example.id -} diff --git a/examples/basic_usage/outputs.tf b/examples/basic_usage/outputs.tf deleted file mode 100644 index 542df31..0000000 --- a/examples/basic_usage/outputs.tf +++ /dev/null @@ -1,24 +0,0 @@ -output "arn" { - description = "The EC2 instance ARN." - value = module.example.arn -} - -output "availability_zone" { - description = "The AZ where the EC2 instance is deployed." - value = module.example.availability_zone -} - -output "id" { - description = "The EC2 instance ID." - value = module.example.id -} - -output "private_ip" { - description = "The private IP of the EC2 instance." - value = module.example.private_ip -} - -output "subnet_id" { - description = "The ID of the subnet where the EC2 instance is deployed." - value = module.example.subnet_id -} diff --git a/examples/basic_usage/variables.tf b/examples/basic_usage/variables.tf deleted file mode 100644 index 70b275a..0000000 --- a/examples/basic_usage/variables.tf +++ /dev/null @@ -1,42 +0,0 @@ -# ------------------------------------------------------------------------------ -# Required parameters -# -# You must provide a value for each of these parameters. -# ------------------------------------------------------------------------------ - -variable "tf_role_arn" { - description = "The ARN of the role that can terraform non-specialized resources." - type = string -} - -# ------------------------------------------------------------------------------ -# Optional parameters -# -# These parameters have reasonable defaults. -# ------------------------------------------------------------------------------ - -variable "ami_owner_account_id" { - default = "self" - description = "The ID of the AWS account that owns the AMI, or \"self\" if the AMI is owned by the same account as the provisioner." - type = string -} - -variable "aws_availability_zone" { - default = "a" - description = "The AWS availability zone to deploy into (e.g. a, b, c, etc.)." - type = string -} - -variable "aws_region" { - default = "us-east-1" - description = "The AWS region to deploy into (e.g. us-east-1)." - type = string -} - -variable "tags" { - default = { - Testing = true - } - description = "Tags to apply to all AWS resources created." - type = map(string) -} diff --git a/examples/basic_usage/versions.tf b/examples/basic_usage/versions.tf deleted file mode 100644 index 9db27b0..0000000 --- a/examples/basic_usage/versions.tf +++ /dev/null @@ -1,23 +0,0 @@ -terraform { - # If you use any other providers you should also pin them to the - # major version currently being used. This practice will help us - # avoid unwelcome surprises. - required_providers { - # Version 4.9 of the Terraform AWS provider made changes to the S3 bucket - # refactor that is in place for versions 4.0-4.8 of the provider. With v4.9 - # only non-breaking changes and deprecation notices are introduced. Using - # this version will simplify migration to the new, broken out AWS S3 bucket - # configuration resources. Please see - # https://github.com/hashicorp/terraform-provider-aws/pull/23985 - # for more information about the changes in v4.9 and - # https://www.hashicorp.com/blog/terraform-aws-provider-4-0-refactors-s3-bucket-resource - # for more information about the S3 bucket refactor. - aws = { - source = "hashicorp/aws" - version = "~> 4.9" - } - } - - # We want to hold off on 1.1 or higher until we have tested it. - required_version = "~> 1.0" -} diff --git a/examples/basic_usage/vpc.tf b/examples/basic_usage/vpc.tf deleted file mode 100644 index 947e0eb..0000000 --- a/examples/basic_usage/vpc.tf +++ /dev/null @@ -1,20 +0,0 @@ -#------------------------------------------------------------------------------- -# Create a VPC -#------------------------------------------------------------------------------- - -resource "aws_vpc" "example" { - cidr_block = "10.230.0.0/24" - enable_dns_hostnames = true - tags = { "Name" : "Example" } -} - -#------------------------------------------------------------------------------- -# Create a subnet -#------------------------------------------------------------------------------- - -resource "aws_subnet" "example" { - availability_zone = "${var.aws_region}${var.aws_availability_zone}" - cidr_block = "10.230.0.0/28" - tags = { "Name" : "Example" } - vpc_id = aws_vpc.example.id -} diff --git a/lambda_artifacts_bucket.tf b/lambda_artifacts_bucket.tf new file mode 100644 index 0000000..af7e1b8 --- /dev/null +++ b/lambda_artifacts_bucket.tf @@ -0,0 +1,46 @@ +# This bucket is used to store the deployment packages for any Lambda functions +# that will be used in a CyHy environment. +resource "aws_s3_bucket" "lambda_artifacts" { + bucket_prefix = var.cyhy_lambda_artifacts_s3_bucket_prefix + + tags = { + "Name" = "Lambda Deployment Artifacts" + } + + lifecycle { + prevent_destroy = true + } +} + +# Ensure the S3 bucket is encrypted +resource "aws_s3_bucket_server_side_encryption_configuration" "lambda_artifacts" { + bucket = aws_s3_bucket.lambda_artifacts.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# This blocks ANY public access to the bucket or the objects it +# contains, even if misconfigured to allow public access. +resource "aws_s3_bucket_public_access_block" "lambda_artifacts" { + block_public_acls = true + block_public_policy = true + bucket = aws_s3_bucket.lambda_artifacts.id + ignore_public_acls = true + restrict_public_buckets = true +} + +# Any objects placed into this bucket should be owned by the bucket +# owner. This ensures that even if objects are added by a different +# account, the bucket-owning account retains full control over the +# objects stored in this bucket. +resource "aws_s3_bucket_ownership_controls" "lambda_artifacts" { + bucket = aws_s3_bucket.lambda_artifacts.id + + rule { + object_ownership = "BucketOwnerEnforced" + } +} diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..221ecc0 --- /dev/null +++ b/locals.tf @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------------ +# Retrieve the effective Account ID, User ID, and ARN in which Terraform is +# authorized. This is used to determine the CyHy account ID. +# ------------------------------------------------------------------------------ +data "aws_caller_identity" "cyhy" {} + +# Retrieve the information for all accounts in the organization. This is used, +# for instance, to lookup the account ID for the Users account. +data "aws_organizations_organization" "cool" { + provider = aws.organizationsreadonly +} + +# ------------------------------------------------------------------------------ +# Evaluate expressions for use throughout this configuration. +# ------------------------------------------------------------------------------ +locals { + # Get the CyHy account ID. + cyhy_account_id = data.aws_caller_identity.cyhy.id + + # Find the Users account + users_account_id = [ + for account in data.aws_organizations_organization.cool.accounts : + account.id + if account.name == "Users" + ][0] +} diff --git a/main.tf b/main.tf deleted file mode 100644 index 0646a05..0000000 --- a/main.tf +++ /dev/null @@ -1,61 +0,0 @@ -# ------------------------------------------------------------------------------ -# Deploy the example AMI from cisagov/skeleton-packer in AWS. -# ------------------------------------------------------------------------------ - -# ------------------------------------------------------------------------------ -# Look up the latest example AMI from cisagov/skeleton-packer. -# -# NOTE: This Terraform data source must return at least one AMI result -# or the apply will fail. -# ------------------------------------------------------------------------------ - -# The AMI from cisagov/skeleton-packer -data "aws_ami" "example" { - filter { - name = "name" - values = [ - "example-hvm-*-x86_64-ebs", - ] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "root-device-type" - values = ["ebs"] - } - - most_recent = true - owners = [ - var.ami_owner_account_id - ] -} - -# The default tags configured for the default provider -data "aws_default_tags" "default" {} - -# The example EC2 instance -resource "aws_instance" "example" { - ami = data.aws_ami.example.id - availability_zone = "${var.aws_region}${var.aws_availability_zone}" - instance_type = "t3.micro" - subnet_id = var.subnet_id - - # The tag or tags specified here will be merged with the provider's - # default tags. - tags = { - "Name" = "Example" - } - # volume_tags does not yet inherit the default tags from the - # provider. See hashicorp/terraform-provider-aws#19188 for more - # details. - volume_tags = merge( - data.aws_default_tags.default.tags, - { - "Name" = "Example" - }, - ) -} diff --git a/outputs.tf b/outputs.tf index ce18699..5d62c78 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,24 +1,19 @@ -output "arn" { - description = "The EC2 instance ARN." - value = aws_instance.example.arn +output "cw_alarm_sns_topic" { + description = "The SNS topic to which a message is sent when a CloudWatch alarm is triggered." + value = module.cw_alarm_sns.sns_topic } -output "availability_zone" { - description = "The AZ where the EC2 instance is deployed." - value = aws_instance.example.availability_zone +output "lambda_artifacts_bucket" { + description = "The S3 bucket in the Cyber Hygiene account where Lambda artifacts are stored." + value = aws_s3_bucket.lambda_artifacts } -output "id" { - description = "The EC2 instance ID." - value = aws_instance.example.id +output "provisionaccount_role" { + description = "The IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account." + value = module.provisionaccount.provisionaccount_role } -output "private_ip" { - description = "The private IP of the EC2 instance." - value = aws_instance.example.private_ip -} - -output "subnet_id" { - description = "The ID of the subnet where the EC2 instance is deployed." - value = aws_instance.example.subnet_id +output "ssm_session_role" { + description = "An IAM role that allows creation of SSM SessionManager sessions to any EC2 instance in this account." + value = module.session_manager.ssm_session_role } diff --git a/providers.tf b/providers.tf index bc1ee01..194af06 100644 --- a/providers.tf +++ b/providers.tf @@ -1,13 +1,24 @@ -# This is an example of what a provider looks like. -# -# provider "aws" { -# alias = "myprovider" -# assume_role { -# role_arn = "arn:aws:iam::123456789012:role/MyRole" -# session_name = "MySessionName" -# } -# default_tags { -# tags = var.tags -# } -# region = var.aws_region -# } +# This is the "default" provider that is used to create resources +# inside the COOL CyHy account. +provider "aws" { + default_tags { + tags = var.tags + } + # Use this profile once the account has been bootstrapped. + profile = "cool-cyhy-provisionaccount" + # Use this profile, defined using programmatic credentials for + # AWSAdministratorAccess as obtained for the COOL CyHy account + # from the AWS SSO page, to bootstrap the account. + # profile = "cool-cyhy-account-admin" + region = var.aws_region +} + +# Read-only AWS Organizations provider +provider "aws" { + alias = "organizationsreadonly" + default_tags { + tags = var.tags + } + profile = "cool-master-organizationsreadonly" + region = var.aws_region +} diff --git a/provisionaccount.tf b/provisionaccount.tf new file mode 100644 index 0000000..d36c80e --- /dev/null +++ b/provisionaccount.tf @@ -0,0 +1,7 @@ +module "provisionaccount" { + source = "github.com/cisagov/provisionaccount-role-tf-module" + + provisionaccount_role_description = var.provisionaccount_role_description + provisionaccount_role_name = var.provisionaccount_role_name + users_account_id = local.users_account_id +} diff --git a/provisionlambdabucket_policy.tf b/provisionlambdabucket_policy.tf new file mode 100644 index 0000000..c66cd10 --- /dev/null +++ b/provisionlambdabucket_policy.tf @@ -0,0 +1,32 @@ +# ------------------------------------------------------------------------------ +# Create the IAM policy that allows all of the permissions necessary to +# provision the Lambda deployment artifacts bucket. +# ------------------------------------------------------------------------------ + +data "aws_iam_policy_document" "provisionlambdabucket_policy_doc" { + statement { + actions = [ + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:DeleteBucketPolicy", + "s3:Get*", + "s3:ListBucket", + "s3:PutBucketOwnershipControls", + "s3:PutBucketPolicy", + "s3:PutBucketPublicAccessBlock", + "s3:PutBucketTagging", + "s3:PutEncryptionConfiguration", + "s3:TagResource", + ] + + resources = [ + "arn:aws:s3:::${var.cyhy_lambda_artifacts_s3_bucket_prefix}*", + ] + } +} + +resource "aws_iam_policy" "provisionlambdabucket_policy" { + description = var.provisionlambdabucket_policy_description + name = var.provisionlambdabucket_policy_name + policy = data.aws_iam_policy_document.provisionlambdabucket_policy_doc.json +} diff --git a/provisionlambdabucket_policy_attachment.tf b/provisionlambdabucket_policy_attachment.tf new file mode 100644 index 0000000..d259b88 --- /dev/null +++ b/provisionlambdabucket_policy_attachment.tf @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------------ +# Attach to the ProvisionAccount role the IAM policy that allows provisioning of +# the Lambda deployment artifacts bucket. +# ------------------------------------------------------------------------------ + +resource "aws_iam_role_policy_attachment" "provisionlambdabucket_policy_attachment" { + policy_arn = aws_iam_policy.provisionlambdabucket_policy.arn + role = var.provisionaccount_role_name +} diff --git a/provisionssmsessionmanager_policy.tf b/provisionssmsessionmanager_policy.tf new file mode 100644 index 0000000..756428d --- /dev/null +++ b/provisionssmsessionmanager_policy.tf @@ -0,0 +1,53 @@ +# ------------------------------------------------------------------------------ +# Create the IAM policy that allows all of the permissions necessary to +# provision the SSM Document resource and set up SSM session logging in this +# account. +# ------------------------------------------------------------------------------ + +data "aws_iam_policy_document" "provisionssmsessionmanager_policy_doc" { + # SSM document permissions + statement { + actions = [ + "ssm:AddTagsToResource", + "ssm:CreateDocument", + "ssm:DeleteDocument", + "ssm:DescribeDocument*", + "ssm:GetDocument", + "ssm:UpdateDocument*", + ] + + resources = [ + "arn:aws:ssm:${var.aws_region}:${local.cyhy_account_id}:document/SSM-SessionManagerRunShell", + ] + } + + # CloudWatch log group permissions + statement { + actions = [ + "logs:CreateLogGroup", + "logs:DescribeLogGroups", + "logs:ListTagsLogGroup", + ] + + resources = [ + "*", + ] + } + statement { + actions = [ + "logs:DeleteLogGroup", + "logs:PutRetentionPolicy", + "logs:TagLogGroup", + ] + + resources = [ + "arn:aws:logs:${var.aws_region}:${local.cyhy_account_id}:log-group:${module.session_manager.ssm_session_log_group.name}:*", + ] + } +} + +resource "aws_iam_policy" "provisionssmsessionmanager_policy" { + description = var.provisionssmsessionmanager_policy_description + name = var.provisionssmsessionmanager_policy_name + policy = data.aws_iam_policy_document.provisionssmsessionmanager_policy_doc.json +} diff --git a/provisionssmsessionmanager_policy_attachment.tf b/provisionssmsessionmanager_policy_attachment.tf new file mode 100644 index 0000000..da257a2 --- /dev/null +++ b/provisionssmsessionmanager_policy_attachment.tf @@ -0,0 +1,10 @@ +# ------------------------------------------------------------------------------ +# Attach to the ProvisionAccount role the IAM policy that allows all of the +# permissions necessary to provision the SSM Document resource and set up SSM +# session logging in this account. +# ------------------------------------------------------------------------------ + +resource "aws_iam_role_policy_attachment" "provisionssmsessionmanager_policy_attachment" { + policy_arn = aws_iam_policy.provisionssmsessionmanager_policy.arn + role = module.provisionaccount.provisionaccount_role.name +} diff --git a/read_cool_lambda_bucket_policy.tf b/read_cool_lambda_bucket_policy.tf new file mode 100644 index 0000000..e15bcae --- /dev/null +++ b/read_cool_lambda_bucket_policy.tf @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------------ +# Create the IAM policy that allows all of the permissions necessary +# to read from the bucket containing the COOL Lambda deployments. +# ------------------------------------------------------------------------------ + +data "aws_iam_policy_document" "read_cool_lambda_bucket_policy_doc" { + statement { + actions = [ + "s3:GetObject", + "s3:ListBucket", + ] + resources = [ + "arn:aws:s3:::${var.cool_lambda_artifacts_s3_bucket}", + "arn:aws:s3:::${var.cool_lambda_artifacts_s3_bucket}/*" + ] + } +} + +resource "aws_iam_policy" "read_cool_lambda_bucket_policy" { + description = var.read_cool_lambda_bucket_policy_description + name = var.read_cool_lambda_bucket_policy_name + policy = data.aws_iam_policy_document.read_cool_lambda_bucket_policy_doc.json +} diff --git a/read_cool_lambda_bucket_policy_attachment.tf b/read_cool_lambda_bucket_policy_attachment.tf new file mode 100644 index 0000000..7a42601 --- /dev/null +++ b/read_cool_lambda_bucket_policy_attachment.tf @@ -0,0 +1,10 @@ +# ------------------------------------------------------------------------------ +# Attach to the ProvisionAccount role the IAM policy that allows reading from +# the S3 bucket in the Terraform account where Lambda deployment packages are +# stored. +# ------------------------------------------------------------------------------ + +resource "aws_iam_role_policy_attachment" "read_cool_lambda_bucket" { + policy_arn = aws_iam_policy.read_cool_lambda_bucket_policy.arn + role = module.provisionaccount.provisionaccount_role.name +} diff --git a/session_manager.tf b/session_manager.tf new file mode 100644 index 0000000..8a83b5f --- /dev/null +++ b/session_manager.tf @@ -0,0 +1,11 @@ +# ------------------------------------------------------------------------------ +# Provision SSM Session Manager and configure it for session logging. +# ------------------------------------------------------------------------------ + +module "session_manager" { + source = "github.com/cisagov/session-manager-tf-module" + + other_accounts = [ + local.users_account_id, + ] +} diff --git a/sns.tf b/sns.tf new file mode 100644 index 0000000..b5852b7 --- /dev/null +++ b/sns.tf @@ -0,0 +1,15 @@ +# ------------------------------------------------------------------------------ +# Create the SNS topic that allows email to be sent for CloudWatch +# alarms. Subscribe the account email to the new SNS topic. +# ------------------------------------------------------------------------------ + +module "cw_alarm_sns" { + providers = { + aws = aws + aws.organizations_read_only = aws.organizationsreadonly + } + source = "github.com/cisagov/sns-send-to-account-email-tf-module" + + topic_display_name = "cloudwatch_alarms" + topic_name = "cloudwatch-alarms" +} diff --git a/user-group-mod-notification.tf b/user-group-mod-notification.tf new file mode 100644 index 0000000..8b987cd --- /dev/null +++ b/user-group-mod-notification.tf @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------------ +# Create the SNS topic that allows email to be sent whenever a new IAM or SSO +# user is created or deleted, a user is added or removed from a group, or a +# group is created or deleted. Subscribe the account email to the new SNS +# topic. +# ------------------------------------------------------------------------------ + +data "aws_iam_policy_document" "sns_topic_access_policy_doc" { + # Allow EventBridge to publish to the SNS topic. + statement { + actions = [ + "sns:Publish", + ] + + resources = [ + # We can't use module.user_group_mod_sns.sns_topic.arn here because it + # creates a cycle; fortunately, we can create the SNS topic ARN manually. + "arn:aws:sns:${var.aws_region}:${local.cyhy_account_id}:user-or-group-modified", + ] + + principals { + identifiers = ["events.amazonaws.com"] + type = "Service" + } + } +} + +module "user_group_mod_sns" { + providers = { + aws = aws + aws.organizations_read_only = aws.organizationsreadonly + } + source = "github.com/cisagov/sns-send-to-account-email-tf-module" + + topic_access_policy = data.aws_iam_policy_document.sns_topic_access_policy_doc.json + topic_display_name = "IAM or SSO user or group modified" + topic_name = "user-or-group-modified" +} + +# ------------------------------------------------------------------------------ +# Create the EventBridge event rule that is triggered whenever a new IAM or SSO +# user is created or deleted, a user is added or removed from a group, or a +# group is created or deleted. Connect this rule to the SNS topic created +# above. +# ------------------------------------------------------------------------------ + +module "user_group_mod_event" { + providers = { + aws = aws + } + source = "github.com/cisagov/user-group-mod-alert-tf-module" + + target_arn = module.user_group_mod_sns.sns_topic.arn +} diff --git a/variables.tf b/variables.tf index 416ad14..2688f93 100644 --- a/variables.tf +++ b/variables.tf @@ -4,8 +4,13 @@ # You must provide a value for each of these parameters. # ------------------------------------------------------------------------------ -variable "subnet_id" { - description = "The ID of the AWS subnet to deploy into (e.g. subnet-0123456789abcdef0)." +variable "cool_lambda_artifacts_s3_bucket" { + description = "The name of the bucket where COOL Lambda deployment packages are to be stored." + type = string +} + +variable "disable_inactive_users_lambda_key" { + description = "The S3 key associated with the Lambda function deployment package to disable inactive IAM users." type = string } @@ -14,20 +19,69 @@ variable "subnet_id" { # # These parameters have reasonable defaults. # ------------------------------------------------------------------------------ -variable "ami_owner_account_id" { - default = "self" - description = "The ID of the AWS account that owns the Example AMI, or \"self\" if the AMI is owned by the same account as the provisioner." + +variable "aws_region" { + default = "us-east-1" + description = "The AWS region where the non-global resources for the Cyber Hygiene account are to be provisioned (e.g. \"us-east-1\")." type = string } -variable "aws_availability_zone" { - default = "a" - description = "The AWS availability zone to deploy into (e.g. a, b, c, etc.)." +variable "cyhy_lambda_artifacts_s3_bucket_prefix" { + default = "cool-cyhy-lambda-deployment-artifacts" + description = "The prefix of the name of the bucket in the Cyber Hygiene account where any Lambda deployment artifacts for a CyHy environment will be stored. A unique bucket name beginning with the specified prefix will be created." type = string } -variable "aws_region" { - default = "us-east-1" - description = "The AWS region to deploy into (e.g. us-east-1)." +variable "provisionaccount_role_description" { + default = "Allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account." + description = "The description to associate with the IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account." type = string } + +variable "provisionaccount_role_name" { + default = "ProvisionAccount" + description = "The name to assign the IAM role that allows sufficient permissions to provision all AWS resources in the Cyber Hygiene account." + type = string +} + +variable "provisionlambdabucket_policy_description" { + default = "Allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account." + description = "The description to associate with the IAM policy that allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account." + type = string +} + +variable "provisionlambdabucket_policy_name" { + default = "ProvisionLambdaArtifactsBucket" + description = "The name to assign the IAM policy that allows sufficient permissions to provision the Lambda deployment artifacts S3 bucket in the Cyber Hygiene account." + type = string +} + +variable "provisionssmsessionmanager_policy_description" { + default = "Allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account." + description = "The description to associate with the IAM policy that allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account." + type = string +} + +variable "provisionssmsessionmanager_policy_name" { + default = "ProvisionSSMSessionManager" + description = "The name to assign the IAM policy that allows sufficient permissions to provision the SSM Document resource and set up SSM session logging in the Cyber Hygiene account." + type = string +} + +variable "read_cool_lambda_bucket_policy_description" { + default = "Allows read-only access to the bucket in the Terraform account containing Lambda deployments." + description = "The description to associate with the IAM role that allows read-only access to the bucket in the Terraform account containing Lambda deployments." + type = string +} + +variable "read_cool_lambda_bucket_policy_name" { + default = "LambdaBucketReadOnly" + description = "The name to assign the IAM policy that allows read-only access to the bucket in the Terraform account containing Lambda deployments." + type = string +} + +variable "tags" { + default = {} + description = "Tags to apply to all AWS resources provisioned." + type = map(string) +}