Shielding Your Apps in the Cloud: Integrating CloudFront and AWS WAF with Terraform

lpossamai

Lucas Possamai

Posted on January 24, 2024

Shielding Your Apps in the Cloud: Integrating CloudFront and AWS WAF with Terraform

Introduction

In the evolving landscape of cloud computing, securing your applications is as crucial as ever. With AWS's vast array of services, it's important to leverage the right tools to protect your infrastructure effectively. In this blog post, we'll dive into how you can enhance your AWS security posture by integrating CloudFront and AWS WAF with Terraform, ensuring your applications are fortified against threats.

Understanding the Components

  • Terragrunt: An extension of Terraform, Terragrunt assists in managing complex infrastructure with less duplication and more efficiency. Its power lies in its ability to manage dependencies and its dry configuration approach.
  • CloudFront: AWS CloudFront is a content delivery network (CDN) service that speeds up the distribution of your static and dynamic web content. Beyond performance, it offers essential security features to protect your applications.
  • AWS WAF: The AWS Web Application Firewall (WAF) helps protect your web applications from common web exploits that could affect application availability, compromise security, or consume excessive resources.
  • Integrating for Security: We'll explore how these components work in unison, providing a robust defense mechanism for your applications.

Integrating Terragrunt with AWS Services

In recent blog post, we discussed how we can connect Terragrunt/Terraform with AWS.

Assuming you have that in place (or similar), I'll walk you through the steps required to deploy the services mentioned in this post as well as some additional monitoring solutions such as CloudWatch Alerts with notifications sent to Slack.

Setting Up Security Measures

Configuring AWS WAF:

A deep dive into setting up rules in AWS WAF to protect your applications from common web attacks and vulnerabilities.

Using the Cloudposse Terraform module, I have the following configuration:

inputs = {
  name  = "web-acl-cf"
  scope = "CLOUDFRONT"

  default_action = "allow"

  visibility_config = {
    cloudwatch_metrics_enabled = true
    metric_name                = "cloudfront-alb-waf"
    sampled_requests_enabled   = true
  }

  ###################
  # Logging
  ###################
  log_destination_configs = [dependency.s3.outputs.s3_bucket_arn]

  ###################
  # WAF Rules
  ###################
  managed_rule_group_statement_rules = [
    {
      name            = "AWSManagedRulesKnownBadInputsRuleSet"
      override_action = "count"
      priority        = 1

      statement = {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontAWSManagedRulesKnownBadInputsRuleSet"
      }
    },
    {
      name            = "AWSManagedRulesAmazonIpReputationList"
      override_action = "count"
      priority        = 2

      statement = {
        name        = "AWSManagedRulesAmazonIpReputationList"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontAWSManagedRulesAmazonIpReputationList"
      }
    },
    {
      name            = "AWSManagedRulesBotControlRuleSet"
      override_action = "count"
      priority        = 3

      statement = {
        name        = "AWSManagedRulesBotControlRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontAWSManagedRulesBotControlRuleSet"
      }
    },
    {
      name            = "AWSManagedRulesCommonRuleSet"
      override_action = "count"
      priority        = 4

      statement = {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontAWSManagedRulesCommonRuleSet"
      }
    },
    {
      name            = "AWSManagedRulesPHPRuleSet"
      override_action = "count"
      priority        = 5

      statement = {
        name        = "AWSManagedRulesPHPRuleSet"
        vendor_name = "AWS"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontAWSManagedRulesPHPRuleSet"
      }
    }
  ]

  rate_based_statement_rules = [
    {
      name     = "RateBasedRule"
      action   = "block"
      priority = 6

      statement = {
        limit              = local.environment_vars.locals.waf_rate_limit
        aggregate_key_type = "IP"
      }

      visibility_config = {
        cloudwatch_metrics_enabled = true
        sampled_requests_enabled   = true
        metric_name                = "CloudfrontRateBasedRule"
      }
    }
  ]

  tags = local.environment_vars.locals.tags
}
Enter fullscreen mode Exit fullscreen mode

Note that the only rule that is blocking at the moment is the RateBasedRule rule.

The AWS Managed rules specified above have been taken from this official AWS documentation.

To view the AWS WAF Dashboard, make sure to select the CloudFront "region":

Image description

You'll be able to see the new rules:

Image description

You can play around with the AWS WAF rules by following their official documentation.

Leveraging CloudFront for Security:

Understanding how CloudFront can be configured to prevent DDoS attacks, bot traffic, and other types of threats.

Consuming the Terraform AWS Modules CloudFront module, I can easily deploy a CloudFront distribution in Terraform:

