Skip to content

Commit

Permalink
Added CloudFront WAF module. (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesiarmes authored Apr 17, 2024
1 parent 5ad9cc9 commit b237b98
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 9 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ terraform.

## Modules

| Provider | Module | Description |
|----------|------------------------|---------------------------------|
| AWS | [backend][aws-backend] | S3 storage backend for tfstate. |
| Provider | Module | Description |
|----------|--------------------------------------|--------------------------------------------------------------------------|
| AWS | [backend][aws-backend] | S3 storage backend for tfstate. |
| AWS | [cloudfront_waf][aws-cloudfront-waf] | CloudFront distribution that passes traffic through WAF without caching. |

[aws-backend]: ./aws/backend/README.md
[aws-cloudfront-waf]: ./aws/cloudfront_waf/README.md
[opentofu]: https://opentofu.org/
[terraform]: https://www.terraform.io/
12 changes: 6 additions & 6 deletions aws/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AWS Backend module
# AWS Backend Module

This module creates an AWS backend for OpenTofu.

Expand Down Expand Up @@ -58,8 +58,8 @@ You now have a fully configured AWS backend for your project!

## Inputs

| Name | Description | Type | Default | Required |
|---------------------|----------------------------------------------------------------------|----------|---------|:--------:|
| project | The name of the project | `string` | n/a | yes |
| environment | The environment for the project | `string` | `"dev"` | yes |
| key_recovery_period | The number of days to retain the KMS key for recovery after deletion | `number` | `30` | no |
| Name | Description | Type | Default | Required |
|---------------------|-----------------------------------------------------------------------|----------|---------|:--------:|
| project | The name of the project. | `string` | n/a | yes |
| environment | The environment for the project. | `string` | `"dev"` | no |
| key_recovery_period | The number of days to retain the KMS key for recovery after deletion. | `number` | `30` | no |
45 changes: 45 additions & 0 deletions aws/cloudfront_waf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CloudFront WAF Module

This module creates a CloudFront [distribution] that passes traffic through a
Web Application Firewall (WAF) _without_ caching.

## Usage

Add this module to your `main.tf` (or appropriate) file:

```hcl
module "backend" {
source = "github.com/codeforamerica/tofu-modules/aws/backend"
project = "my-project"
environment = "dev"
domain = "my-project.org"
log_bucket = module.log_bucket.s3_bucket_bucket_domain_name
}
```

Make sure you re-run `tofu init` after adding the module to your configuration.

```bash
tofu init
tofu plan
```

To update the source for this module, pass `-upgrade` to `tofu init`:

```bash
tofu init -upgrade
```

## Inputs

| Name | Description | Type | Default | Required |
|---------------|-----------------------------------------------------------------------------------------------------|----------|---------|----------|
| domain | Primary domain for the distribution. The hosted zone for this domain should be in the same account. | `string` | n/a | yes |
| log_bucket | Domain name of the S3 bucket to send logs to. | `string` | n/a | yes |
| project | The name of the project. | `string` | n/a | yes |
| environment | The environment for the project. | `string` | `"dev"` | no |
| origin_domain | Fully qualified domain name for the origin. Defaults to `origin.${subdomain}.${domain}`. | `string` | n/a | no |
| subdomain | Subdomain for the distribution. Defaults to the environment. | `string` | n/a | no |

[distribution]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html
11 changes: 11 additions & 0 deletions aws/cloudfront_waf/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
data "aws_cloudfront_origin_request_policy" "managed_cors" {
name = "Managed-CORS-CustomOrigin"
}

data "aws_cloudfront_response_headers_policy" "managed_cors" {
name = "Managed-SimpleCORS"
}

data "aws_route53_zone" "domain" {
name = var.domain
}
46 changes: 46 additions & 0 deletions aws/cloudfront_waf/dns.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
resource "aws_route53_record" "subdomain" {
zone_id = data.aws_route53_zone.domain.zone_id
name = "${local.subdomain}.${var.domain}"
type = "A"

alias {
name = aws_cloudfront_distribution.waf.domain_name
zone_id = aws_cloudfront_distribution.waf.hosted_zone_id
evaluate_target_health = false
}
}

resource "aws_acm_certificate" "subdomain" {
# Specify the name rather than referencing the resource directly. This allows
# us to create the certificate before the DNS record exists.
domain_name = "${local.subdomain}.${var.domain}"
validation_method = "DNS"

lifecycle {
create_before_destroy = true
}
}

resource "aws_route53_record" "validation" {
for_each = {
for dvo in aws_acm_certificate.subdomain.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}

allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 300
type = each.value.type
zone_id = data.aws_route53_zone.domain.zone_id
}

resource "aws_acm_certificate_validation" "validation" {
certificate_arn = aws_acm_certificate.subdomain.arn
validation_record_fqdns = [
for record in aws_route53_record.validation : record.fqdn
]
}
5 changes: 5 additions & 0 deletions aws/cloudfront_waf/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
locals {
subdomain = var.subdomain == "" ? var.environment : var.subdomain
origin_domain = var.origin_domain == "" ? "origin.${local.subdomain}.${var.domain}" : var.origin_domain
prefix = "${var.project}-${var.environment}"
}
212 changes: 212 additions & 0 deletions aws/cloudfront_waf/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
resource "aws_cloudfront_cache_policy" "waf_passthrough" {
name = "${var.project}-${var.environment}-waf-passthrough"
comment = "We don't really care about caching, we just want to pass traffic to the WAF."
default_ttl = 1
max_ttl = 1
min_ttl = 1

parameters_in_cache_key_and_forwarded_to_origin {
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true

cookies_config {
cookie_behavior = "all"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Host"]
}
}
query_strings_config {
query_string_behavior = "all"
}
}
}

