Skip to content

Commit

Permalink
feat: Added rate limiting configuration. (#8)
Browse files Browse the repository at this point in the history
fix: Updated metric names.
docs: Added documentation for `ip_set_rules` and `rate_limit_rules`.
  • Loading branch information
jamesiarmes authored Oct 29, 2024
1 parent 435a193 commit 8ec494c
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 43 deletions.
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

0 comments on commit 8ec494c

Please sign in to comment.