From 273594a964d91a10438355f9f4198340ccdc6369 Mon Sep 17 00:00:00 2001 From: Alsia Plybeah Date: Wed, 24 May 2023 12:21:34 -0400 Subject: [PATCH] PRP-325 & PRP-324 Manage WAF and logging resources in Terraform (#103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ticket https://wicmtdp.atlassian.net/browse/PRP-325 https://wicmtdp.atlassian.net/browse/PRP-324 Changes added waf association resources enable load balancer logging in terraform state remove kinesis data firehose and related resources Context for reviewers Associated resources for the WAF have been manually added in the AWS console. The terraform state needs to reflect this In the console we enabled cloudwatch logs for the ALBs and the WAF in order to make debugging and querying data easier. The terraform configuration needs to reflect what’s in the console. --- infra/app/app-waf/main.tf | 18 ++-- infra/app/env-template/main.tf | 7 +- infra/modules/cognito/main.tf | 12 +++ infra/modules/cognito/variables.tf | 7 +- infra/modules/s3-encrypted/main.tf | 4 +- infra/modules/s3-encrypted/variables.tf | 5 - infra/modules/service/main.tf | 28 +++++- infra/modules/service/outputs.tf | 2 +- infra/modules/service/variables.tf | 5 + infra/modules/waf/main.tf | 117 ++++++++++++------------ infra/modules/waf/variables.tf | 5 + 11 files changed, 131 insertions(+), 79 deletions(-) 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" +}