diff --git a/infra/app/app-waf/main.tf b/infra/app/app-waf/main.tf index 7d8a4bb6..5b00d4bd 100644 --- a/infra/app/app-waf/main.tf +++ b/infra/app/app-waf/main.tf @@ -1,9 +1,10 @@ locals { - project_name = module.project_config.project_name - app_name = "wic-prp" - region = "us-west-2" - waf_name = "${local.project_name}-${local.app_name}-waf" - waf_iam_name = "${local.app_name}-waf-firehose-role" + project_name = module.project_config.project_name + app_name = "wic-prp" + region = "us-west-2" + waf_name = "${local.project_name}-${local.app_name}-waf" + waf_iam_name = "${local.app_name}-waf-firehose-role" + waf_logging_name = "aws-waf-logs-${local.project_name}" # Set project tags that will be used to tag all resources. tags = merge(module.project_config.default_tags, { @@ -47,7 +48,8 @@ module "project_config" { module "waf" { - source = "../../modules/waf" - waf_name = local.waf_name - waf_iam_name = local.waf_iam_name + source = "../../modules/waf" + waf_name = local.waf_name + waf_iam_name = local.waf_iam_name + waf_logging_name = local.waf_logging_name } diff --git a/infra/app/env-template/main.tf b/infra/app/env-template/main.tf index 1436cc1f..f8238317 100644 --- a/infra/app/env-template/main.tf +++ b/infra/app/env-template/main.tf @@ -29,6 +29,7 @@ locals { side_load_s3_name = "${local.project_name}-side-load-${var.environment_name}" contact_email = "wic-projects-team@navapbc.com" staff_idp_client_domain = "${var.environment_name}-idp.wic-services.org" + waf_name = "${local.project_name}-${local.project_name}-waf" } module "project_config" { @@ -66,6 +67,7 @@ module "participant" { service_name = local.participant_service_name image_repository_url = data.aws_ecr_repository.participant_image_repository.repository_url image_repository_arn = data.aws_ecr_repository.participant_image_repository.arn + waf_name = local.waf_name image_tag = var.participant_image_tag vpc_id = data.aws_vpc.default.id subnet_ids = data.aws_subnets.default.ids @@ -183,6 +185,7 @@ module "staff_idp" { client_logout_urls = ["https://${var.staff_url}/login"] client_domain = local.staff_idp_client_domain hosted_zone_domain = "wic-services.org" + waf_name = local.waf_name } module "staff_secret" { @@ -200,6 +203,7 @@ module "staff" { source = "../../modules/service" service_name = local.staff_service_name image_repository_url = data.aws_ecr_repository.staff_image_repository.repository_url + waf_name = local.waf_name image_repository_arn = data.aws_ecr_repository.staff_image_repository.arn image_tag = var.staff_image_tag vpc_id = data.aws_vpc.default.id @@ -267,6 +271,7 @@ module "analytics" { source = "../../modules/service" service_name = local.analytics_service_name image_repository_url = data.aws_ecr_repository.analytics_image_repository.repository_url + waf_name = local.waf_name image_repository_arn = data.aws_ecr_repository.analytics_image_repository.arn image_tag = var.analytics_image_tag vpc_id = data.aws_vpc.default.id @@ -321,7 +326,6 @@ module "analytics" { module "doc_upload" { source = "../../modules/s3-encrypted" - environment_name = var.environment_name s3_bucket_name = local.document_upload_s3_name read_role_names = [module.participant.task_role_name, module.staff.task_role_name] write_role_names = [module.participant.task_role_name] @@ -355,7 +359,6 @@ module "refresh_s3_presigned_urls" { module "side_load" { source = "../../modules/s3-encrypted" - environment_name = var.environment_name s3_bucket_name = local.side_load_s3_name read_role_names = [module.participant.task_role_name] admin_role_names = [module.participant.task_role_name] diff --git a/infra/modules/cognito/main.tf b/infra/modules/cognito/main.tf index 3624c269..57384004 100644 --- a/infra/modules/cognito/main.tf +++ b/infra/modules/cognito/main.tf @@ -146,3 +146,15 @@ resource "aws_ssm_parameter" "client_secret" { type = "SecureString" value = aws_cognito_user_pool_client.client.client_secret } + +############################################## +## WAF Association +############################################## +data "aws_wafv2_web_acl" "waf" { + name = var.waf_name + scope = "REGIONAL" +} +resource "aws_wafv2_web_acl_association" "cognito" { + resource_arn = aws_cognito_user_pool.pool.arn + web_acl_arn = data.aws_wafv2_web_acl.waf.arn +} diff --git a/infra/modules/cognito/variables.tf b/infra/modules/cognito/variables.tf index 1178dde0..ebef4555 100644 --- a/infra/modules/cognito/variables.tf +++ b/infra/modules/cognito/variables.tf @@ -105,4 +105,9 @@ variable "client_domain" { variable "hosted_zone_domain" { type = string description = "The aws_route53_zone domain" -} \ No newline at end of file +} + +variable "waf_name" { + type = string + description = "The name of the WAF associated with this resource " +} diff --git a/infra/modules/s3-encrypted/main.tf b/infra/modules/s3-encrypted/main.tf index dc38ff6f..2c7df157 100644 --- a/infra/modules/s3-encrypted/main.tf +++ b/infra/modules/s3-encrypted/main.tf @@ -91,7 +91,7 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "s3_encrypted" { } resource "aws_kms_key" "s3_encrypted" { - description = "KMS key for ${var.environment_name} Document Upload" + description = "KMS key for S3 buckets" # The waiting period, specified in number of days. After receiving a deletion request, AWS KMS will delete the KMS key after the waiting period ends. During the waiting period, the KMS key status and key state is Pending deletion. See https://docs.aws.amazon.com/kms/latest/developerguide/deleting-keys.html#deleting-keys-how-it-works deletion_window_in_days = "10" # Generates new cryptographic material every 365 days, this is used to encrypt your data. The KMS key retains the old material for decryption purposes. @@ -116,7 +116,7 @@ resource "aws_s3_bucket_logging" "s3_encrypted_log" { bucket = aws_s3_bucket.s3_encrypted.id # Checkov recommends using an s3 bucket to store logging for other s3 buckets. The bucket created on #L61 is the target bucket target_bucket = aws_s3_bucket.s3_encrypted_log.bucket - target_prefix = var.environment_name + target_prefix = aws_s3_bucket.s3_encrypted.bucket } resource "aws_s3_bucket_versioning" "s3_encrypted_log" { diff --git a/infra/modules/s3-encrypted/variables.tf b/infra/modules/s3-encrypted/variables.tf index 2bb419ef..5214294f 100644 --- a/infra/modules/s3-encrypted/variables.tf +++ b/infra/modules/s3-encrypted/variables.tf @@ -1,8 +1,3 @@ -variable "environment_name" { - type = string - description = "name of the application environment" -} - variable "write_role_names" { type = list(string) description = "role names that have access to write s3 permissions" diff --git a/infra/modules/service/main.tf b/infra/modules/service/main.tf index cb57010a..b39a2d91 100644 --- a/infra/modules/service/main.tf +++ b/infra/modules/service/main.tf @@ -46,8 +46,11 @@ resource "aws_lb" "alb" { # https://docs.bridgecrew.io/docs/ensure-that-alb-drops-http-headers drop_invalid_header_fields = true - # TODO(https://github.com/navapbc/template-infra/issues/162) Add access logs - # checkov:skip=CKV_AWS_91:Add access logs in future PR + access_logs { + enabled = true + prefix = var.service_name + bucket = var.service_name + } } # NOTE: for the demo we expose private http endpoint @@ -286,6 +289,14 @@ resource "aws_cloudwatch_log_group" "service_logs" { # TODO(https://github.com/navapbc/template-infra/issues/164) Encrypt with customer managed KMS key # checkov:skip=CKV_AWS_158:Encrypt service logs with customer key in future work } +#################### +## Logging Bucket ## +#################### + +module "alb_logging" { + source = "../s3-encrypted" + s3_bucket_name = var.service_name +} #################### ## Access Control ## @@ -517,3 +528,16 @@ resource "aws_security_group" "app" { cidr_blocks = ["0.0.0.0/0"] } } + +############################################## +## WAF Association +############################################## +data "aws_wafv2_web_acl" "waf" { + name = var.waf_name + scope = "REGIONAL" +} + +resource "aws_wafv2_web_acl_association" "alb" { + resource_arn = aws_lb.alb.arn # load balancer arn + web_acl_arn = data.aws_wafv2_web_acl.waf.arn +} diff --git a/infra/modules/service/outputs.tf b/infra/modules/service/outputs.tf index db74785d..dba327be 100644 --- a/infra/modules/service/outputs.tf +++ b/infra/modules/service/outputs.tf @@ -4,4 +4,4 @@ output "task_role_name" { output "app_security_group" { value = aws_security_group.app -} \ No newline at end of file +} diff --git a/infra/modules/service/variables.tf b/infra/modules/service/variables.tf index cbb2596f..3fc59e72 100644 --- a/infra/modules/service/variables.tf +++ b/infra/modules/service/variables.tf @@ -151,3 +151,8 @@ variable "task_role_max_session_duration" { description = "The maximum session duration for the ECS task role (in seconds)" default = 60 * 60 # 1 hour } + +variable "waf_name" { + type = string + description = "The name of the WAF associated with this resource " +} diff --git a/infra/modules/waf/main.tf b/infra/modules/waf/main.tf index 257d20ba..30991289 100644 --- a/infra/modules/waf/main.tf +++ b/infra/modules/waf/main.tf @@ -10,7 +10,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { name = "AWSGeneralRules" - priority = 1 + priority = 0 override_action { count {} @@ -32,7 +32,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { name = "AWSManageKnownBadInputs" - priority = 2 + priority = 1 # setting to none re this solution here: https://github.com/bridgecrewio/checkov/issues/2101 # count rule override: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-rule-group-override-options.html#web-acl-rule-group-override-options-rule-group override_action { @@ -55,7 +55,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Inspect IPs that have been identified as bots by Amazon name = "AWSIPReputationList" - priority = 3 + priority = 2 override_action { count {} } @@ -76,7 +76,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Inspects IPs for services known to anonymize client information e.g. proxies name = "AWSAnonList" - priority = 4 + priority = 3 override_action { # does this need an override? count {} } @@ -97,7 +97,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Blocks requests associated with SQL database exploitation name = "AWSSQLManagement" - priority = 5 + priority = 4 override_action { count {} } @@ -118,7 +118,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Blocks requests associated with Linux exploitation name = "AWSLinuxManagement" - priority = 6 + priority = 5 override_action { count {} } @@ -138,7 +138,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Blocks requests associated with POSIX and POSIX-like OS exploitation name = "AWSUnixManagement" - priority = 7 + priority = 6 override_action { count {} } @@ -158,7 +158,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Applies a rate based rule to IPs originating in the US name = "AWSRateBasedRuleDomesticDOS" - priority = 8 + priority = 7 action { block {} @@ -186,7 +186,7 @@ resource "aws_wafv2_web_acl" "waf" { rule { # Applies a rate based rule to IPs originating outside of the US name = "AWSRateBasedRuleGlobalDOS" - priority = 9 + priority = 8 action { block {} @@ -215,6 +215,48 @@ resource "aws_wafv2_web_acl" "waf" { sampled_requests_enabled = false } } + rule { + name = "BlockSuspiciousPOST" + priority = 9 + action { + block {} + } + statement { + and_statement { + statement { + byte_match_statement { + search_string = "POST" + positional_constraint = "EXACTLY" + field_to_match { + method {} + } + text_transformation { + priority = 0 + type = "NONE" + } + } + } + statement { + byte_match_statement { + search_string = "/" + positional_constraint = "EXACTLY" + field_to_match { + uri_path {} + } + text_transformation { + priority = 0 + type = "NONE" + } + } + } + } + } + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "BlockSuspiciousPOST" + sampled_requests_enabled = true + } + } visibility_config { cloudwatch_metrics_enabled = true metric_name = "waf-general-metrics" @@ -224,63 +266,22 @@ resource "aws_wafv2_web_acl" "waf" { # logging configuration resource resource "aws_wafv2_web_acl_logging_configuration" "waf_logging" { - log_destination_configs = [aws_kinesis_firehose_delivery_stream.waf_logging.arn] + log_destination_configs = [aws_cloudwatch_log_group.waf.arn] resource_arn = aws_wafv2_web_acl.waf.arn } -# firehose to recieve logs -resource "aws_kinesis_firehose_delivery_stream" "waf_logging" { - name = "aws-waf-logs-metrics-stream" - destination = "extended_s3" - server_side_encryption { - enabled = true - key_type = "CUSTOMER_MANAGED_CMK" - key_arn = module.s3_encrypted_bucket.bucket_kms_arn - } - extended_s3_configuration { - role_arn = aws_iam_role.firehose_perms.arn - bucket_arn = module.s3_encrypted_bucket.encrypted_bucket_arn - } -} -# IAM Role for Kinesis -resource "aws_iam_role" "firehose_perms" { - name = var.waf_iam_name - description = "IAM role for the KDF" - assume_role_policy = data.aws_iam_policy_document.firehose_assume_role.json -} +resource "aws_cloudwatch_log_group" "waf" { + name = var.waf_logging_name # make this a variable + retention_in_days = 30 -# assume role -data "aws_iam_policy_document" "firehose_assume_role" { - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - principals { - type = "Service" - identifiers = ["firehose.amazonaws.com"] - } - } -} - -# role policy -data "aws_iam_policy_document" "firehose_perms" { - statement { - sid = "AccessKDF" - effect = "Allow" - actions = [ - "kinesis:Get*", - "kinesis:PutRecord", - "s3:GetBucket", - "s3:PutObject" - ] - resources = [module.s3_encrypted_bucket.encrypted_bucket_arn, "${module.s3_encrypted_bucket.encrypted_bucket_arn}/*"] - } + # Checkov throws alerts in the event of default encryption for Cloudwatch,which is server-side encrytion for data at rest. + # checkov:skip=CKV_AWS_158:Disabling this becuase if the key is deleted or otherwise unassociated, the cloudwatch logs will be inaccessible. } # s3 logging bucket; this is a refactor to DRY up the code module "s3_encrypted_bucket" { - source = "../s3-encrypted" - s3_bucket_name = "wic-prp-waf" - environment_name = "waf" + source = "../s3-encrypted" + s3_bucket_name = "wic-prp-waf" } resource "aws_s3_bucket_lifecycle_configuration" "waf_logging" { diff --git a/infra/modules/waf/variables.tf b/infra/modules/waf/variables.tf index 9010519d..90297c13 100644 --- a/infra/modules/waf/variables.tf +++ b/infra/modules/waf/variables.tf @@ -7,3 +7,8 @@ variable "waf_iam_name" { type = string description = "Name of the IAM role associated with the firewall" } + +variable "waf_logging_name" { + type = string + description = "Name of the logging group associated with the firewall" +}