The highest trafficked sites using Cloudflare receive billions of requests per day. But only about 5% of those requests typically trigger security rules, whether they be “managed” rules such as our WAF and DDoS protections, or custom rules such as those configured by customers using our powerful Firewall Rules and Rate Limiting engines.

When enforcement is taken on a request that interrupts the flow of malicious traffic, a Firewall Event is logged with detail about the request including which rule triggered us to take action and what action we took, e.g., challenged or blocked outright.

Previously, if you wanted to ingest all of these events into your SIEM or logging platform, you had to take the whole firehose of requests—good and bad—and then filter them client side. If you’re paying by the log line or scaling your own storage solution, this cost can add up quickly. And if you have a security team monitoring logs, they’re being sent a lot of extraneous data to sift through before determining what needs their attention most.

As of today, customers using Cloudflare Logs can create Logpush jobs that send only Firewall Events. These events arrive much faster than our existing HTTP requests logs: they are typically delivered to your logging platform within 60 seconds of sending the response to the client.

In this post we’ll show you how to use Terraform and Sumo Logic, an analytics integration partner, to get this logging set up live in just a few minutes.

Process overview

The steps below take you through the process of configuring Cloudflare Logs to push security events directly to your logging platform. For purposes of this tutorial, we’ve chosen Sumo Logic as our log destination, but you’re free to use any of our analytics partners, or any logging platform that can read from cloud storage such as AWS S3, Azure Blob Storage, or Google Cloud Storage.

To configure Sumo Logic and Cloudflare we make use of Terraform, a popular Infrastructure-as-Code tool from HashiCorp. If you’re new to Terraform, see Getting started with Terraform and Cloudflare for a guided walkthrough with best practice recommendations such as how to version and store your configuration in git for easy rollback.

Once the infrastructure is in place, you’ll send a malicious request towards your site to trigger the Cloudflare Web Application Firewall, and watch as the Firewall Events generated by that request shows up in Sumo Logic about a minute later.

Prerequisites

Install Terraform and Go

First you’ll need to install Terraform. See our Developer Docs for instructions.

Next you’ll need to install Go. The easiest way on macOS to do so is with Homebrew:

$ brew install golang $ export GOPATH=$HOME/go $ mkdir $GOPATH

Go is required because the Sumo Logic Terraform Provider is a "community" plugin, which means it has to be built and installed manually rather than automatically through the Terraform Registry, as will happen later for the Cloudflare Terraform Provider.

Install the Sumo Logic Terraform Provider Module

The official installation instructions for installing the Sumo Logic provider can be found on their GitHub Project page, but here are my notes:

$ mkdir -p $GOPATH/src/github.com/terraform-providers && cd $_ $ git clone https://github.com/SumoLogic/sumologic-terraform-provider.git $ cd sumologic-terraform-provider $ make install

Prepare Sumo Logic to receive Cloudflare Logs

Install Sumo Logic livetail utility

While not strictly necessary, the livetail tool from Sumo Logic makes it easy to grab the Cloudflare Logs challenge token we’ll need in a minute, and also to view the fruits of your labor: seeing a Firewall Event appear in Sumo Logic shortly after the malicious request hit the edge.

On macOS:

$ brew cask install livetail ... ==> Verifying SHA-256 checksum for Cask 'livetail'. ==> Installing Cask livetail ==> Linking Binary 'livetail' to '/usr/local/bin/livetail'. 🍺 livetail was successfully installed!

Generate Sumo Logic Access Key

This step assumes you already have a Sumo Logic account. If not, you can sign up for a free trial here.

Browse to https://service.$ENV.sumologic.com/ui/#/security/access-keys where $ENV should be replaced by the environment you chose on signup. Click the "+ Add Access Key" button, give it a name, and click "Create Key" In the next step you'll save the Access ID and Access Key that are provided as environment variables, so don’t close this modal until you do.

Generate Cloudflare Scoped API Token

Log in to the Cloudflare Dashboard Click on the profile icon in the top-right corner and then select "My Profile" Select "API Tokens" from the nav bar and click "Create Token" Click the "Get started" button next to the "Create Custom Token" label

On the Create Custom Token screen:

Provide a token name, e.g., "Logpush - Firewall Events" Under Permissions, change Account to Zone, and then select Logs and Edit, respectively, in the two drop-downs to the right Optionally, change Zone Resources and IP Address Filtering to restrict restrict access for this token to specific zones or from specific IPs

