WordPress Admin Protection with a Cloudflare Firewall Rule

I enjoy sharing content on the Internet using this WordPress blog as a vehicle. With that joy comes the responsibility of protecting the site against attackers to avoid causing harm to those that visit. I have used Cloudflare to assist with the security of my website for the past 7 years and am a big fan of their tools and extensible set of APIs.

One of the easiest and most powerful tools is the Cloudflare firewall. This can be used to block access to the WordPress admin console based on filters such as the public IP address of the visitor. In this post, I show how to build a Cloudflare firewall rule to filter malicious access requests to the admin console using the web UI and Terraform code.

Basic Firewall Rule Configuration

I’ll start by creating a firewall rule by hand as a basic configuration example. I’ve navigated to my account > Firewall > Firewall Rules and selected Create a Firewall Rule to show the example below:

This rule is triggered when a visitor requests the WordPress admin panel, located at /wp-admin in a default installation, using a public IP address other than mine. Traffic matching this rule is blocked. If my public IP address changes, I can re-visit the firewall rule and update the configuration as required.

This approach works for an ad-hoc configuration but does not scale well and requires more operational overhead than I would prefer. Time to automate!

Firewall Rules as Code

I’ve put together an infrastructure as code approach to the Cloudflare firewall rule using Terraform. The example below is split into four objects: the Terraform Cloudflare provider, zone data, a filter, and a firewall rule.

I’ve taken apart the code in the sections below.

Cloudflare Provider

The provider requires an API token in order to authenticate. I suggest reading the Managing API Tokens and Keys documentation prior to making a token. For this example, the API token will need:

  • Zone.Zone Settings – Read
  • Zone.Zone – Read
  • Zone.Firewall Services – Edit

Zone Data

Next is the zone data. This provides the zone id value needed for further configuration. I could have statically defined the zone id value the configuration, but prefer to pull id values dynamically from the actual deployment.

Filter Resource

The third object is the filter resource. This is an API-only resource that defines the filter expression used by other objects, such as firewall rules. Note that I’ve supplied zones[0] to the zone_id key. This is because the zone data pulled earlier returns an array of zones stored in zones regardless of the quantity of zones returned.

I’ve shared the raw schema below:

"instances": [
        {
          "schema_version": 0,
          "attributes": {
            "filter": [
              {
                "name": "wahlnetwork.com",
                "paused": false,
                "status": "active"
              }
            ],
            "id": "2020-06-22 20:10:15.651915722 +0000 UTC",
            "zones": [
              {
                "id": "111111111111111111111111111111111111",
                "name": "wahlnetwork.com"
              }
            ]
          }
        }
      ]

I feel safe with this code construction because there’s only a single zone named wahlnetwork.com and I don’t see why I would add more.

Firewall Rule Resource

The fourth and final object is the firewall rule resource. I’ve supplied all of the earlier information to construct a firewall rule: the zone_id from the zone data along with a description and filter_id from the filter resource. The block action supplies the final piece needed to generate a firewall rule. Additionally, I’ve embedded earlier values in this resource to ensure that the Terraform graph maps the dependencies between these objects.

In a real-world deployment, I would suggest placing the api_token value into a secret stored in the repository or elsewhere such as Vault. The secret value can be passed along as an environmental variable to the Terraform CLI using something like -var 'api_token=${api_token}' in most cases.

Next Steps

Please accept a crisp high five for reaching this point in the post!

If you’d like to learn more about Infrastructure as Code, or other modern technology approaches, head over to the Guided Learning page.