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
}
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":
You'll be able to see the new rules:
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
}
]
}
- 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 ascdn.example.com
.
Continuous Monitoring and Maintenance
AWS WAF
The WAF dashboard will give you quite a good visibility on things:
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
}
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
}
},
I have alerts in the following WAF resources:
- BlockedRequests
- CountedRequests
Top comments (0)