Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added rate limiting configuration. #8

Merged
merged 8 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
- name: Bump version and create changelog
id: bump
uses: commitizen-tools/commitizen-action@master
Expand Down
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Contributing

## Commit message format

All commit messages should follow the [Conventional Commits][commits] format.
This format allows us to automatically generate changelogs and version numbers
based on the commit messages.

Common commit types include:

* `fix`: A bug fix
* `feat`: A new feature
* `ci`: Changes to CI/CD
* `docs`: Changes to documentation

adding `!` after the type indicates a breaking change. For example, `feat!`
would indicate a new feature that breaks existing functionality, and would
therefore require a major version bump.

`bump` is a special type used to indicate a version bump. This is used by the
automated release process, and should be avoided in normal commits.

## Coding standards

Code should follow the [OpenTofu style conventions][style]. This ensures that
all code is consistent and easy to read and maintain.

To make resources easier to find, you may group them together in a single file
within your module. For example, while `main.tf` handles the main configuration,
you may create a `dns.tf` file to handle all DNS-related resources.

Additionally, the following should be grouped together within their own files:

* `data.tf` for data sources
* `local.tf` for local values
* `output.tf` for outputs

## Code reviews

All code should be contributing in the form of a pull request. Pull requests
should have an approval from _at least_ one required reviewer as defined in the
`CODEOWNERS` file. Additional reviews are welcome, and may be requested by
either the submitter or the required reviewer.

[commits]: https://www.conventionalcommits.org/en/v1.0.0/
[style]: https://opentofu.org/docs/language/syntax/style/
73 changes: 61 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,20 @@ these rules are spaced out to allow for custom rules to be inserted between.

## 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 |
| [ip_set_rules] | The environment for the project. | `map(object)` | `"dev"` | no |
| log_group | CloudWatch log group to send WAF logs to. | `list(string)` | `[]` | no |
| origin_domain | Fully qualified domain name for the origin. Defaults to `origin.${subdomain}.${domain}`. | `string` | n/a | no |
| passive | Enable passive mode for the WAF, counting all requests rather than blocking. | `bool` | `false` | no |
| subdomain | Subdomain for the distribution. Defaults to the environment. | `string` | n/a | no |
| tags | Optional tags to be applied to all resources. | `list` | `[]` | no |

| 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 |
| log_group | CloudWatch log group to send WAF logs to. | `string` | n/a | yes |
| project | Project that these resources are supporting. | `string` | n/a | yes |
| environment | The environment for the deployment. | `string` | `"dev"` | no |
| [ip_set_rules] | Custom IP Set rules for the WAF | `map(object)` | `{}` | no |
| [rate_limit_rules] | Rate limiting configuration for the WAF. | `map(object)` | `{}` | no |
| origin_domain | Fully qualified domain name for the origin. Defaults to `origin.${subdomain}.${domain}`. | `string` | n/a | no |
| passive | Enable passive mode for the WAF, counting all requests rather than blocking. | `bool` | `false` | no |
| subdomain | Subdomain for the distribution. Defaults to the environment. | `string` | n/a | no |
| tags | Optional tags to be applied to all resources. | `map(string)` | `{}` | no |

### ip_set_rules