inputs = {
  aliases = [
    "cdn.${local.environment_vars.locals.public_domain_name}",
    "*.${local.environment_vars.locals.public_domain_name}",
  ]

  comment         = "CDN for ${local.environment_vars.locals.public_domain_name}"
  enabled         = true
  http_version    = "http1.1"
  is_ipv6_enabled = false
  web_acl_id      = dependency.waf.outputs.arn

  # When you enable additional metrics for a distribution, CloudFront sends up to 8 metrics to CloudWatch in the US East (N. Virginia) Region.
  # This rate is charged only once per month, per metric (up to 8 metrics per distribution).
  create_monitoring_subscription = local.environment_vars.locals.cloudfront_monitoring_subscription

  ####################
  # Logging
  ####################
  logging_config = {
    bucket = dependency.s3.outputs.s3_bucket_bucket_domain_name
    prefix = "${local.env}.${local.environment_vars.locals.public_domain_name}"
  }

  ####################
  # SSL certificate
  ####################
  viewer_certificate = {
    acm_certificate_arn      = dependency.acm.outputs.acm_certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  ####################
  # Origin
  ####################
  origin = {
    alb = {
      domain_name         = local.environment_vars.locals.cloudfront_alb_alias_name
      origin_id           = "alb"
      connection_attempts = 3
      connection_timeout  = 10
      custom_origin_config = {
        http_port                = 80
        https_port               = 443
        origin_keepalive_timeout = include.root.locals.common_vars.locals.ONE_MINUTE_IN_MS
        origin_read_timeout    = include.root.locals.common_vars.locals.THREE_MINUTES_IN_MS
        origin_protocol_policy = "https-only"
        origin_ssl_protocols   = ["TLSv1.2"]
      }

      custom_header = [
        {
          name  = "X-Cloudfront-Security-Header"
          value = include.root.locals.common_vars.locals.cloudfront_security_header
        }
      ]

      origin_shield = {
        enabled              = true
        origin_shield_region = local.aws_region
      }
    }
  }

  ####################
  # Caching behavior
  ####################
  default_cache_behavior = {
    cache_policy_id = local.environment_vars.locals.cloudfront_response_headers_policy_id
    # Allow: [Headers - All viewer headers | Cookies - All | Query strings - All]
    origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3"
    target_origin_id         = "alb"
    viewer_protocol_policy   = "redirect-to-https"
    allowed_methods          = ["DELETE", "POST", "GET", "HEAD", "OPTIONS", "PUT", "PATCH"]
    cached_methods           = ["GET", "HEAD"]
    compress                 = true
    query_string             = true
    min_ttl                  = 0
    default_ttl              = 0
    max_ttl                  = 0
    # The parameter ForwardedValues cannot be used when a cache policy is associated to the cache behavior.
    use_forwarded_values = false
  }

  ordered_cache_behavior = [
    {
      cache_policy_id = local.environment_vars.locals.cloudfront_response_headers_policy_id
      # Allow: [Headers - All viewer headers | Cookies - All | Query strings - All]
      origin_request_policy_id = "216adef6-5c7f-47e4-b989-5492eafa07d3"
      target_origin_id         = "alb"
      viewer_protocol_policy   = "redirect-to-https"
      path_pattern             = "/*"
      allowed_methods          = ["DELETE", "POST", "GET", "HEAD", "OPTIONS", "PUT", "PATCH"]
      cached_methods           = ["GET", "HEAD"]
      compress                 = true
      query_string             = true
      min_ttl                  = 0
      default_ttl              = 0
      max_ttl                  = 0
      # The parameter ForwardedValues cannot be used when a cache policy is associated to the cache behavior.
      use_forwarded_values = false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
  • You can see that I have the following origin: origin_id = "alb": That's my Application Load Balancer. I created a Route53 Alias (i.e. alb.example.com) and I made CF to accept that endpoint as an origin.
  • custom_header: In my ALB, I have rules that it will only accept traffic from CloudFront if that header is present. This is another security best practice.
  • In aliases, I specify that my CF distribution will respond as cdn.example.com.

Continuous Monitoring and Maintenance

AWS WAF

The WAF dashboard will give you quite a good visibility on things:

Image description

However, I do have some extra alerts going to Slack. For this integration, I use the terraform-aws-notify-slack TF module with CloudWatch.

terraform-aws-notify-slack

inputs = {
  sns_topic_name       = "notify-slack-topic"
  slack_webhook_url    = local.environment_vars.locals.slack_webhook_url
  slack_channel        = local.environment_vars.locals.notify_slack_slack_channel
  slack_username       = "reporter"
  kms_key_arn          = dependency.sns_topic_kms.outputs.key_arn
  sns_topic_kms_key_id = dependency.sns_topic_kms.outputs.key_id
  lambda_function_name = "notify-slack-${local.env}"

  lambda_description = "Lambda function which sends notifications to Slack"
  log_events         = true

  # Added this argument not to recreate the missing package once it was created
  recreate_missing_package = false

  tags = local.environment_vars.locals.tags
}
Enter fullscreen mode Exit fullscreen mode

WAF Alerts

One example is:

"waf-CloudfrontRateBasedRule" = {
      alarm_name          = "waf-CloudfrontRateBasedRule-${local.env}"
      comparison_operator = "GreaterThanOrEqualToThreshold"
      evaluation_periods  = 1
      metric_name         = "BlockedRequests"
      period              = "300"
      statistic           = "Average"
      threshold           = local.environment_vars.locals.waf_rate_limit
      alarm_description   = "This metric monitors the BlockedRequests of the CloudfrontRateBasedRule WAF rule"
      alarm_actions       = [dependency.notify_slack.outputs.slack_topic_arn]
      ok_actions          = [dependency.notify_slack.outputs.slack_topic_arn]
      treat_missing_data  = "notBreaching"
      namespace           = "AWS/WAFV2"
      dimensions = {
        WebACL = dependency.waf.outputs.id
      }
    },
Enter fullscreen mode Exit fullscreen mode

I have alerts in the following WAF resources:

  • BlockedRequests
  • CountedRequests
💖 💪 🙅 🚩
lpossamai
Lucas Possamai

Posted on January 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related