Skip to content
Terraform
Visit Terraform on GitHub
Set theme to dark (⇧+D)

Woah, slow down there

With our zone settings locked down, and our site starting to get some more attention, it's unfortunately begun attracting some of the less scrupulous characters on the internet. Our server access logs show attempts to brute force our login page at https://www.example.com/login. Let's see what we can do with Cloudflare's rate limiting product to put a stop to these efforts.

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

After creating a new branch we specify the rate limiting rule:

$ git checkout -b step4-ratelimitSwitched to a new branch 'step4-ratelimit'
$ cat >> cloudflare.cf <<'EOF'
resource "cloudflare_rate_limit" "login-limit" {  zone = "${var.domain}"
  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, so let's break it down:

00: resource "cloudflare_rate_limit" "login-limit" {01:   zone = "${var.domain}"02:03:   threshold = 504:   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 5 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 the Cloudflare edge what to be on the lookout for, i.e., HTTP or HTTPS POST requests to https://www.example.com/login. We further restrict the match to HTTP 401 (Unauthorized) or 403 (Forbidden) response codes returned from the origin.

15:   action {16:     mode = "simulate"17:     timeout = 30018:     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 = false24:   description = "Block failed login attempts (5 in 1 min) for 5 minutes."25: }

After matching traffic, we set the action for our edge to take. When testing, it's a good idea to set the mode to simulate and review logs before taking enforcement action (see below). The timeout field here indicates that we want to enforce this action for 300 seconds (5 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

As usual, we take a look at the proposed plan before we apply any changes:

$ terraform planRefreshing Terraform state in-memory prior to plan...The refreshed state will be used to calculate this plan, but will not bepersisted 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 Terraformcan't guarantee that exactly these actions will be performed if"terraform apply" is subsequently run.

The plan looks good so let's go ahead, 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 masterSwitched to branch 'master'
$ git merge step4-ratelimitUpdating 321c2bd..0f7e499Fast-forward cloudflare.tf | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+)
$ terraform apply --auto-approvecloudflare_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.

Note that if you haven't purchased rate limiting yet, 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 as planned in logs (but not yet enforcing), it's time to switch from simulate to ban:

$ git checkout step4-ratelimit$ sed -i.bak -e 's/simulate/ban/' cloudflare.tf
$ git diffdiff --git a/cloudflare.tf b/cloudflare.tfindex 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 planRefreshing Terraform state in-memory prior to plan...The refreshed state will be used to calculate this plan, but will not bepersisted 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 Terraformcan't guarantee that exactly these actions will be performed if"terraform apply" is subsequently run.

4. Merge and deploy the updated rule, then push 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 pushSwitched to branch 'master'Updating 0f7e499..e1c38cfFast-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-approvecloudflare_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 is working as expected

This step is optional, but it's a good way to demonstrate that the rule is working 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