diff --git a/infrastructure/blocks/auth/main.tf b/infrastructure/blocks/auth/main.tf index f417fa48..42cfd92a 100644 --- a/infrastructure/blocks/auth/main.tf +++ b/infrastructure/blocks/auth/main.tf @@ -5,9 +5,9 @@ terraform { } module "auth" { - source = "../../modules/auth" - tags = var.tags - domain_name = var.domain_name - resource-name-prefix = var.resource-name-prefix - layers = var.layers + source = "../../modules/auth" + tags = var.tags + domain_name = var.domain_name + resource-name-prefix = var.resource-name-prefix + layers = var.layers } diff --git a/infrastructure/blocks/s3/main.tf b/infrastructure/blocks/s3/main.tf index 2ab51ad6..8c6a340d 100644 --- a/infrastructure/blocks/s3/main.tf +++ b/infrastructure/blocks/s3/main.tf @@ -19,7 +19,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "rapid_data_storag rule { apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" + sse_algorithm = "aws:kms" } } } @@ -84,7 +84,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { rule { apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" + sse_algorithm = "aws:kms" } } } diff --git a/infrastructure/modules/app-cluster/cloudtrail.tf b/infrastructure/modules/app-cluster/cloudtrail.tf index 878dd5ec..6f9adf8e 100644 --- a/infrastructure/modules/app-cluster/cloudtrail.tf +++ b/infrastructure/modules/app-cluster/cloudtrail.tf @@ -19,10 +19,13 @@ data "aws_iam_policy_document" "access_logs_key_policy" { "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*", + "kms:ScheduleKeyDeletion", + "kms:EnableKeyRotation", + "kms:TagResource" ] resources = [ - "arn:aws:kms:${data.aws_region.region.name}:${data.aws_caller_identity.current.account_id}:key/*", + "arn:aws:kms:${data.aws_region.region.region}:${data.aws_caller_identity.current.account_id}:key/*", ] } @@ -33,7 +36,7 @@ data "aws_iam_policy_document" "access_logs_key_policy" { principals { type = "Service" - identifiers = ["logs.${data.aws_region.region.name}.amazonaws.com"] + identifiers = ["logs.${data.aws_region.region.region}.amazonaws.com"] } actions = [ @@ -45,14 +48,14 @@ data "aws_iam_policy_document" "access_logs_key_policy" { ] resources = [ - "arn:aws:kms:${data.aws_region.region.name}:${data.aws_caller_identity.current.account_id}:key/*", + "arn:aws:kms:${data.aws_region.region.region}:${data.aws_caller_identity.current.account_id}:key/*", ] condition { test = "ArnEquals" variable = "kms:EncryptionContext:aws:logs:arn" values = [ - "arn:aws:logs:${data.aws_region.region.name}:${data.aws_caller_identity.current.account_id}:log-group:${var.resource-name-prefix}_access_logs" + "arn:aws:logs:${data.aws_region.region.region}:${data.aws_caller_identity.current.account_id}:log-group:${var.resource-name-prefix}_access_logs" ] } } @@ -76,7 +79,7 @@ data "aws_iam_policy_document" "access_logs_key_policy" { ] resources = [ - "arn:aws:kms:${data.aws_region.region.name}:${data.aws_caller_identity.current.account_id}:key/*", + "arn:aws:kms:${data.aws_region.region.region}:${data.aws_caller_identity.current.account_id}:key/*", ] } } diff --git a/infrastructure/modules/app-cluster/db.tf b/infrastructure/modules/app-cluster/db.tf index 28339d1e..0d40feeb 100644 --- a/infrastructure/modules/app-cluster/db.tf +++ b/infrastructure/modules/app-cluster/db.tf @@ -3,9 +3,9 @@ data "aws_caller_identity" "current" {} resource "aws_dynamodb_table" "service_table" { # checkov:skip=CKV_AWS_119:No need for customer managed keys - name = "${var.resource-name-prefix}_service_table" - hash_key = "PK" - range_key = "SK" + name = "${var.resource-name-prefix}_service_table" + hash_key = "PK" + range_key = "SK" billing_mode = "PAY_PER_REQUEST" deletion_protection_enabled = true diff --git a/infrastructure/modules/app-cluster/load_balancer.tf b/infrastructure/modules/app-cluster/load_balancer.tf index 8b220637..747c40c8 100644 --- a/infrastructure/modules/app-cluster/load_balancer.tf +++ b/infrastructure/modules/app-cluster/load_balancer.tf @@ -130,7 +130,6 @@ resource "aws_security_group_rule" "load_balancer_security_group_rule_egress_htt } resource "aws_lb_target_group" "target_group" { - name = "${var.resource-name-prefix}-tg" port = 80 protocol = "HTTP" target_type = "ip" diff --git a/infrastructure/modules/app-cluster/main.tf b/infrastructure/modules/app-cluster/main.tf index 6940c209..c4283822 100644 --- a/infrastructure/modules/app-cluster/main.tf +++ b/infrastructure/modules/app-cluster/main.tf @@ -409,4 +409,8 @@ resource "aws_security_group" "service_security_group" { } tags = var.tags + + lifecycle { + create_before_destroy = true + } } diff --git a/infrastructure/modules/app-cluster/providers.tf b/infrastructure/modules/app-cluster/providers.tf new file mode 100644 index 00000000..ed160fde --- /dev/null +++ b/infrastructure/modules/app-cluster/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 2.7.0" + } + } +} diff --git a/infrastructure/modules/app-cluster/routing.tf b/infrastructure/modules/app-cluster/routing.tf index 65bc5248..3b59a45a 100644 --- a/infrastructure/modules/app-cluster/routing.tf +++ b/infrastructure/modules/app-cluster/routing.tf @@ -6,6 +6,16 @@ resource "aws_route53_zone" "primary-hosted-zone" { tags = var.tags } +resource "aws_route53_record" "copy-ns-records" { + count = var.parent_hosted_zone_id == "" ? 0 : 1 + + zone_id = var.parent_hosted_zone_id + name = aws_route53_zone.primary-hosted-zone[0].name + type = "NS" + ttl = "30" + records = aws_route53_zone.primary-hosted-zone[0].name_servers +} + # Create the certificate resource "aws_acm_certificate" "rapid-certificate" { count = var.certificate_validation_arn == "" ? 1 : 0 diff --git a/infrastructure/modules/app-cluster/variables.tf b/infrastructure/modules/app-cluster/variables.tf index 9232c48f..5b739d3e 100644 --- a/infrastructure/modules/app-cluster/variables.tf +++ b/infrastructure/modules/app-cluster/variables.tf @@ -102,6 +102,12 @@ variable "hosted_zone_id" { description = "Hosted Zone ID with the domain Name Servers, pass quotes to create a new one from scratch" } +variable "parent_hosted_zone_id" { + type = string + description = "Parent hosted Zone ID, if domain you use is aws subdomain. This will copy NS records to parent hosted zone" + default = "" +} + variable "certificate_validation_arn" { type = string description = "Arn of the certificate used by the domain" diff --git a/infrastructure/modules/auth/db.tf b/infrastructure/modules/auth/db.tf index c1e8cb8e..e0ecfcba 100644 --- a/infrastructure/modules/auth/db.tf +++ b/infrastructure/modules/auth/db.tf @@ -3,9 +3,9 @@ data "aws_caller_identity" "current" {} resource "aws_dynamodb_table" "permissions_table" { # checkov:skip=CKV_AWS_119:No need for customer managed keys - name = "${var.resource-name-prefix}_${var.permissions_table_name}" - hash_key = "PK" - range_key = "SK" + name = "${var.resource-name-prefix}_${var.permissions_table_name}" + hash_key = "PK" + range_key = "SK" billing_mode = "PAY_PER_REQUEST" deletion_protection_enabled = true diff --git a/infrastructure/modules/auth/main.tf b/infrastructure/modules/auth/main.tf index b95ecfd7..e9ae7ff3 100644 --- a/infrastructure/modules/auth/main.tf +++ b/infrastructure/modules/auth/main.tf @@ -6,6 +6,12 @@ resource "aws_cognito_user_pool" "rapid_user_pool" { admin_create_user_config { allow_admin_create_user_only = true + + invite_message_template { + email_message = var.invite_message_email_message + email_subject = var.invite_message_email_subject + sms_message = "Your username is {username} and temporary password is {####}." + } } mfa_configuration = "OPTIONAL" @@ -29,14 +35,17 @@ resource "aws_cognito_user_pool" "rapid_user_pool" { advanced_security_mode = "ENFORCED" } - dynamic "email_configuration" { - for_each = var.ses_domain_identity_arn != null ? [1] : [] - content { - email_sending_account = "DEVELOPER" - from_email_address = "no-reply@${coalesce(var.ses_email_domain, var.domain_name)}" - source_arn = var.ses_domain_identity_arn - } + email_configuration { + email_sending_account = var.ses_domain_identity_arn != null ? "DEVELOPER" : "COGNITO_DEFAULT" + from_email_address = ( + var.ses_domain_identity_arn != null ? + "\"${var.sender_display_name}\" " : + null + ) + source_arn = var.ses_domain_identity_arn + reply_to_email_address = var.reply_to_email_address } + } resource "aws_cognito_resource_server" "rapid_resource_server" { diff --git a/infrastructure/modules/auth/providers.tf b/infrastructure/modules/auth/providers.tf new file mode 100644 index 00000000..ed160fde --- /dev/null +++ b/infrastructure/modules/auth/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 2.7.0" + } + } +} diff --git a/infrastructure/modules/auth/users.tf b/infrastructure/modules/auth/users.tf index b9698877..50160682 100644 --- a/infrastructure/modules/auth/users.tf +++ b/infrastructure/modules/auth/users.tf @@ -158,6 +158,10 @@ resource "aws_secretsmanager_secret_version" "e2e_test_client_write_all_secrets_ # UI_TEST_USER resource "random_password" "password" { length = 16 + min_lower = 1 + min_numeric = 1 + min_special = 1 + min_upper = 1 special = true override_special = "!#$%&*()-_=+[]{}<>:?" } diff --git a/infrastructure/modules/auth/variables.tf b/infrastructure/modules/auth/variables.tf index 46801ed5..ba0ddc81 100644 --- a/infrastructure/modules/auth/variables.tf +++ b/infrastructure/modules/auth/variables.tf @@ -119,3 +119,31 @@ variable "ses_email_domain" { description = "The domain to use for the SES email 'from' address (e.g., no-reply@). Defaults to domain_name if not specified." default = null } + +variable "invite_message_email_message" { + description = "Invite message email message" + type = string + validation { + condition = strcontains(var.invite_message_email_message, "{username}") && strcontains(var.invite_message_email_message, "{####}") + error_message = "Email message template. Must contain {username} and {####} placeholders, for username and temporary password, respectively." + } + default = "Your username is {username} and temporary password is {####}." +} + +variable "invite_message_email_subject" { + description = "Invite message email subject" + type = string + default = "Your temporary password" +} + +variable "sender_display_name" { + description = "Email sender display name. Applicable only if ses_domain_identity_arn is configured." + type = string + default = "" +} + +variable "reply_to_email_address" { + description = "Email REPLY-TO email address" + type = string + default = "" +} \ No newline at end of file diff --git a/infrastructure/modules/data-workflow/dynamodb.tf b/infrastructure/modules/data-workflow/dynamodb.tf index 0d818888..ddcaee78 100644 --- a/infrastructure/modules/data-workflow/dynamodb.tf +++ b/infrastructure/modules/data-workflow/dynamodb.tf @@ -1,8 +1,8 @@ resource "aws_dynamodb_table" "schema_table" { # checkov:skip=CKV_AWS_119:No need for customer managed keys - name = "${var.resource-name-prefix}_schema_table" - hash_key = "PK" - range_key = "SK" + name = "${var.resource-name-prefix}_schema_table" + hash_key = "PK" + range_key = "SK" billing_mode = "PAY_PER_REQUEST" deletion_protection_enabled = true diff --git a/infrastructure/modules/data-workflow/providers.tf b/infrastructure/modules/data-workflow/providers.tf new file mode 100644 index 00000000..ed160fde --- /dev/null +++ b/infrastructure/modules/data-workflow/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 2.7.0" + } + } +} diff --git a/infrastructure/modules/rapid/main.tf b/infrastructure/modules/rapid/main.tf index bcc7ff00..2599ea85 100644 --- a/infrastructure/modules/rapid/main.tf +++ b/infrastructure/modules/rapid/main.tf @@ -1,4 +1,7 @@ module "app_cluster" { + providers = { + aws = aws.default + } source = "../app-cluster" app-replica-count-desired = var.app-replica-count-desired app-replica-count-max = var.app-replica-count-max @@ -16,6 +19,7 @@ module "app_cluster" { rapid_ecr_url = var.rapid_ecr_url certificate_validation_arn = var.certificate_validation_arn hosted_zone_id = var.hosted_zone_id + parent_hosted_zone_id = var.parent_hosted_zone_id aws_account = var.aws_account aws_region = var.aws_region data_s3_bucket_arn = aws_s3_bucket.this.arn @@ -37,17 +41,27 @@ module "app_cluster" { } module "auth" { - source = "../auth" - tags = var.tags - domain_name = var.domain_name - resource-name-prefix = var.resource-name-prefix - password_policy = var.password_policy - layers = var.layers - ses_domain_identity_arn = var.ses_domain_identity_arn - ses_email_domain = var.ses_email_domain + providers = { + aws = aws.default + } + source = "../auth" + tags = var.tags + domain_name = var.domain_name + resource-name-prefix = var.resource-name-prefix + password_policy = var.password_policy + layers = var.layers + ses_domain_identity_arn = var.ses_domain_identity_arn + ses_email_domain = var.ses_email_domain + invite_message_email_subject = var.invite_message_email_subject + invite_message_email_message = var.invite_message_email_message + sender_display_name = var.sender_display_name + reply_to_email_address = var.reply_to_email_address } module "data_workflow" { + providers = { + aws = aws.default + } source = "../data-workflow" resource-name-prefix = var.resource-name-prefix aws_account = var.aws_account @@ -55,6 +69,10 @@ module "data_workflow" { } module "ui" { + providers = { + aws.default = aws.default + aws.us_east = aws.us_east + } source = "../ui" tags = var.tags log_bucket_name = aws_s3_bucket.logs.id @@ -69,6 +87,7 @@ module "ui" { route_53_validation_record_fqdns = module.app_cluster.route_53_validation_record_fqdns geo_restriction_locations = var.geo_restriction_locations sql_injection_protection = var.sql_injection_protection + aws_role_arn_to_assume = var.aws_role_arn_to_assume } resource "aws_s3_bucket" "this" { @@ -77,6 +96,8 @@ resource "aws_s3_bucket" "this" { #checkov:skip=CKV2_AWS_62:No need for event notifications #checkov:skip=CKV2_AWS_61:No need for lifecycle configuration + provider = aws.default + bucket = var.resource-name-prefix force_destroy = false @@ -85,6 +106,8 @@ resource "aws_s3_bucket" "this" { } resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + provider = aws.default + bucket = aws_s3_bucket.this.id rule { @@ -96,6 +119,8 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "this" { } resource "aws_s3_bucket_versioning" "this" { + provider = aws.default + bucket = aws_s3_bucket.this.id versioning_configuration { status = "Enabled" @@ -103,6 +128,8 @@ resource "aws_s3_bucket_versioning" "this" { } resource "aws_s3_bucket_logging" "this" { + provider = aws.default + bucket = aws_s3_bucket.this.id target_bucket = aws_s3_bucket.logs.bucket @@ -110,11 +137,15 @@ resource "aws_s3_bucket_logging" "this" { } resource "aws_s3_bucket_notification" "this" { + provider = aws.default + bucket = aws_s3_bucket.this.id eventbridge = true } resource "aws_s3_bucket_public_access_block" "this" { + provider = aws.default + bucket = aws_s3_bucket.this.id ignore_public_acls = true block_public_acls = true @@ -129,6 +160,8 @@ resource "aws_s3_bucket" "logs" { #checkov:skip=CKV_AWS_21:No need to version log bucket #checkov:skip=CKV2_AWS_62:No need for event notifications #checkov:skip=CKV2_AWS_61:No need for lifecycle configuration + provider = aws.default + bucket = "${var.resource-name-prefix}-logs" force_destroy = false @@ -136,11 +169,15 @@ resource "aws_s3_bucket" "logs" { } resource "aws_s3_bucket_acl" "logs" { + provider = aws.default + bucket = aws_s3_bucket.logs.id acl = "private" } resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { + provider = aws.default + bucket = aws_s3_bucket.logs.id rule { @@ -153,6 +190,8 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { # Resource to avoid error "AccessControlListNotSupported: The bucket does not allow ACLs" resource "aws_s3_bucket_ownership_controls" "logs" { + provider = aws.default + bucket = aws_s3_bucket.logs.id rule { object_ownership = "BucketOwnerPreferred" @@ -160,6 +199,8 @@ resource "aws_s3_bucket_ownership_controls" "logs" { } resource "aws_s3_bucket_public_access_block" "logs" { + provider = aws.default + bucket = aws_s3_bucket.logs.id ignore_public_acls = true block_public_acls = true @@ -169,6 +210,8 @@ resource "aws_s3_bucket_public_access_block" "logs" { } resource "aws_s3_bucket_policy" "log_bucket_policy" { + provider = aws.default + bucket = aws_s3_bucket.this.id policy = <