Click "Continue to summary" and then "Create token" on the next screen. Save the token somewhere secure, e.g., your password manager, as it'll be needed in just a minute.

Set environment variables

Rather than add sensitive credentials to source files (that may get submitted to your source code repository), we'll set environment variables and have the Terraform modules read from them.

$ export CLOUDFLARE_API_TOKEN="<your scoped cloudflare API token>" $ export CF_ZONE_ID="<tag of zone you wish to send logs for>"

We'll also need your Sumo Logic environment, Access ID, and Access Key:

$ export SUMOLOGIC_ENVIRONMENT="eu" $ export SUMOLOGIC_ACCESSID="<access id from previous step>" $ export SUMOLOGIC_ACCESSKEY="<access key from previous step>"

Create the Sumo Logic Collector and HTTP Source

We'll create a directory to store our Terraform project in and build it up as we go:

$ mkdir -p ~/src/fwevents && cd $_

Then we'll create the Collector and HTTP source that will store and provide Firewall Events logs to Sumo Logic:

$ cat <<'EOF' | tee main.tf ################## ### SUMO LOGIC ### ################## provider "sumologic" { environment = var.sumo_environment access_id = var.sumo_access_id } resource "sumologic_collector" "collector" { name = "CloudflareLogCollector" timezone = "Etc/UTC" } resource "sumologic_http_source" "http_source" { name = "firewall-events-source" collector_id = sumologic_collector.collector.id timezone = "Etc/UTC" } EOF

Then we'll create a variables file so Terraform has credentials to communicate with Sumo Logic:

$ cat <<EOF | tee variables.tf ################## ### SUMO LOGIC ### ################## variable "sumo_environment" { default = "$SUMOLOGIC_ENVIRONMENT" } variable "sumo_access_id" { default = "$SUMOLOGIC_ACCESSID" } EOF

With our Sumo Logic configuration set, we’ll initialize Terraform with terraform init and then preview what changes Terraform is going to make by running terraform plan :

$ terraform init Initializing the backend... Initializing provider plugins... Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.

$ 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. ------------------------------------------------------------------------ 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: # sumologic_collector.collector will be created + resource "sumologic_collector" "collector" { + destroy = true + id = (known after apply) + lookup_by_name = false + name = "CloudflareLogCollector" + timezone = "Etc/UTC" } # sumologic_http_source.http_source will be created + resource "sumologic_http_source" "http_source" { + automatic_date_parsing = true + collector_id = (known after apply) + cutoff_timestamp = 0 + destroy = true + force_timezone = false + id = (known after apply) + lookup_by_name = false + message_per_request = false + multiline_processing_enabled = true + name = "firewall-events-source" + timezone = "Etc/UTC" + url = (known after apply) + use_autoline_matching = true } Plan: 2 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.

Assuming everything looks good, let’s execute the plan:

$ terraform apply -auto-approve sumologic_collector.collector: Creating... sumologic_collector.collector: Creation complete after 3s [id=108448215] sumologic_http_source.http_source: Creating... sumologic_http_source.http_source: Creation complete after 0s [id=150364538] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Success! At this point you could log into the Sumo Logic web interface and confirm that your Collector and HTTP Source were created successfully.

Create a Cloudflare Logpush Job

Before we’ll start sending logs to your collector, you need to demonstrate the ability to read from it. This validation step prevents accidental (or intentional) misconfigurations from overrunning your logs.

Tail the Sumo Logic Collector and await the challenge token

In a new shell window—you should keep the current one with your environment variables set for use with Terraform—we'll start tailing Sumo Logic for events sent from the firewall-events-source HTTP source.

The first time that you run livetail you'll need to specify your Sumo Logic Environment, Access ID and Access Key, but these values will be stored in the working directory for subsequent runs:

$ livetail _source=firewall-events-source ### Welcome to Sumo Logic Live Tail Command Line Interface ### 1 US1 2 US2 3 EU 4 AU 5 DE 6 FED 7 JP 8 CA Please select Sumo Logic environment: See http://help.sumologic.com/Send_Data/Collector_Management_API/Sumo_Logic_Endpoints to choose the correct environment. 3 ### Authenticating ### Please enter your Access ID: <access id> Please enter your Access Key <access key> ### Starting Live Tail session ###