Expand Down Expand Up @@ -102,10 +104,57 @@ module "cloudfront_waf" {
}
```

| Name | Description | Type | Default | Required |
|----------|-------------------------------------------------------------------------------|----------|-----------|----------|
| action | The action to perform. | `string` | `"allow"` | no |
| arn | ARN of the IP set to match on. | `string` | n/a | yes |
| name | Name for this rule. Defaults to `${project}-${environment}-rate-${rule.key}`. | `string` | `""` | no |
| priority | Rule priority. Defaults to the rule's position in the map. | `number` | `nil` | no |

### rate_limit_rules

To rate limit traffic based on IP address, you can specify a map of rate limit
rules to create. The rate limit rules are applied in the order they are defined,
or though the `priority` field.

> [!NOTE]
> Rate limit rules are added after all IP set rules by default. Use `priority`
> to order your rules if you need more control.

For example, to rate limit requests to 300 over a 5-minute period:

```hcl
module "cloudfront_waf" {
source = "github.com/codeforamerica/tofu-modules-aws-cloudfront-waf?ref=1.1.0"

project = "my-project"
environment = "staging"
domain = "my-project.org"
log_bucket = module.logging.bucket

rate_limit_rules = {
limit = {
name = "my-project-staging-rate-limit"
action = "block"
limit = 500
window = 500
}
}
}
```

| Name | Description | Type | Default | Required |
|----------|-----------------------------------------------------------------------------------------|----------|-----------|----------|
| action | The action to perform. | `string` | `"block"` | no |
| name | Name for this rule. Defaults to `${project}-${environment}-rate-${rule.key}`. | `string` | `""` | no |
| limit | The number of requests allowed within the window. Minimum value of 10. | `number` | `10` | no |
| priority | Rule priority. Defaults to the rule's position in the map + the number of IP set rules. | `number` | `nil` | no |
| window | Number of seconds to limit requests in. Options are: 60, 120, 300, 600 | `number` | `60` | no |

[distribution]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html
[ip-rules]: https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-ipset-match.html
[ip_set_rules]: #ip_set_rules
[rate_limit_rules]: #rate_limit_rules
[rules-common]: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs
[rules-inputs]: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-known-bad-inputs
[rules-ip-rep]: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-amazon
Expand Down
68 changes: 42 additions & 26 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -127,33 +127,49 @@ resource "aws_wafv2_web_acl" "waf" {

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

# TODO: Make rate-limiting configurable.
rule {
name = "AWS-RateBasedRule-IP-300"
priority = 100
# For each rate-limiting rule, create a rule with the appropriate action.
dynamic "rule" {
for_each = var.rate_limit_rules
content {
name = rule.value.name != "" ? rule.value.name : "${local.prefix}-rate-${rule.key}"
priority = rule.value.priority != null ? rule.value.priority : index(var.ip_set_rules, rule.key) + length(var.ip_set_rules)

action {
count {}
}
action {
dynamic "allow" {
for_each = rule.value.action == "allow" ? [true] : []
content {}
}

statement {
rate_based_statement {
aggregate_key_type = "IP"
evaluation_window_sec = 300
limit = 300
dynamic "block" {
for_each = rule.value.action == "block" ? [true] : []
content {}
}

dynamic "count" {
for_each = rule.value.action == "count" ? [true] : []
content {}
}
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.prefix}-waf-rate-limit"
sampled_requests_enabled = true
statement {
rate_based_statement {
aggregate_key_type = "IP"
evaluation_window_sec = rule.value.window
limit = rule.value.limit
}
}

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

Expand All @@ -164,12 +180,12 @@ resource "aws_wafv2_web_acl" "waf" {
override_action {
dynamic "none" {
for_each = var.passive ? [] : [true]
content { }
content {}
}

dynamic "count" {
for_each = var.passive ? [true] : []
content { }
content {}
}
}

Expand All @@ -194,12 +210,12 @@ resource "aws_wafv2_web_acl" "waf" {
override_action {
dynamic "none" {
for_each = var.passive ? [] : [true]
content { }
content {}
}

dynamic "count" {
for_each = var.passive ? [true] : []
content { }
content {}
}
}

Expand All @@ -224,12 +240,12 @@ resource "aws_wafv2_web_acl" "waf" {
override_action {
dynamic "none" {
for_each = var.passive ? [] : [true]
content { }
content {}
}

dynamic "count" {
for_each = var.passive ? [true] : []
content { }
content {}
}
}

Expand All @@ -254,12 +270,12 @@ resource "aws_wafv2_web_acl" "waf" {
override_action {
dynamic "none" {
for_each = var.passive ? [] : [true]
content { }
content {}
}

dynamic "count" {
for_each = var.passive ? [true] : []
content { }
content {}
}
}

Expand Down
4 changes: 2 additions & 2 deletions testing.tfvars
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
log_bucket = "testing-bucket.s3.amazonaws.com"
domain = "example.com"
project = "example"
domain = "example.com"
project = "example"
16 changes: 14 additions & 2 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,25 @@ variable "ip_set_rules" {
priority = optional(number, null)
arn = string
}))
description = "Custom WAF rules to apply to the CloudFront distribution."
description = "Custom IP Set rules for the WAF."
default = {}
}

variable "rate_limit_rules" {
type = map(object({
name = optional(string, "")
action = optional(string, "block")
limit = optional(number, 10)
window = optional(number, 60)
priority = optional(number, null)
}))
description = "Rate limiting configuration for the WAF."
default = {}
}

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

Expand Down