Bash Script to Enable Under-Attack in Cloudflare

From Brian Nelson Ramblings
Revision as of 01:36, 20 March 2026 by Brian (Talk | contribs) (Created page with "==Bash Script to Monitor PHP Process and Enable Under Attack Mode in Cloudflare== Does your domain hit max_children often due to high attacks from bots/scrapers? Did you kno...")

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Bash Script to Monitor PHP Process and Enable Under Attack Mode in Cloudflare

Does your domain hit max_children often due to high attacks from bots/scrapers?

Did you know you can enable under-attack via the api?

API Enable Under-Attack Mode via Cloudflare

#!/usr/bin/env bash
#
# cf-phpfpm-attack-toggle.sh
# Toggle Cloudflare Under Attack mode based on PHP-FPM max_children usage.

### CONFIG #########################################################

# Cloudflare
CF_API_TOKEN="[Input your API Token]"
CF_DOMAIN="[Input your Domain]"             # your domain
CF_NORMAL_LEVEL="medium"            # normal security level
CF_ATTACK_LEVEL="under_attack"      # attack mode level 

# PHP-FPM
PHP_FPM_POOL_CONF="[Input Pool Config Location]"  # adjust for your distro
PHP_FPM_PROCESS_NAME="php-fpm"              # e.g. php-fpm, php-fpm82, php-fpm8.2
PHP_USER="[Input your user]"

# Thresholds
HIGH_USAGE_PCT=90   # enable Under Attack if >= this % of max_children in use
LOW_USAGE_PCT=50    # disable Under Attack if <= this % of max_children in use

# State file to avoid flapping
STATE_FILE="/var/run/cf_under_attack.state"

#################################################################### 
 
set -euo pipefail

log() {
  echo "[$(date -Is)] $*"
}

get_zone_id() {
  curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${CF_DOMAIN}" \
   -H "Authorization: Bearer ${CF_API_TOKEN}" \
   -H "Content-Type: application/json" \
 | jq -r '.result[0].id'
}

get_cf_security_level() {
 local zone_id="$1"
 curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/settings/security_level" \
   -H "Authorization: Bearer ${CF_API_TOKEN}" \
   -H "Content-Type: application/json" \
 | jq -r '.result.value'
}

set_cf_security_level() {
  local zone_id="$1"
  local level="$2"
  curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/${zone_id}/settings/security_level" \
    -H "Authorization: Bearer ${CF_API_TOKEN}" \
    -H "Content-Type: application/json" \
    --data "{\"value\":\"${level}\"}" \
  | jq -r '.success'
}

get_max_children() {
  # reads first pm.max_children in pool conf
  awk -F'=' '/^space:*pm\.max_childrenspace:*=/ {gsub(/space:/, "", $2); print $2; exit}' \
    "${PHP_FPM_POOL_CONF}"
}

get_current_children_count() {
  # count running php-fpm workers (exclude master)
  #  ps -o cmd= -C "${PHP_FPM_PROCESS_NAME}" 2>/dev/null | grep "sitesent" | wc -l
  local count
  count=$(ps -o cmd= -C "${PHP_FPM_PROCESS_NAME}" 2>/dev/null | grep "${PHP_USER}" | wc -l)
  #Fix for math bug of if no process 0 are active
   echo $(( count + 1 ))
}

get_state() {
  if [[ -f "${STATE_FILE}" ]]; then
    cat "${STATE_FILE}"
  else
    echo "normal"
  fi
}

set_state() {
  echo "$1" > "${STATE_FILE}"
}

main() {
  # Get PHP-FPM usage
  local max_children
  max_children=$(get_max_children || echo 0)
  if [[ "${max_children}" -le 0 ]]; then
    log "ERROR: Could not determine pm.max_children from ${PHP_FPM_POOL_CONF}"
    exit 1
  fi

local current_children
current_children=$(get_current_children_count) 

  local usage_pct
  usage_pct=$(( current_children * 100 / max_children )) 

  log "PHP-FPM: ${current_children}/${max_children} children (${usage_pct}%)" 

  # Get Cloudflare zone and current level
  local zone_id
  zone_id=$(get_zone_id)
  if [[ -z "${zone_id}" || "${zone_id}" == "null" ]]; then
    log "ERROR: Unable to determine Cloudflare zone ID for ${CF_DOMAIN}"
    exit 1
  fi

  local current_level
  current_level=$(get_cf_security_level "${zone_id}")
  log "Cloudflare security level: ${current_level}"

  local state
  state=$(get_state)
  log "Current state flag: ${state}"

  # Logic:
  # - If usage >= HIGH_USAGE_PCT and not already under attack -> enable
  # - If usage <= LOW_USAGE_PCT and currently under attack      -> disable 

  if (( usage_pct >= HIGH_USAGE_PCT )) && [[ "${state}" != "attack" ]]; then
    log "High PHP-FPM usage (>= ${HIGH_USAGE_PCT}%). Enabling Under Attack mode..."
    if [[ "${current_level}" != "${CF_ATTACK_LEVEL}" ]]; then
      local ok
      ok=$(set_cf_security_level "${zone_id}" "${CF_ATTACK_LEVEL}")
      if [[ "${ok}" == "true" ]]; then
        log "Cloudflare set to ${CF_ATTACK_LEVEL}"
        set_state "attack"
      else
        log "ERROR: Failed to enable Under Attack mode"
      fi
    else
      log "Cloudflare already in ${CF_ATTACK_LEVEL} level."
      set_state "attack"
    fi

  elif (( usage_pct <= LOW_USAGE_PCT )) && [[ "${state}" == "attack" ]]; then
    log "PHP-FPM usage low (<= ${LOW_USAGE_PCT}%). Disabling Under Attack mode..."
    if [[ "${current_level}" != "${CF_NORMAL_LEVEL}" ]]; then
      local ok
      ok=$(set_cf_security_level "${zone_id}" "${CF_NORMAL_LEVEL}")
      if [[ "${ok}" == "true" ]]; then
        log "Cloudflare set to ${CF_NORMAL_LEVEL}"
        set_state "normal"
      else
        log "ERROR: Failed to disable Under Attack mode"
      fi
    else
      log "Cloudflare already at normal level (${CF_NORMAL_LEVEL})."
      set_state "normal"
    fi 

  else
    log "No change to Cloudflare mode (usage: ${usage_pct}%, state: ${state})."
  fi
}

main "$@"

Setup Cron to Auto Enable/Disable Under-Attack Mode in Cloudflare

Now set it up to run every minute

* * * * * bash /path/to/script/cf-phpfpm-attack-toggle.sh >> /var/log/cf-under-attack.log

Now when your site gets heavy traffic it will auto enable under-attack mode.

How to setup the API Key in Cloudflare

To generate the API key in cloudflare, you will need to go to

Manage Account -> Account API Tokens -> Create Token

Make sure to select Zone -> Zone Settings both (Edit/Read)

Those are the only settings this API token needs