From 88caed8bb86c5f3cc901b4281a9b21e4a26899b4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Sakhamuru Date: Thu, 26 Feb 2026 09:36:36 +0530 Subject: [PATCH 1/2] Import Export cloudflare rules --- ansible/cloudflare/cloudflare_rules.sh | 122 +++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 ansible/cloudflare/cloudflare_rules.sh diff --git a/ansible/cloudflare/cloudflare_rules.sh b/ansible/cloudflare/cloudflare_rules.sh new file mode 100644 index 0000000..c4a11b3 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# =============================================== +# Cloudflare Rules Export / Import Script +# =============================================== +# Usage: +# Export rules: +# ./cloudflare_rules.sh export +# +# Import rules: +# ./cloudflare_rules.sh import +# +# Alternatively, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID +# environment variables and run: +# ./cloudflare_rules.sh export +# +# =============================================== + +# === CONFIGURATION === +API_TOKEN="${2:-$CLOUDFLARE_API_TOKEN}" +ZONE_ID="${3:-$CLOUDFLARE_ZONE_ID}" + +usage() { + echo "Usage: $0 {export|import} [API_TOKEN] [ZONE_ID]" + echo "" + echo "Arguments:" + echo " export/import Operation to perform" + echo " API_TOKEN Cloudflare API Token (optional if CLOUDFLARE_API_TOKEN env var is set)" + echo " ZONE_ID Cloudflare Zone ID (optional if CLOUDFLARE_ZONE_ID env var is set)" + exit 1 +} + +if [[ -z "$API_TOKEN" || -z "$ZONE_ID" ]]; then + echo "Error: API_TOKEN and ZONE_ID must be provided either as arguments or environment variables." + usage +fi + +BASE_URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" +HEADERS=(-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json") + +# Helper to perform curl with error checking +call_cloudflare() { + local method=$1 + local url=$2 + local output_file=$3 + local data_file=$4 + local description=$5 + + echo "$description..." + + local curl_opts=(-s -w "\n%{http_code}" -X "$method" "$url" "${HEADERS[@]}") + + local full_response + if [[ -n "$output_file" ]]; then + # For export + full_response=$(curl "${curl_opts[@]}" -o "$output_file") + elif [[ -n "$data_file" ]]; then + # For import + if [[ ! -f "$data_file" ]]; then + echo "Error: Data file $data_file not found. Skipping $description." + return 1 + fi + full_response=$(curl "${curl_opts[@]}" --data @"$data_file") + fi + + local http_code + http_code=$(echo "$full_response" | tail -n1) + + if [[ "$http_code" -lt 200 || "$http_code" -gt 299 ]]; then + echo "Error: $description failed with HTTP status $http_code" + if [[ -n "$full_response" && "$full_response" != "$http_code" ]]; then + echo "Response: $(echo "$full_response" | head -n -1)" + fi + return 1 + fi + return 0 +} + +# === EXPORT FUNCTION === +export_rules() { + local failed=0 + call_cloudflare "GET" "$BASE_URL/phases/http_request_firewall_custom/entrypoint" "waf_rules.json" "" "Exporting WAF Custom Rules" || failed=1 + call_cloudflare "GET" "$BASE_URL/phases/http_ratelimit/entrypoint" "rate_limit_rules.json" "" "Exporting Rate Limiting Rules" || failed=1 + call_cloudflare "GET" "$BASE_URL/phases/http_request_cache_settings/entrypoint" "cache_rules.json" "" "Exporting Cache Rules" || failed=1 + call_cloudflare "GET" "$BASE_URL/phases/http_request_redirect/entrypoint" "redirect_rules.json" "" "Exporting Redirect Rules" || failed=1 + + if [[ $failed -eq 0 ]]; then + echo "Export completed successfully. JSON files saved in current directory." + else + echo "Export completed with errors." + exit 1 + fi +} + +# === IMPORT FUNCTION === +import_rules() { + local failed=0 + call_cloudflare "PUT" "$BASE_URL/phases/http_request_firewall_custom/entrypoint" "" "waf_rules.json" "Importing WAF Custom Rules" || failed=1 + call_cloudflare "PUT" "$BASE_URL/phases/http_ratelimit/entrypoint" "" "rate_limit_rules.json" "Importing Rate Limiting Rules" || failed=1 + call_cloudflare "PUT" "$BASE_URL/phases/http_request_cache_settings/entrypoint" "" "cache_rules.json" "Importing Cache Rules" || failed=1 + call_cloudflare "PUT" "$BASE_URL/phases/http_request_redirect/entrypoint" "" "redirect_rules.json" "Importing Redirect Rules" || failed=1 + + if [[ $failed -eq 0 ]]; then + echo "Import completed successfully." + else + echo "Import completed with errors." + exit 1 + fi +} + +# === MAIN === +case "$1" in + export) + export_rules + ;; + import) + import_rules + ;; + *) + usage + ;; +esac From 9b4a82d4d051ccd2d2bbd6e64f5e5452d6eb153f Mon Sep 17 00:00:00 2001 From: Ramakrishna Sakhamuru Date: Mon, 4 May 2026 12:00:22 +0530 Subject: [PATCH 2/2] Updated code to Import Export cloudflare rules --- ansible/cloudflare/cloudflare_rules.py | 177 ++++++++++++++++++ ansible/cloudflare/cloudflare_rules.sh | 122 ------------ .../cloudflare_rules/cache_rules.json | 10 + .../cloudflare_rules/page_rules.json | 1 + .../cloudflare_rules/rate_limit_rules.json | 36 ++++ .../cloudflare_rules/redirect_rules.json | 10 + .../cloudflare_rules/waf_rules.json | 35 ++++ 7 files changed, 269 insertions(+), 122 deletions(-) create mode 100644 ansible/cloudflare/cloudflare_rules.py delete mode 100644 ansible/cloudflare/cloudflare_rules.sh create mode 100644 ansible/cloudflare/cloudflare_rules/cache_rules.json create mode 100644 ansible/cloudflare/cloudflare_rules/page_rules.json create mode 100644 ansible/cloudflare/cloudflare_rules/rate_limit_rules.json create mode 100644 ansible/cloudflare/cloudflare_rules/redirect_rules.json create mode 100644 ansible/cloudflare/cloudflare_rules/waf_rules.json diff --git a/ansible/cloudflare/cloudflare_rules.py b/ansible/cloudflare/cloudflare_rules.py new file mode 100644 index 0000000..008794e --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +""" +Cloudflare Rules Export / Import Script + +Usage: + Export rules: + python cloudflare_rules.py export + + Import rules: + python cloudflare_rules.py import + + Alternatively, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID + environment variables and run: + python cloudflare_rules.py export +""" + +import json +import os +import sys + +import requests + + +def usage(): + print("Usage: python cloudflare_rules.py {export|import} [API_TOKEN] [ZONE_ID]") + print() + print("Arguments:") + print(" export/import Operation to perform") + print(" API_TOKEN Cloudflare API Token (optional if CLOUDFLARE_API_TOKEN env var is set)") + print(" ZONE_ID Cloudflare Zone ID (optional if CLOUDFLARE_ZONE_ID env var is set)") + sys.exit(1) + + +def call_cloudflare(method, url, api_token, output_file=None, data_file=None, description=""): + print(f"{description}...") + + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + data = None + if data_file: + if not os.path.isfile(data_file): + print(f"Error: Data file {data_file} not found. Skipping {description}.") + return False + with open(data_file, "r") as f: + data = f.read() + + response = requests.request(method, url, headers=headers, data=data) + + if response.status_code < 200 or response.status_code > 299: + print(f"Error: {description} failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + return False + + if output_file: + with open(output_file, "w") as f: + f.write(response.text) + + return True + + +def export_rules(api_token, zone_id): + base_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + pagerules_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/pagerules" + + failed = False + if not call_cloudflare("GET", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, output_file="waf_rules.json", description="Exporting WAF Custom Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, output_file="rate_limit_rules.json", description="Exporting Rate Limiting Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, output_file="cache_rules.json", description="Exporting Cache Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, output_file="redirect_rules.json", description="Exporting Redirect Rules"): + failed = True + if not call_cloudflare("GET", pagerules_url, api_token, output_file="page_rules.json", description="Exporting Page Rules"): + failed = True + + if not failed: + print("Export completed successfully. JSON files saved in current directory.") + else: + print("Export completed with errors.") + sys.exit(1) + + +def import_page_rules(api_token, zone_id): + pagerules_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/pagerules" + + print("Importing Page Rules...") + if not os.path.isfile("page_rules.json"): + print("Error: Data file page_rules.json not found. Skipping Page Rules import.") + return False + + with open("page_rules.json", "r") as f: + data = json.load(f) + + rules = data.get("result", []) + if not rules: + print("No page rules found to import.") + return True + + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + page_failed = False + for i, rule in enumerate(rules): + payload = { + "targets": rule["targets"], + "actions": rule["actions"], + "priority": rule.get("priority", i + 1), + "status": rule.get("status", "active"), + } + + response = requests.post(pagerules_url, headers=headers, json=payload) + + if response.status_code < 200 or response.status_code > 299: + print(f"Error: Importing Page Rule {i} failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + page_failed = True + + if page_failed: + return False + + print("Page Rules imported successfully.") + return True + + +def import_rules(api_token, zone_id): + base_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + + failed = False + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, data_file="waf_rules.json", description="Importing WAF Custom Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, data_file="rate_limit_rules.json", description="Importing Rate Limiting Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, data_file="cache_rules.json", description="Importing Cache Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, data_file="redirect_rules.json", description="Importing Redirect Rules"): + failed = True + if not import_page_rules(api_token, zone_id): + failed = True + + if not failed: + print("Import completed successfully.") + else: + print("Import completed with errors.") + sys.exit(1) + + +def main(): + if len(sys.argv) < 2: + usage() + + operation = sys.argv[1] + api_token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("CLOUDFLARE_API_TOKEN", "") + zone_id = sys.argv[3] if len(sys.argv) > 3 else os.environ.get("CLOUDFLARE_ZONE_ID", "") + + if not api_token or not zone_id: + print("Error: API_TOKEN and ZONE_ID must be provided either as arguments or environment variables.") + usage() + + if operation == "export": + export_rules(api_token, zone_id) + elif operation == "import": + import_rules(api_token, zone_id) + else: + usage() + + +if __name__ == "__main__": + main() diff --git a/ansible/cloudflare/cloudflare_rules.sh b/ansible/cloudflare/cloudflare_rules.sh deleted file mode 100644 index c4a11b3..0000000 --- a/ansible/cloudflare/cloudflare_rules.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/bash - -# =============================================== -# Cloudflare Rules Export / Import Script -# =============================================== -# Usage: -# Export rules: -# ./cloudflare_rules.sh export -# -# Import rules: -# ./cloudflare_rules.sh import -# -# Alternatively, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID -# environment variables and run: -# ./cloudflare_rules.sh export -# -# =============================================== - -# === CONFIGURATION === -API_TOKEN="${2:-$CLOUDFLARE_API_TOKEN}" -ZONE_ID="${3:-$CLOUDFLARE_ZONE_ID}" - -usage() { - echo "Usage: $0 {export|import} [API_TOKEN] [ZONE_ID]" - echo "" - echo "Arguments:" - echo " export/import Operation to perform" - echo " API_TOKEN Cloudflare API Token (optional if CLOUDFLARE_API_TOKEN env var is set)" - echo " ZONE_ID Cloudflare Zone ID (optional if CLOUDFLARE_ZONE_ID env var is set)" - exit 1 -} - -if [[ -z "$API_TOKEN" || -z "$ZONE_ID" ]]; then - echo "Error: API_TOKEN and ZONE_ID must be provided either as arguments or environment variables." - usage -fi - -BASE_URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" -HEADERS=(-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json") - -# Helper to perform curl with error checking -call_cloudflare() { - local method=$1 - local url=$2 - local output_file=$3 - local data_file=$4 - local description=$5 - - echo "$description..." - - local curl_opts=(-s -w "\n%{http_code}" -X "$method" "$url" "${HEADERS[@]}") - - local full_response - if [[ -n "$output_file" ]]; then - # For export - full_response=$(curl "${curl_opts[@]}" -o "$output_file") - elif [[ -n "$data_file" ]]; then - # For import - if [[ ! -f "$data_file" ]]; then - echo "Error: Data file $data_file not found. Skipping $description." - return 1 - fi - full_response=$(curl "${curl_opts[@]}" --data @"$data_file") - fi - - local http_code - http_code=$(echo "$full_response" | tail -n1) - - if [[ "$http_code" -lt 200 || "$http_code" -gt 299 ]]; then - echo "Error: $description failed with HTTP status $http_code" - if [[ -n "$full_response" && "$full_response" != "$http_code" ]]; then - echo "Response: $(echo "$full_response" | head -n -1)" - fi - return 1 - fi - return 0 -} - -# === EXPORT FUNCTION === -export_rules() { - local failed=0 - call_cloudflare "GET" "$BASE_URL/phases/http_request_firewall_custom/entrypoint" "waf_rules.json" "" "Exporting WAF Custom Rules" || failed=1 - call_cloudflare "GET" "$BASE_URL/phases/http_ratelimit/entrypoint" "rate_limit_rules.json" "" "Exporting Rate Limiting Rules" || failed=1 - call_cloudflare "GET" "$BASE_URL/phases/http_request_cache_settings/entrypoint" "cache_rules.json" "" "Exporting Cache Rules" || failed=1 - call_cloudflare "GET" "$BASE_URL/phases/http_request_redirect/entrypoint" "redirect_rules.json" "" "Exporting Redirect Rules" || failed=1 - - if [[ $failed -eq 0 ]]; then - echo "Export completed successfully. JSON files saved in current directory." - else - echo "Export completed with errors." - exit 1 - fi -} - -# === IMPORT FUNCTION === -import_rules() { - local failed=0 - call_cloudflare "PUT" "$BASE_URL/phases/http_request_firewall_custom/entrypoint" "" "waf_rules.json" "Importing WAF Custom Rules" || failed=1 - call_cloudflare "PUT" "$BASE_URL/phases/http_ratelimit/entrypoint" "" "rate_limit_rules.json" "Importing Rate Limiting Rules" || failed=1 - call_cloudflare "PUT" "$BASE_URL/phases/http_request_cache_settings/entrypoint" "" "cache_rules.json" "Importing Cache Rules" || failed=1 - call_cloudflare "PUT" "$BASE_URL/phases/http_request_redirect/entrypoint" "" "redirect_rules.json" "Importing Redirect Rules" || failed=1 - - if [[ $failed -eq 0 ]]; then - echo "Import completed successfully." - else - echo "Import completed with errors." - exit 1 - fi -} - -# === MAIN === -case "$1" in - export) - export_rules - ;; - import) - import_rules - ;; - *) - usage - ;; -esac diff --git a/ansible/cloudflare/cloudflare_rules/cache_rules.json b/ansible/cloudflare/cloudflare_rules/cache_rules.json new file mode 100644 index 0000000..bcb1286 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/cache_rules.json @@ -0,0 +1,10 @@ +{ + "result": null, + "success": false, + "errors": [ + { + "message": "request is not authorized" + } + ], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/page_rules.json b/ansible/cloudflare/cloudflare_rules/page_rules.json new file mode 100644 index 0000000..ac5e026 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/page_rules.json @@ -0,0 +1 @@ +{"result":[{"id":"380581670412fcfb02a75e0f049f84fe","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/oai*"}}],"actions":[{"id":"browser_check","value":"off"},{"id":"security_level","value":"essentially_off"},{"id":"cache_level","value":"bypass"}],"priority":3,"status":"active","created_on":"2019-04-03T06:14:26.000000Z","modified_on":"2022-07-14T12:07:36.000000Z"},{"id":"56386c243bba2ac9c67cb21173da335e","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/api*"}}],"actions":[{"id":"browser_check","value":"off"},{"id":"security_level","value":"essentially_off"},{"id":"cache_level","value":"bypass"}],"priority":2,"status":"active","created_on":"2019-02-26T15:20:15.000000Z","modified_on":"2022-07-15T09:47:38.000000Z"},{"id":"8ed1bac74f92fef5bada7e4091897453","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/query*"}}],"actions":[{"id":"browser_cache_ttl","value":1800},{"id":"cache_level","value":"cache_everything"}],"priority":1,"status":"active","created_on":"2019-02-26T15:21:39.000000Z","modified_on":"2019-02-27T22:27:21.000000Z"}],"success":true,"errors":[],"messages":[]} \ No newline at end of file diff --git a/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json b/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json new file mode 100644 index 0000000..30fbbfa --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json @@ -0,0 +1,36 @@ +{ + "result": { + "description": "", + "id": "ffbc4d7fcdae4cd3a36aaaa9ac921483", + "kind": "zone", + "last_updated": "2025-07-09T09:18:41.981517Z", + "name": "default", + "phase": "http_ratelimit", + "rules": [ + { + "action": "block", + "description": "Leaked credential check", + "enabled": true, + "expression": "(cf.waf.credential_check.password_leaked)", + "id": "e254b99c19e746ce9b0d9e33eaff8992", + "last_updated": "2025-07-09T09:18:41.981517Z", + "ratelimit": { + "characteristics": [ + "ip.src", + "cf.colo.id" + ], + "mitigation_timeout": 3600, + "period": 10, + "requests_per_period": 5 + }, + "ref": "e254b99c19e746ce9b0d9e33eaff8992", + "version": "1" + } + ], + "source": "rate_limit", + "version": "1" + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/redirect_rules.json b/ansible/cloudflare/cloudflare_rules/redirect_rules.json new file mode 100644 index 0000000..fc06c29 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/redirect_rules.json @@ -0,0 +1,10 @@ +{ + "result": null, + "success": false, + "errors": [ + { + "message": "phase \"http_request_redirect\" not allowed at zone level" + } + ], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/waf_rules.json b/ansible/cloudflare/cloudflare_rules/waf_rules.json new file mode 100644 index 0000000..c066812 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/waf_rules.json @@ -0,0 +1,35 @@ +{ + "result": { + "description": "", + "id": "3d79da9812e44c449dde54903717be8a", + "kind": "zone", + "last_updated": "2025-05-20T10:09:27.585167Z", + "name": "default", + "phase": "http_request_firewall_custom", + "rules": [ + { + "action": "skip", + "action_parameters": { + "phases": [ + "http_request_firewall_managed" + ] + }, + "description": "Allow Blog uploads", + "enabled": true, + "expression": "(http.request.full_uri contains \"blog.doaj.org/wp-admin/\")", + "id": "b47819cd21af43f09d0d9b972755b749", + "last_updated": "2025-05-20T10:09:27.585167Z", + "logging": { + "enabled": true + }, + "ref": "b47819cd21af43f09d0d9b972755b749", + "version": "1" + } + ], + "source": "firewall_custom", + "version": "1" + }, + "success": true, + "errors": [], + "messages": [] +}