Testing Next-Gen WAF Rate Limiting Rule with GitHub Actions

Brooks Cunningham

Senior Security Strategist, Fastly

In this blog post, we'll show you how to configure and test a rate limiting rule. Rate limiting is a technique used to control the number of requests that can be made to a given endpoint within a specified time period. This can be useful for preventing denial-of-service attacks (DDoS) and enforcing Terms of Service. In order to get started, you’ll need the following:

Prerequisites

  • A Fastly account, if you don’t already have an account, grab a free account here

  • A GitHub account

Create a rate limiting rule

Create a rule like the following screenshot. This rule will rate-limit on the request header “api-key”. When there are 5 or more requests with the same api-key within 1 minute window, then that api-key will be blocked.

Test the Rule with a Github Action

Now that the rule has been deployed, you can test it by making a series of requests to the protected endpoint. In this example, you will use a Github action to spin up a Next-Gen WAF Agent. With the same GitHub Action, you will send requests with curl to the Agent. If you make more requests than the specified limit, you should receive a 429 Too Many Requests error.

For the following GitHub Action to work you will need to set up your Next-Gen WAF Agent Access Key ID and Secret Access Key as Github Secrets.

name: Rate Limiting Demo

on:
  workflow_dispatch:

jobs:
  ngwaf-rate-limiting-demo:
    runs-on: ubuntu-latest
    environment: production
    timeout-minutes: 2
    services:
      sigsci:    
        image: signalsciences/sigsci-agent:latest
        env:
         SIGSCI_ACCESSKEYID: ${{ secrets.NGWAF_TERRAFORM_NGWAF_SITE_ACCESSKEYID }}
         SIGSCI_SECRETACCESSKEY: ${{ secrets.NGWAF_TERRAFORM_NGWAF_SITE_SECRETACCESSKEY }}
         SIGSCI_REVPROXY_LISTENER: "app1:{listener=http://0.0.0.0:8888,upstreams=https://http-me.edgecompute.app:443/,pass-host-header=false};"
         SIGSCI_UPLOAD_INTERVAL: "5s"
        ports:
          - 8888:8888

    steps:
      - name: Wait for NGWAF Agent
        run: |
          # Wait for agent to be ready
          sleep 5    

      - name: Send requests through NGWAF
        run: |
          echo "#### Sending requests for rate limiting test domains"
          for i in {1..10}; do
            echo "Request Number: $i"
            response=$(curl -si -H 'host:api.bubbs.coffee' -X POST 'http://0.0.0.0:8888/anything/start-ai-chat' -H api-key:gh-action-rl-demo)
            echo "$response"
            # Extract the HTTP status code from the first line (e.g., "HTTP/1.1 406 Not Acceptable")
            status=$(echo "$response" | head -n 1 | awk '{print $2}')
            if [ $i -gt 4 ] && [ "$status" != "406" ]; then
              echo "Error: Unexpected status code: $status on request $i"
              exit 1
            fi
            echo
          done

      - name: Wait for logs to upload
        run: |
          # Wait for agent to upload logs
          sleep 10

Example of failed GitHub Action output

Without the rule in place, you should see the job fail at the 5th request.

#### Sending requests for rate limiting test domains
Request Number: 1
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000049-IAD

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509c9f","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

Request Number: 2
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000057-IAD

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509ca0","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

... truncated for brevity

Request Number: 5
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000153-IAD

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509ca3","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Error: Unexpected status code: 200 on request 5

Example of successful GitHub Action output

Below is an example that shows a successful GitHub Action run. The block action 406 is returned on requests 5 through 10.

#### Sending requests for rate limiting test domains
Request Number: 1
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:19 GMT
X-Served-By: cache-chi-kigq8000027-CHI

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138fd","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

Request Number: 2
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138fe","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

Request Number: 3
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138ff","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

Request Number: 4
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI

{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e13900","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}

Request Number: 5
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 25 Feb 2025 20:59:20 GMT
Content-Length: 20

406 Not Acceptable

... truncated for brevity

Request Number: 10
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 25 Feb 2025 20:59:20 GMT
Content-Length: 20

406 Not Acceptable

Ensure security settings are validated

We've outlined a practical approach to testing rate-limiting rules in Fastly's Next-Gen WAF using GitHub Actions. This method allows for automated validation of these rules, crucial for defending against cyber attacks and enforcing usage policies. Watch the demo for a hands-on example: Github Action and NGWAF Rate Limiting Demo.