Cloudflare Docs
Terraform
Visit Terraform on GitHub
Set theme to dark (⇧+D)

Set up rate limiting

As your site gains more attention, you could discover attempts to brute force your login page at https://www.example.com/login from your serve access logs. In this tutorial, you will learn how to stop those attempts with Cloudflare’s rate limiting product.

1. Create a new branch and append the rate limiting settings

After creating a new branch, specify the rate limiting rule.

$ git checkout -b step4-ratelimit
Switched to a new branch 'step4-ratelimit'

$ cat >> cloudflare.tf <<'EOF'

resource "cloudflare_rate_limit" "login-limit" {
  zone_id = var.zone_id

  threshold = 5
  period    = 60
  match {
    request {
      url_pattern = "${var.domain}/login"
      schemes     = ["HTTP", "HTTPS"]
      methods     = ["POST"]
    }
    response {
      statuses       = [401, 403]
      origin_traffic = true
    }
  }
  action {
    mode    = "simulate"
    timeout = 300
    response {
      content_type = "text/plain"
      body         = "You have failed to login 5 times in a 60 second period and will be blocked from attempting to login again for the next 5 minutes."
    }
  }
  disabled    = false
  description = "Block failed login attempts (5 in 1 min) for 5 minutes."
}
EOF

This rule is a bit more complex than the zone settings rule and will be broken down.

00: resource "cloudflare_rate_limit" "login-limit" {
01:   zone_id = var.zone_id
02:
03:   threshold = 5
04:   period    = 60

The threshold is an integer count of how many times an event — defined by the match block below — has to be detected in the period before the rule takes action. The period is measured in seconds, so the above rule says to take action if the match fires five times in 60 seconds.

05:   match {
06:     request {
07:       url_pattern = "${var.domain}/login"
08:       schemes     = ["HTTP", "HTTPS"]
09:       methods     = ["POST"]
10:     }
11:     response {
12:       statuses = [401, 403]
13:     }
14:   }

The match block tells Cloudflare’s edge what to watch for, such as HTTP or HTTPS POST requests to https://www.example.com/login. Cloudflare further restricts the match to HTTP 401 (Unauthorized) or 403 (Forbidden) response codes returned from the origin.

15:   action {
16:     mode    = "simulate"
17:     timeout = 300
18:     response {
19:       content_type = "text/plain"
20:       body         = "You have failed to login 5 times in a 60 second period and will be blocked from attempting to login again for the next 5 minutes."
21:     }
22:   }
23:   disabled    = false
24:   description = "Block failed login attempts (5 in 1 min) for 5 minutes."
25: }

After matching traffic, set the action the edge should take. When testing, set the mode to simulate and review logs before taking enforcement action (see below). The timeout field indicates that the action should be enforced for 300 seconds (five minutes) and the response block indicates what should be sent back to the caller that tripped the rate limit.

2. Preview and merge the changes

Review the proposed plan before applying any changes.

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

cloudflare_record.www: Refreshing state... (ID: c38d3103767284e7cd14d5dad3ab8669)
cloudflare_zone_settings_override.example-com-settings: Refreshing state... (ID: e2e6491340be87a3726f91fc4148b126)

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + cloudflare_rate_limit.login-limit
      id:                                     <computed>
      action.#:                               "1"
      action.0.mode:                          "simulate"
      action.0.response.#:                    "1"
      action.0.response.0.body:               "You have failed to login 5 times in a 60 second period and will be blocked from attempting to login again for the next 5 minutes."
      action.0.response.0.content_type:       "text/plain"
      action.0.timeout:                       "300"
      description:                            "Block failed login attempts (5 in 1 min) for 5 minutes."
      disabled:                               "false"
      match.#:                                "1"
      match.0.request.#:                      "1"
      match.0.request.0.methods.#:            "1"
      match.0.request.0.methods.1012961568:   "POST"
      match.0.request.0.schemes.#:            "2"
      match.0.request.0.schemes.2328579708:   "HTTP"
      match.0.request.0.schemes.2534674783:   "HTTPS"
      match.0.request.0.url_pattern:          "www.example.com/login"
      match.0.response.#:                     "1"
      match.0.response.0.origin_traffic:      "true"
      match.0.response.0.statuses.#:          "2"
      match.0.response.0.statuses.1057413486: "403"
      match.0.response.0.statuses.221297644:  "401"
      period:                                 "60"
      threshold:                              "5"
      zone:                                   "example.com"
      zone_id:                                <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

The plan looks good, so you can merge it in and apply it.

$ git add cloudflare.tf
$ git commit -m "Step 4 - Add rate limiting rule to protect /login."
[step4-ratelimit 0f7e499] Step 4 - Add rate limiting rule to protect /login.
 1 file changed, 28 insertions(+)

$ git checkout master
Switched to branch 'master'

$ git merge step4-ratelimit
Updating 321c2bd..0f7e499
Fast-forward
 cloudflare.tf | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

