diff --git a/main.tf b/main.tf index 41301e9..78e0f20 100644 --- a/main.tf +++ b/main.tf @@ -105,6 +105,7 @@ locals { ### # the cdn that serves videos from an s3 bucket, e.g. static.mentorpal.org ### + module "cdn_static" { source = "git::https://github.com/cloudposse/terraform-aws-cloudfront-s3-cdn?ref=tags/0.74.0" namespace = "static-${var.eb_env_namespace}" @@ -115,6 +116,9 @@ module "cdn_static" { dns_alias_enabled = true parent_zone_name = var.aws_route53_zone_name acm_certificate_arn = data.aws_acm_certificate.cdn.arn + # bugfix: required for video playback after upload + forward_query_string = true + query_string_cache_keys = ["v"] } # export s3 arn so serverless can pick it up to configure iam policies @@ -317,6 +321,27 @@ resource "aws_lb_listener_rule" "redirect_http_to_https" { } } +##### +# Firewall +# +##### + + +module "firewall" { + source = "./modules/waf" + aws_region = var.aws_region + environment = var.eb_env_stage + top_level_domain = var.site_domain_name + rate_limit = 100 + tags = var.eb_env_tags +} + +resource "aws_wafv2_web_acl_association" "load_blancer_firewall" { + resource_arn = module.elastic_beanstalk_environment.load_balancers[0] + web_acl_arn = module.firewall.wafv2_webacl_arn +} + + ###### # Cloudwatch alarms # - https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-cloudwatch-metrics.html diff --git a/modules/waf/main.tf b/modules/waf/main.tf new file mode 100644 index 0000000..3df3c1a --- /dev/null +++ b/modules/waf/main.tf @@ -0,0 +1,157 @@ +resource "aws_wafv2_web_acl" "wafv2_webacl" { + name = "mentorpal-${var.environment}-wafv2-webacl" + scope = "REGIONAL" + tags = var.tags + + default_action { + allow {} + } + + rule { + name = "ip-rate-limit-rule" + priority = 2 + + action { + block {} + } + + statement { + rate_based_statement { + aggregate_key_type = "IP" + limit = var.rate_limit + } + } + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "${var.rate_limit}-ip-rate-limit-rule" + sampled_requests_enabled = false + } + } + + rule { + name = "bot-control" + priority = 3 + + override_action { + # in order to test, lets just collect stats before enabling rules on prod: + count {} + # none {} + } + statement { + managed_rule_group_statement { + # see https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html#aws-managed-rule-groups-bot + name = "AWSManagedRulesBotControlRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "AWS-AWSBotControl-rule" + sampled_requests_enabled = true + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "mentorpal-${var.environment}-wafv2-webacl" + sampled_requests_enabled = true + } +} + +resource "aws_ssm_parameter" "origin_acl_arn" { + name = "/mentorpal/${var.environment}/firewall/WEBACL_ARN" + type = "String" + value = aws_wafv2_web_acl.wafv2_webacl.arn + + tags = var.tags +} + +resource "aws_s3_bucket" "s3_logs" { + bucket = "mentorpal-aws-waf-logs-${var.aws_region}-${var.environment}" + acl = "private" + tags = var.tags +} + +data "aws_iam_policy_document" "policy_assume_kinesis" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["firehose.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "firehose_role" { + name = "mentorpal-firehose-aws-waf-logs-${var.aws_region}-${var.environment}" + assume_role_policy = data.aws_iam_policy_document.policy_assume_kinesis.json + tags = var.tags +} + +# https://docs.aws.amazon.com/firehose/latest/dev/controlling-access.html#using-iam-s3 +data "aws_iam_policy_document" "s3_policy_document" { + statement { + sid = "1" + actions = [ + "s3:GetBucketLocation", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + ] + + resources = [ + aws_s3_bucket.s3_logs.arn, + ] + } + + statement { + sid = "2" + actions = [ + "s3:AbortMultipartUpload", + "s3:GetObject", + "s3:PutObject", + ] + + resources = [ + "${aws_s3_bucket.s3_logs.arn}/*", + ] + } +} + +resource "aws_iam_policy" "s3_policy" { + name = "mentorpal-kinesis-s3-write-policy-${var.environment}" + policy = data.aws_iam_policy_document.s3_policy_document.json +} + +resource "aws_iam_role_policy_attachment" "firehose_s3_policy_attachment" { + role = aws_iam_role.firehose_role.name + policy_arn = aws_iam_policy.s3_policy.arn +} + +resource "aws_kinesis_firehose_delivery_stream" "waf_logs_kinesis_stream" { + # the name must begin with aws-waf-logs- + name = "aws-waf-logs-kinesis-stream-mentorpal-${var.environment}" + destination = "s3" + s3_configuration { + role_arn = aws_iam_role.firehose_role.arn + bucket_arn = aws_s3_bucket.s3_logs.arn + compression_format = "GZIP" + } + tags = var.tags +} + +resource "aws_wafv2_web_acl_logging_configuration" "waf_logging_conf_staging" { + log_destination_configs = [aws_kinesis_firehose_delivery_stream.waf_logs_kinesis_stream.arn] + resource_arn = aws_wafv2_web_acl.wafv2_webacl.arn + redacted_fields { + single_header { + name = "authorization" + } + } +} + +output "wafv2_webacl_arn" { + value = aws_wafv2_web_acl.wafv2_webacl.arn +} diff --git a/modules/waf/vars.tf b/modules/waf/vars.tf new file mode 100644 index 0000000..206f924 --- /dev/null +++ b/modules/waf/vars.tf @@ -0,0 +1,21 @@ +variable "aws_region" { + type = string + description = "AWS region" +} + +variable "environment" { + type = string +} +variable "top_level_domain" { + type = string + default = "mentorpal.org" +} + +variable "tags" { + type = map(string) +} + +variable "rate_limit" { + type = number + default = 100 # minimum +} diff --git a/modules/waf/versions.tf b/modules/waf/versions.tf new file mode 100644 index 0000000..3b292ca --- /dev/null +++ b/modules/waf/versions.tf @@ -0,0 +1,6 @@ +terraform { + required_version = ">= 0.15.0" + required_providers { + aws = ">= 3.1" + } +} diff --git a/versions.tf b/versions.tf index 342d1e2..18f91b3 100644 --- a/versions.tf +++ b/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 0.14.0" + required_version = ">= 0.15.0" required_providers { aws = "~> 3.0"