resource "aws_cloudfront_distribution" "waf" {
enabled = true
comment = "Pass traffic through WAF before sending to the origin."
is_ipv6_enabled = true
aliases = ["${local.subdomain}.${var.domain}"]
price_class = "PriceClass_100"
web_acl_id = aws_wafv2_web_acl.waf.arn

origin {
domain_name = local.origin_domain
origin_id = local.origin_domain
connection_attempts = 3
connection_timeout = 10

custom_origin_config {
http_port = 80
https_port = 443
origin_keepalive_timeout = 5
origin_protocol_policy = "https-only"
origin_read_timeout = 30
origin_ssl_protocols = ["TLSv1.2"]
}
}

logging_config {
include_cookies = false
bucket = var.log_bucket
prefix = "cloudfront/${local.subdomain}.${var.domain}"
}

default_cache_behavior {
cache_policy_id = aws_cloudfront_cache_policy.waf_passthrough.id
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.origin_domain
compress = true
default_ttl = 0
max_ttl = 0
min_ttl = 0

origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed_cors.id
response_headers_policy_id = data.aws_cloudfront_response_headers_policy.managed_cors.id

viewer_protocol_policy = "redirect-to-https"
}

restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["US", "CA", "GB", "DE"]
}
}

viewer_certificate {
acm_certificate_arn = aws_acm_certificate.subdomain.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
}

resource "aws_wafv2_web_acl" "waf" {
name = local.prefix
description = "Web application firewall rules for ${var.project}."
scope = "CLOUDFRONT"

default_action {
allow {}
}

rule {
name = "AWS-RateBasedRule-IP-300"
priority = 0

action {
count {}
}

statement {
rate_based_statement {
aggregate_key_type = "IP"
evaluation_window_sec = 300
limit = 300
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-rate-limit"
sampled_requests_enabled = true
}
}

rule {
name = "AWS-AWSManagedRulesAmazonIpReputationList"
priority = 1

override_action {
none {}
}

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

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-ip-reputation"
sampled_requests_enabled = true
}
}

rule {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 2

override_action {
none {}
}

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

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-common-rules"
sampled_requests_enabled = true
}
}

rule {
name = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
priority = 3

override_action {
none {}
}

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

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-known-bad-inputs"
sampled_requests_enabled = true
}
}

rule {
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 4

override_action {
none {}
}

statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-sqli"
sampled_requests_enabled = true
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf"
sampled_requests_enabled = true
}
}
32 changes: 32 additions & 0 deletions aws/cloudfront_waf/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
variable "domain" {
type = string
description = "Domain used for this deployment."
}

variable "environment" {
type = string
description = "Environment for the deployment."
default = "dev"
}

variable "log_bucket" {
type = string
description = "S3 Bucket to send logs to."
}

variable "origin_domain" {
type = string
description = "Origin domain this deployment will point to. Defaults to origin.subdomain.domain."
default = ""
}

variable "project" {
type = string
description = "Project that these resources are supporting."
}

variable "subdomain" {
type = string
description = "Subdomain used for this deployment. Defaults to the environment."
default = ""
}
Loading

0 comments on commit b237b98

Please sign in to comment.