Skip to content

Commit

Permalink
feat: extract WAF into its own module (#356)
Browse files Browse the repository at this point in the history
* feat: extract WAF into its own module

* Update README.md

* feat: allow exact domain name match or any subdomain

* document limits

* [WIP] Fix inteprolation

* [WIP] Fix or statement

* [WIP] Add alb and waf to stack app

* wip: create s3 policy in module

* fix: default to []

* fix: pass through from s3-private

* fix: json poilcy

* fix?: refactor policy

* fix: fmt

* fix: naming of waf_acl_arn

Co-Authored-By: Sam Kah Chiin <[email protected]>

---------

Co-authored-by: Sam Kah Chiin <[email protected]>
Co-authored-by: Sam Kah Chiin <[email protected]>
  • Loading branch information
3 people authored Jul 4, 2024
1 parent 5d24f0c commit 4039cc6
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 171 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"[terraform]": {
"editor.defaultFormatter": "hashicorp.terraform",
"editor.defaultFormatter": "gamunu.opentofu",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
Expand Down
25 changes: 2 additions & 23 deletions aws/ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The `idle_timeout` is set to 60 seconds. Ensure that your webserver's keep-alive

When using Rails with Puma, the default timeout is 20 seconds. This can be changed by setting the `persistent_timeout` option in `config/puma.rb`.

If you enable WAF, and you don't specify any further rules, then the default rule will be to block all traffic.
For better security, add the WAF module.

## Usage

Expand Down Expand Up @@ -143,28 +143,7 @@ module "ecs" {
}
# WAF
enable_waf = false
domain_name = "example.com" # only requests originating on this domain will be allowed
waf_rules = [
{
name = "AllowCloudflare"
priority = 1
action_type = "ALLOW"
header_name = "X-Custom-Header"
header_value = "your-secret-value" # inject some secret to the headers in CF
positional_constraint = "EXACTLY"
text_transformation = "NONE"
},
{
name = "BlockOtherTraffic"
priority = 2
action_type = "BLOCK"
header_name = "X-Other-Header"
header_value = "block-value"
positional_constraint = "CONTAINS"
text_transformation = "LOWERCASE"
}
]
waf_acl_arn = null # see module aws/waf
# Access Logs, stored in S3
# see: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html
Expand Down
60 changes: 8 additions & 52 deletions aws/ecs/alb.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,65 +17,21 @@ resource "aws_alb" "alb" {
}

dynamic "access_logs" {
for_each = var.enable_access_logs ? [1] : []
for_each = var.alb_access_logs == null ? [] : [1]
content {
bucket = var.access_logs_bucket
prefix = var.access_logs_prefix
bucket = var.alb_access_logs.bucket
prefix = var.alb_access_logs.bucket_prefix
enabled = true
}
}
}

# Access logs
data "aws_elb_service_account" "main" {}
# WAF
resource "aws_wafv2_web_acl_association" "alb_waf" {
count = var.waf_acl_arn == null ? 0 : 1

data "aws_iam_policy_document" "lb_logs" {
policy_id = "lb_logs"

statement {
actions = [
"s3:PutObject",
]
effect = "Allow"
resources = ["arn:aws:s3:::${var.access_logs_bucket}/${var.access_logs_prefix}/*"]

principals {
identifiers = ["${data.aws_elb_service_account.main.arn}"]
type = "AWS"
}
}

statement {
actions = [
"s3:PutObject"
]
effect = "Allow"
resources = ["arn:aws:s3:::${var.access_logs_bucket}/${var.access_logs_prefix}/*"]
principals {
identifiers = ["delivery.logs.amazonaws.com"]
type = "Service"
}
}


statement {
actions = [
"s3:GetBucketAcl"
]
effect = "Allow"
resources = ["arn:aws:s3:::${var.access_logs_bucket}"]
principals {
identifiers = ["delivery.logs.amazonaws.com"]
type = "Service"
}
}
}

resource "aws_s3_bucket_policy" "lb-logs" {
count = var.enable_access_logs ? 1 : 0

bucket = var.access_logs_bucket
policy = data.aws_iam_policy_document.lb_logs.json
resource_arn = aws_alb.alb.arn
web_acl_arn = var.waf_acl_arn
}

resource "aws_alb_listener" "http" {
Expand Down
60 changes: 10 additions & 50 deletions aws/ecs/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -225,57 +225,17 @@ variable "nlb_subnet_ids" {
}

# WAF & Access Logs
variable "enable_waf" {
description = "Enable WAF for the ALB"
type = bool
default = false
}

variable "domain_name" {
description = "Allowlisted domain name for the WAF"
type = string
default = ""

}

variable "waf_rules" {
description = "List of WAF rules to include in the Web ACL"
type = list(object({
name = string
priority = number
action_type = string # one of: ALLOW, BLOCK, COUNT
header_name = optional(string)
header_value = optional(string)
positional_constraint = optional(string, "EXACTLY")
text_transformation = optional(string, "NONE")
}))
default = [
{
name = "AWSManagedRulesCommonRuleSet"
priority = 1
action_type = "COUNT"
header_name = null
header_value = null
positional_constraint = "EXACTLY"
text_transformation = "NONE"
}
]
}

variable "enable_access_logs" {
description = "Enable access logging for the ALB"
type = bool
default = false
}

variable "access_logs_bucket" {
description = "S3 bucket for ALB access logs"
variable "waf_acl_arn" {
description = "ARN of the WAF Web ACL to associate with the ALB"
type = string
default = ""
default = null
nullable = true
}

variable "access_logs_prefix" {
description = "S3 prefix for ALB access logs"
type = string
default = "lb-logs"
variable "alb_access_logs" {
type = object({
bucket = string # S3 bucket for ALB access logs
bucket_prefix = optional(string, "lb-logs") # S3 prefix for ALB access logs
})
default = null
}
1 change: 1 addition & 0 deletions aws/s3-private/s3.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module "s3" {
kms_deletion_window_in_days = var.kms_deletion_window_in_days
multi_region_kms_key = var.multi_region_kms_key
enable_encryption = var.sse_algorithm == "aws:kms"
writers = var.writers
}

resource "aws_s3_bucket_lifecycle_configuration" "main-bucket-lifecycle-rule" {
Expand Down
8 changes: 8 additions & 0 deletions aws/s3-private/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ variable "sse_algorithm" {
error_message = "sse algorithm must be one of AES256, aws:kms, aws:kms:dsse"
}
}

variable "writers" {
type = list(object({
policy_id = string
prefix = string
}))
default = []
}
41 changes: 41 additions & 0 deletions aws/s3/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,44 @@ resource "aws_s3_bucket_versioning" "main" {
status = var.versioning ? "Enabled" : "Disabled"
}
}

data "aws_elb_service_account" "main" {}

locals {
writers_policy_json = jsonencode({
"Version" : "2012-10-17",
"Statement" : flatten([
for writer in var.writers : [
{
"Effect" : "Allow",
"Action" : "s3:PutObject",
"Resource" : "${aws_s3_bucket.main.arn}/${writer.prefix}/*",
"Principal" : {
"AWS" : "${data.aws_elb_service_account.main.arn}"
}
},
{
"Effect" : "Allow",
"Action" : "s3:PutObject",
"Resource" : "${aws_s3_bucket.main.arn}/${writer.prefix}/*",
"Principal" : {
"Service" : "delivery.logs.amazonaws.com"
}
},
{
"Effect" : "Allow",
"Action" : "s3:GetBucketAcl",
"Resource" : "${aws_s3_bucket.main.arn}",
"Principal" : {
"Service" : "delivery.logs.amazonaws.com"
}
}
]
])
})
}

resource "aws_s3_bucket_policy" "writers" {
bucket = aws_s3_bucket.main.id
policy = local.writers_policy_json
}
8 changes: 8 additions & 0 deletions aws/s3/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ variable "multi_region_kms_key" {
default = false
type = bool
}

variable "writers" {
type = list(object({
policy_id = string
prefix = string
}))
default = []
}
3 changes: 3 additions & 0 deletions aws/stack/app/ecs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ module "ecs" {
monitored_service_groups = var.monitored_service_groups
enable_container_insights = var.enable_container_insights

alb_access_logs = var.alb_access_logs
waf_acl_arn = var.waf_acl_arn

allow_internal_traffic_to_ports = var.allow_internal_traffic_to_ports
allow_alb_traffic_to_ports = var.allow_alb_traffic_to_ports
alb_listener_rules = var.alb_listener_rules
Expand Down
14 changes: 13 additions & 1 deletion aws/stack/app/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,19 @@ variable "alb_listener_rules" {
default = []
}

variable "alb_access_logs" {
type = object({
bucket = string
bucket_prefix = string
})
default = null
}

variable "waf_acl_arn" {
type = string
default = null
}

variable "ecs_name" {
type = string
default = null
Expand Down Expand Up @@ -465,7 +478,6 @@ variable "alb_subnet_type" {
}
}


variable "additional_certificate_arns" {
description = "Additional certificates to add to the load balancer"
default = []
Expand Down
66 changes: 66 additions & 0 deletions aws/waf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# WAF

This module creates a WAF with rules. It will by default add `AWSManagedRulesCommonRuleSet`.

If you pass allowed domain names, it will add a rule to only allow traffic from those domains.

The WAF is regional, so it can be associated with all ALBs from one region (regardless of the environment).

**Notes**:

* Up to 50 ALBs can be attached to a single WebACL.
* up to 100 Amazon CloudFront distributions, AWS AppSync GraphQL APIs, or Amazon API Gateway REST APIs can be associated with a single WebACL.
* Any resource can only be attached to exactly one WebACL.
* Maximum number of requests per second per web ACL: 25k

Find more limites [here](https://docs.aws.amazon.com/waf/latest/developerguide/limits.html).

## Usage

```hcl
module "waf" {
source = "github.com/dbl-works/terraform//aws/waf?ref=v2021.07.05"
project = local.project
region = local.region # or region_name
# NOTE: all subdomans are permitted
permitted_domain_names = [
"example.com",
"example.cloud",
]
waf_rules = [
{
name = "AllowCloudflare"
priority = 1
action_type = "ALLOW"
header_name = "X-Custom-Header"
header_value = "your-secret-value" # inject some secret to the headers in CF
positional_constraint = "EXACTLY"
text_transformation = "NONE"
},
{
name = "BlockOtherTraffic"
priority = 2
action_type = "BLOCK"
header_name = "X-Other-Header"
header_value = "block-value"
positional_constraint = "CONTAINS"
text_transformation = "LOWERCASE"
}
]
}
```

The module outputs the WAF ARN. Pass this ARN to the ECS module to associate the WAF with the ALB.

## Rules

See [waf-rules](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rules.html).

See [waf-oversize-request-components](https://docs.aws.amazon.com/waf/latest/developerguide/waf-oversize-request-components.html)

* Body and JSON Body - For Application Load Balancer and AWS AppSync, AWS WAF can inspect the first 8 KB of the body of a request. For CloudFront, API Gateway, Amazon Cognito, App Runner, and Verified Access, by default, AWS WAF can inspect the first 16 KB, and you can increase the limit up to 64 KB in your web ACL configuration.
* Headers - AWS WAF can inspect at most the first 8 KB (8,192 bytes) of the request headers and at most the first 200 headers. The content is available for inspection by AWS WAF up to the first limit reached.
* Cookies - AWS WAF can inspect at most the first 8 KB (8,192 bytes) of the request cookies and at most the first 200 cookies. The content is available for inspection by AWS WAF up to the first limit reached.
3 changes: 3 additions & 0 deletions aws/waf/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
output "waf_acl_arn" {
value = aws_wafv2_web_acl.main.arn
}
Loading

0 comments on commit 4039cc6

Please sign in to comment.