Request and receive challenge token

Before requesting a challenge token, we need to figure out where Cloudflare should send logs.

We do this by asking Terraform for the receiver URL of the recently created HTTP source. Note that we modify the URL returned slightly as Cloudflare Logs expects sumo:// rather than https:// .

$ export SUMO_RECEIVER_URL=$(terraform state show sumologic_http_source.http_source | grep url | awk '{print $3}' | sed -e 's/https:/sumo:/; s/"//g') $ echo $SUMO_RECEIVER_URL sumo://endpoint1.collection.eu.sumologic.com/receiver/v1/http/<redacted>

With URL in hand, we can now request the token.

$ curl -sXPOST -H "Content-Type: application/json" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -d '{"destination_conf":"'''$SUMO_RECEIVER_URL'''"}' https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/logpush/ownership {"errors":[],"messages":[],"result":{"filename":"ownership-challenge-bb2912e0.txt","message":"","valid":true},"success":true}

Back in the other window where your livetail is running you should see something like this:

{"content":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwidHlwIjoiSldUIn0..WQhkW_EfxVy8p0BQ.oO6YEvfYFMHCTEd6D8MbmyjJqcrASDLRvHFTbZ5yUTMqBf1oniPNzo9Mn3ZzgTdayKg_jk0Gg-mBpdeqNI8LJFtUzzgTGU-aN1-haQlzmHVksEQdqawX7EZu2yiePT5QVk8RUsMRgloa76WANQbKghx1yivTZ3TGj8WquZELgnsiiQSvHqdFjAsiUJ0g73L962rDMJPG91cHuDqgfXWwSUqPsjVk88pmvGEEH4AMdKIol0EOc-7JIAWFBhcqmnv0uAXVOH5uXHHe_YNZ8PNLfYZXkw1xQlVDwH52wRC93ohIxg.pHAeaOGC8ALwLOXqxpXJgQ","filename":"ownership-challenge-bb2912e0.txt"}

Copy the content value from above into an environment variable, as you'll need it in a minute to create the job:

$ export LOGPUSH_CHALLENGE_TOKEN="<content value>"

Create the Logpush job using the challenge token

With challenge token in hand, we'll use Terraform to create the job.

First you’ll want to choose the log fields that should be sent to Sumo Logic. You can enumerate the list by querying the dataset:

$ curl -sXGET -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/logpush/datasets/firewall_events/fields | jq . { "errors": [], "messages": [], "result": { "Action": "string; the code of the first-class action the Cloudflare Firewall took on this request", "ClientASN": "int; the ASN number of the visitor", "ClientASNDescription": "string; the ASN of the visitor as string", "ClientCountryName": "string; country from which request originated", "ClientIP": "string; the visitor's IP address (IPv4 or IPv6)", "ClientIPClass": "string; the classification of the visitor's IP address, possible values are: unknown | clean | badHost | searchEngine | whitelist | greylist | monitoringService | securityScanner | noRecord | scan | backupService | mobilePlatform | tor", "ClientRefererHost": "string; the referer host", "ClientRefererPath": "string; the referer path requested by visitor", "ClientRefererQuery": "string; the referer query-string was requested by the visitor", "ClientRefererScheme": "string; the referer url scheme requested by the visitor", "ClientRequestHTTPHost": "string; the HTTP hostname requested by the visitor", "ClientRequestHTTPMethodName": "string; the HTTP method used by the visitor", "ClientRequestHTTPProtocol": "string; the version of HTTP protocol requested by the visitor", "ClientRequestPath": "string; the path requested by visitor", "ClientRequestQuery": "string; the query-string was requested by the visitor", "ClientRequestScheme": "string; the url scheme requested by the visitor", "Datetime": "int or string; the date and time the event occurred at the edge", "EdgeColoName": "string; the airport code of the Cloudflare datacenter that served this request", "EdgeResponseStatus": "int; HTTP response status code returned to browser", "Kind": "string; the kind of event, currently only possible values are: firewall", "MatchIndex": "int; rules match index in the chain", "Metadata": "object; additional product-specific information. Metadata is organized in key:value pairs. Key and Value formats can vary by Cloudflare security product and can change over time", "OriginResponseStatus": "int; HTTP origin response status code returned to browser", "OriginatorRayName": "string; the RayId of the request that issued the challenge/jschallenge", "RayName": "string; the RayId of the request", "RuleId": "string; the Cloudflare security product-specific RuleId triggered by this request", "Source": "string; the Cloudflare security product triggered by this request", "UserAgent": "string; visitor's user-agent string" }, "success": true }

Then you’ll append your Cloudflare configuration to the main.tf file:

$ cat <<EOF | tee -a main.tf ################## ### CLOUDFLARE ### ################## provider "cloudflare" { version = "~> 2.0" } resource "cloudflare_logpush_job" "firewall_events_job" { name = "fwevents-logpush-job" zone_id = var.cf_zone_id enabled = true dataset = "firewall_events" logpull_options = "fields=RayName,Source,RuleId,Action,EdgeResponseStatusDatetime,EdgeColoName,ClientIP,ClientCountryName,ClientASNDescription,UserAgent,ClientRequestHTTPMethodName,ClientRequestHTTPHost,ClientRequestHTTPPath×tamps=rfc3339" destination_conf = replace(sumologic_http_source.http_source.url,"https:","sumo:") ownership_challenge = "$LOGPUSH_CHALLENGE_TOKEN" } EOF

And add to the variables.tf file:

$ cat <<EOF | tee -a variables.tf ################## ### CLOUDFLARE ### ################## variable "cf_zone_id" { default = "$CF_ZONE_ID" }

Next we re-run terraform init to install the latest Cloudflare Terraform Provider Module. You’ll need to make sure you have at least version 2.6.0 as this is the version in which we added Logpush job support:

$ terraform init Initializing the backend... Initializing provider plugins... - Checking for available provider plugins... - Downloading plugin for provider "cloudflare" (terraform-providers/cloudflare) 2.6.0... Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.

With the latest Terraform installed, we check out the plan and then apply:

$ 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. sumologic_collector.collector: Refreshing state... [id=108448215] sumologic_http_source.http_source: Refreshing state... [id=150364538] ------------------------------------------------------------------------ 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_logpush_job.firewall_events_job will be created + resource "cloudflare_logpush_job" "firewall_events_job" { + dataset = "firewall_events" + destination_conf = "sumo://endpoint1.collection.eu.sumologic.com/receiver/v1/http/(redacted)" + enabled = true + id = (known after apply) + logpull_options = "fields=RayName,Source,RuleId,Action,EdgeResponseStatusDatetime,EdgeColoName,ClientIP,ClientCountryName,ClientASNDescription,UserAgent,ClientRequestHTTPMethodName,ClientRequestHTTPHost,ClientRequestHTTPPath×tamps=rfc3339" + name = "fwevents-logpush-job" + ownership_challenge = "(redacted)" + zone_id = "(redacted)" } 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.

$ terraform apply --auto-approve sumologic_collector.collector: Refreshing state... [id=108448215] sumologic_http_source.http_source: Refreshing state... [id=150364538] cloudflare_logpush_job.firewall_events_job: Creating... cloudflare_logpush_job.firewall_events_job: Creation complete after 3s [id=13746] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Success! Last step is to test your setup.

Testing your setup by sending a malicious request

The following step assumes that you have the Cloudflare WAF turned on. Alternatively, you can create a Firewall Rule to match your request and generate a Firewall Event that way.

First make sure that livetail is running as described earlier:

$ livetail "_source=firewall-events-source" ### Authenticating ### ### Starting Live Tail session ###

Then in a browser make the following request https://example.com/<script>alert()</script> . You should see the following returned:

And a few moments later in livetail:

{"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"958052","Action":"log","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"} {"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"958051","Action":"log","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"} {"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"973300","Action":"log","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"} {"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"973307","Action":"log","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"} {"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"973331","Action":"log","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"} {"RayName":"58830d3f9945bc36","Source":"waf","RuleId":"981176","Action":"drop","EdgeColoName":"LHR","ClientIP":"203.0.113.69","ClientCountryName":"gb","ClientASNDescription":"NTL","UserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36","ClientRequestHTTPMethodName":"GET","ClientRequestHTTPHost":"upinatoms.com"}

Note that for this one malicious request Cloudflare Logs actually sent 6 separate Firewall Events to Sumo Logic. The reason for this is that this specific request triggered a variety of different Managed Rules: #958051, 958052, 973300, 973307, 973331, and 981176.

Seeing it all in action