$ terraform apply --auto-approve
cloudflare_record.www: Refreshing state... (ID: c38d3103767284e7cd14d5dad3ab8668)
cloudflare_zone_settings_override.example-com-settings: Refreshing state... (ID: e2e6491340be87a3726f91fc4148b125)
cloudflare_rate_limit.login-limit: Creating...
  action.#:                               "" => "1"
  action.0.mode:                          "" => "simulate"
  action.0.response.#:                    "" => "1"
  action.0.response.0.body:               "" => "You have failed to login 5 times in a 60 second period and will be blocked from attempting to login again for the next 5 minutes."
  action.0.response.0.content_type:       "" => "text/plain"
  action.0.timeout:                       "" => "300"
  description:                            "" => "Block failed login attempts (5 in 1 min) for 5 minutes."
  disabled:                               "" => "false"
  match.#:                                "" => "1"
  match.0.request.#:                      "" => "1"
  match.0.request.0.methods.#:            "" => "1"
  match.0.request.0.methods.1012961568:   "" => "POST"
  match.0.request.0.schemes.#:            "" => "2"
  match.0.request.0.schemes.2328579708:   "" => "HTTP"
  match.0.request.0.schemes.2534674783:   "" => "HTTPS"
  match.0.request.0.url_pattern:          "" => "www.example.com/login"
  match.0.response.#:                     "" => "1"
  match.0.response.0.origin_traffic:      "" => "true"
  match.0.response.0.statuses.#:          "" => "2"
  match.0.response.0.statuses.1057413486: "" => "403"
  match.0.response.0.statuses.221297644:  "" => "401"
  period:                                 "" => "60"
  threshold:                              "" => "5"
  zone:                                   "" => "example.com"
  zone_id:                                "" => "<computed>"
cloudflare_rate_limit.login-limit: Creation complete after 1s (ID: 8d518c5d6e63406a9466d83cb8675bb6)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

If you have not purchased rate limiting, you will see the following error when attempting to apply the new rule.

Error: Error applying plan:

1 error(s) occurred:

* cloudflare_rate_limit.login-limit: 1 error(s) occurred:

* cloudflare_rate_limit.login-limit: error creating rate limit for zone: error from makeRequest: HTTP status 400: content "{\n  \"result\": null,\n  \"success\": false,\n  \"errors\": [\n    {\n      \"code\": 10021,\n      \"message\": \"ratelimit.api.not_entitled.account\"\n    }\n  ],\n  \"messages\": []\n}\n"

3. Update the rule to ban (not just simulate)

After confirming that the rule is triggering but not yet enforcing in logs, switch from simulate to ban.

$ git checkout step4-ratelimit
$ sed -i.bak -e 's/simulate/ban/' cloudflare.tf

$ git diff
diff --git a/cloudflare.tf b/cloudflare.tf
index ed5157c..9f25a0c 100644
--- a/cloudflare.tf
+++ b/cloudflare.tf
@@ -42,7 +42,7 @@ resource "cloudflare_rate_limit" "login-limit" {
     }
   }
   action {
-    mode = "simulate"
+    mode = "ban"
     timeout = 300
     response {
       content_type = "text/plain"

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

cloudflare_zone_settings_override.example-com-settings: Refreshing state... (ID: e2e6491340be87a3726f91fc4148b126)
cloudflare_rate_limit.login-limit: Refreshing state... (ID: 8d518c5d6e63406a9466d83cb8675bb6)
cloudflare_record.www: Refreshing state... (ID: c38d3103767284e7cd14d5dad3ab8669)

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  ~ cloudflare_rate_limit.login-limit
      action.0.mode: "simulate" => "ban"


Plan: 0 to add, 1 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

4. Merge and deploy the updated rule, then push the config to GitHub

$ git add cloudflare.tf

$ git commit -m "Step 4 - Update /login rate limit rule from 'simulate' to 'ban'."
[step4-ratelimit e1c38cf] Step 4 - Update /login rate limit rule from 'simulate' to 'ban'.
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git checkout master && git merge step4-ratelimit && git push
Switched to branch 'master'
Updating 0f7e499..e1c38cf
Fast-forward
 cloudflare.tf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 361 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To git@github.com:$GITHUB_USER/cf-config.git
   0f7e499..e1c38cf  master -> master


$ terraform apply --auto-approve
cloudflare_rate_limit.login-limit: Refreshing state... (ID: 8d518c5d6e63406a9466d83cb8675bb6)
cloudflare_record.www: Refreshing state... (ID: c38d3103767284e7cd14d5dad3ab8669)
cloudflare_zone_settings_override.example-com-settings: Refreshing state... (ID: e2e6491340be87a3726f91fc4148b126)
cloudflare_rate_limit.login-limit: Modifying... (ID: 8d518c5d6e63406a9466d83cb8675bb6)
  action.0.mode: "simulate" => "ban"
cloudflare_rate_limit.login-limit: Modifications complete after 0s (ID: 8d518c5d6e63406a9466d83cb8675bb6)

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

$ git push
...

5. Confirm the rule works as expected

(Optional) This step is a good way to demonstrate that the rule works as expected. Note the final 429 response.

$ for i in {1..6}; do curl -XPOST -d '{"username": "foo", "password": "bar"}' -vso /dev/null https://www.example.com/login 2>&1 | grep "< HTTP"; sleep 1; done
< HTTP/1.1 401 OK
< HTTP/1.1 401 OK
< HTTP/1.1 401 OK
< HTTP/1.1 401 OK
< HTTP/1.1 401 OK
< HTTP/1.1 429 OK