From 88caed8bb86c5f3cc901b4281a9b21e4a26899b4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Sakhamuru Date: Thu, 26 Feb 2026 09:36:36 +0530 Subject: [PATCH 1/4] 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/4] 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": [] +} From efc8f4d04a435dca0cc3e163e7f8fd8ed4f68f3f Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 19 May 2026 15:36:46 +0100 Subject: [PATCH 3/4] moved base dir for cloudflare rules --- {ansible/cloudflare => cloudflare}/cloudflare_rules.py | 0 .../cloudflare => cloudflare}/cloudflare_rules/cache_rules.json | 0 .../cloudflare => cloudflare}/cloudflare_rules/page_rules.json | 0 .../cloudflare_rules/rate_limit_rules.json | 0 .../cloudflare_rules/redirect_rules.json | 0 .../cloudflare => cloudflare}/cloudflare_rules/waf_rules.json | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {ansible/cloudflare => cloudflare}/cloudflare_rules.py (100%) rename {ansible/cloudflare => cloudflare}/cloudflare_rules/cache_rules.json (100%) rename {ansible/cloudflare => cloudflare}/cloudflare_rules/page_rules.json (100%) rename {ansible/cloudflare => cloudflare}/cloudflare_rules/rate_limit_rules.json (100%) rename {ansible/cloudflare => cloudflare}/cloudflare_rules/redirect_rules.json (100%) rename {ansible/cloudflare => cloudflare}/cloudflare_rules/waf_rules.json (100%) diff --git a/ansible/cloudflare/cloudflare_rules.py b/cloudflare/cloudflare_rules.py similarity index 100% rename from ansible/cloudflare/cloudflare_rules.py rename to cloudflare/cloudflare_rules.py diff --git a/ansible/cloudflare/cloudflare_rules/cache_rules.json b/cloudflare/cloudflare_rules/cache_rules.json similarity index 100% rename from ansible/cloudflare/cloudflare_rules/cache_rules.json rename to cloudflare/cloudflare_rules/cache_rules.json diff --git a/ansible/cloudflare/cloudflare_rules/page_rules.json b/cloudflare/cloudflare_rules/page_rules.json similarity index 100% rename from ansible/cloudflare/cloudflare_rules/page_rules.json rename to cloudflare/cloudflare_rules/page_rules.json diff --git a/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json b/cloudflare/cloudflare_rules/rate_limit_rules.json similarity index 100% rename from ansible/cloudflare/cloudflare_rules/rate_limit_rules.json rename to cloudflare/cloudflare_rules/rate_limit_rules.json diff --git a/ansible/cloudflare/cloudflare_rules/redirect_rules.json b/cloudflare/cloudflare_rules/redirect_rules.json similarity index 100% rename from ansible/cloudflare/cloudflare_rules/redirect_rules.json rename to cloudflare/cloudflare_rules/redirect_rules.json diff --git a/ansible/cloudflare/cloudflare_rules/waf_rules.json b/cloudflare/cloudflare_rules/waf_rules.json similarity index 100% rename from ansible/cloudflare/cloudflare_rules/waf_rules.json rename to cloudflare/cloudflare_rules/waf_rules.json From eac1a557cbbee452bbb825c86c4003969a349ba2 Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 19 May 2026 17:48:21 +0100 Subject: [PATCH 4/4] adds dry-run parameter and additional exported settings --- cloudflare/README.md | 50 ++++ cloudflare/cloudflare_rules.py | 214 ++++++++++++-- cloudflare/cloudflare_rules/dns_records.json | 272 ++++++++++++++++++ .../cloudflare_rules/zone_settings.json | 1 + 4 files changed, 512 insertions(+), 25 deletions(-) create mode 100644 cloudflare/README.md create mode 100644 cloudflare/cloudflare_rules/dns_records.json create mode 100644 cloudflare/cloudflare_rules/zone_settings.json diff --git a/cloudflare/README.md b/cloudflare/README.md new file mode 100644 index 0000000..fc889b4 --- /dev/null +++ b/cloudflare/README.md @@ -0,0 +1,50 @@ +# Cloudflare Rules Export / Import + +`cloudflare_rules.py` exports and imports Cloudflare zone configuration as JSON files into the `cloudflare_rules/` directory. + +## Usage + +```bash +python cloudflare_rules.py export +python cloudflare_rules.py import +``` + +Alternatively, set environment variables and omit the arguments: + +```bash +export CLOUDFLARE_API_TOKEN=your_token +export CLOUDFLARE_ZONE_ID=your_zone_id + +python cloudflare_rules.py export +python cloudflare_rules.py import +``` + +## API Token Requirements + +Create a **zone-scoped** API token (not an account token) in the Cloudflare dashboard under **My Profile > API Tokens > Create Token**. + +Scope the token to the specific zone, with the following permissions: + +| Category | Permission | Required for | +|----------|------------|--------------| +| Zone / DNS | Read | DNS records export/import | +| Zone / Zone Settings | Read | Zone settings export | +| Zone / Zone WAF | Edit | WAF custom rules export/import | +| Zone / Cache Rules | Edit | Cache rules export/import | +| Zone / Page Rules | Edit | Page rules export/import | + +> **Note:** Import operations require Edit permissions; Read is sufficient for export-only use. + +## What is exported + +| File | Contents | +|------|----------| +| `dns_records.json` | All DNS records for the zone | +| `zone_settings.json` | Zone-level settings (SSL mode, security level, etc.) | +| `waf_rules.json` | WAF custom rules | +| `rate_limit_rules.json` | Rate limiting rules | +| `cache_rules.json` | Cache rules | +| `redirect_rules.json` | Dynamic redirect rules | +| `page_rules.json` | Page rules (legacy) | + +Phases with no rules configured are skipped without error. diff --git a/cloudflare/cloudflare_rules.py b/cloudflare/cloudflare_rules.py index 008794e..a6a538f 100644 --- a/cloudflare/cloudflare_rules.py +++ b/cloudflare/cloudflare_rules.py @@ -10,6 +10,9 @@ Import rules: python cloudflare_rules.py import + Dry-run import (prints payloads without making any changes): + python cloudflare_rules.py import --dry-run + Alternatively, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID environment variables and run: python cloudflare_rules.py export @@ -21,18 +24,21 @@ import requests +RULES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cloudflare_rules") + def usage(): - print("Usage: python cloudflare_rules.py {export|import} [API_TOKEN] [ZONE_ID]") + print("Usage: python cloudflare_rules.py {export|import} [API_TOKEN] [ZONE_ID] [--dry-run]") 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)") + print(" --dry-run Print what would be sent without making any changes (import only)") sys.exit(1) -def call_cloudflare(method, url, api_token, output_file=None, data_file=None, description=""): +def call_cloudflare(method, url, api_token, output_file=None, data_file=None, description="", dry_run=False): print(f"{description}...") headers = { @@ -48,8 +54,24 @@ def call_cloudflare(method, url, api_token, output_file=None, data_file=None, de with open(data_file, "r") as f: data = f.read() + if dry_run: + print(f" [DRY RUN] {method} {url}") + if data: + parsed = json.loads(data) + print(f" Payload: {json.dumps(parsed, indent=2)}") + return True + response = requests.request(method, url, headers=headers, data=data) + if response.status_code == 404: + try: + errors = response.json().get("errors", []) + if any(e.get("code") == 10003 for e in errors): + print(f" No rules present, skipping.") + return True + except Exception: + pass + if response.status_code < 200 or response.status_code > 299: print(f"Error: {description} failed with HTTP status {response.status_code}") if response.text: @@ -63,38 +85,164 @@ def call_cloudflare(method, url, api_token, output_file=None, data_file=None, de return True +def export_dns_records(api_token, zone_id): + print("Exporting DNS Records...") + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + all_records = [] + page = 1 + while True: + response = requests.get(url, headers=headers, params={"page": page, "per_page": 100}) + if response.status_code < 200 or response.status_code > 299: + print(f"Error: Exporting DNS Records failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + return False + + data = response.json() + all_records.extend(data.get("result", [])) + + result_info = data.get("result_info", {}) + if page >= result_info.get("total_pages", 1): + break + page += 1 + + with open(os.path.join(RULES_DIR, "dns_records.json"), "w") as f: + json.dump({"result": all_records}, f, indent=2) + + print(f" Exported {len(all_records)} DNS records.") + return True + + +def import_dns_records(api_token, zone_id, dry_run=False): + print("Importing DNS Records...") + dns_file = os.path.join(RULES_DIR, "dns_records.json") + if not os.path.isfile(dns_file): + print(f"Error: Data file {dns_file} not found. Skipping DNS Records import.") + return False + + with open(dns_file, "r") as f: + data = json.load(f) + + records = data.get("result", []) + if not records: + print("No DNS records found to import.") + return True + + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + exclude_fields = {"id", "zone_id", "zone_name", "created_on", "modified_on", "meta"} + + failed = False + for i, record in enumerate(records): + payload = {k: v for k, v in record.items() if k not in exclude_fields} + if dry_run: + print(f" [DRY RUN] POST {url}") + print(f" Payload: {json.dumps(payload, indent=2)}") + continue + response = requests.post(url, headers=headers, json=payload) + if response.status_code < 200 or response.status_code > 299: + print(f"Error: Importing DNS record {i} ({record.get('name', '')}) failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + failed = True + + if failed: + return False + + print("DNS Records imported successfully." if not dry_run else f" {len(records)} DNS records would be imported.") + return True + + +def import_zone_settings(api_token, zone_id, dry_run=False): + print("Importing Zone Settings...") + settings_file = os.path.join(RULES_DIR, "zone_settings.json") + if not os.path.isfile(settings_file): + print(f"Error: Data file {settings_file} not found. Skipping Zone Settings import.") + return False + + with open(settings_file, "r") as f: + data = json.load(f) + + items = [ + {"id": s["id"], "value": s["value"]} + for s in data.get("result", []) + if s.get("editable", False) + ] + + if not items: + print("No editable zone settings found to import.") + return True + + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/settings" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + if dry_run: + print(f" [DRY RUN] PATCH {url}") + print(f" Payload: {json.dumps({'items': items}, indent=2)}") + return True + + response = requests.patch(url, headers=headers, json={"items": items}) + if response.status_code < 200 or response.status_code > 299: + print(f"Error: Importing Zone Settings failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + return False + + print("Zone Settings imported successfully.") + 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" + os.makedirs(RULES_DIR, exist_ok=True) + 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"): + if not export_dns_records(api_token, zone_id): + failed = True + if not call_cloudflare("GET", f"https://api.cloudflare.com/client/v4/zones/{zone_id}/settings", api_token, output_file=os.path.join(RULES_DIR, "zone_settings.json"), description="Exporting Zone Settings"): 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"): + if not call_cloudflare("GET", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, output_file=os.path.join(RULES_DIR, "waf_rules.json"), description="Exporting WAF Custom 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"): + if not call_cloudflare("GET", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, output_file=os.path.join(RULES_DIR, "rate_limit_rules.json"), description="Exporting Rate Limiting 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"): + if not call_cloudflare("GET", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, output_file=os.path.join(RULES_DIR, "cache_rules.json"), description="Exporting Cache Rules"): failed = True - if not call_cloudflare("GET", pagerules_url, api_token, output_file="page_rules.json", description="Exporting Page Rules"): + if not call_cloudflare("GET", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, output_file=os.path.join(RULES_DIR, "redirect_rules.json"), description="Exporting Redirect Rules"): + failed = True + if not call_cloudflare("GET", pagerules_url, api_token, output_file=os.path.join(RULES_DIR, "page_rules.json"), description="Exporting Page Rules"): failed = True if not failed: - print("Export completed successfully. JSON files saved in current directory.") + print(f"Export completed successfully. JSON files saved in {RULES_DIR}.") else: print("Export completed with errors.") sys.exit(1) -def import_page_rules(api_token, zone_id): +def import_page_rules(api_token, zone_id, dry_run=False): 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.") + page_rules_file = os.path.join(RULES_DIR, "page_rules.json") + if not os.path.isfile(page_rules_file): + print(f"Error: Data file {page_rules_file} not found. Skipping Page Rules import.") return False - with open("page_rules.json", "r") as f: + with open(page_rules_file, "r") as f: data = json.load(f) rules = data.get("result", []) @@ -116,6 +264,11 @@ def import_page_rules(api_token, zone_id): "status": rule.get("status", "active"), } + if dry_run: + print(f" [DRY RUN] POST {pagerules_url}") + print(f" Payload: {json.dumps(payload, indent=2)}") + continue + response = requests.post(pagerules_url, headers=headers, json=payload) if response.status_code < 200 or response.status_code > 299: @@ -127,29 +280,36 @@ def import_page_rules(api_token, zone_id): if page_failed: return False - print("Page Rules imported successfully.") + print("Page Rules imported successfully." if not dry_run else f" {len(rules)} page rules would be imported.") return True -def import_rules(api_token, zone_id): +def import_rules(api_token, zone_id, dry_run=False): base_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + if dry_run: + print("--- DRY RUN: no changes will be made ---") + 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"): + if not import_dns_records(api_token, zone_id, dry_run=dry_run): + failed = True + if not import_zone_settings(api_token, zone_id, dry_run=dry_run): 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"): + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, data_file=os.path.join(RULES_DIR, "waf_rules.json"), description="Importing WAF Custom Rules", dry_run=dry_run): 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"): + if not call_cloudflare("PUT", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, data_file=os.path.join(RULES_DIR, "rate_limit_rules.json"), description="Importing Rate Limiting Rules", dry_run=dry_run): 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"): + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, data_file=os.path.join(RULES_DIR, "cache_rules.json"), description="Importing Cache Rules", dry_run=dry_run): failed = True - if not import_page_rules(api_token, zone_id): + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, data_file=os.path.join(RULES_DIR, "redirect_rules.json"), description="Importing Redirect Rules", dry_run=dry_run): + failed = True + if not import_page_rules(api_token, zone_id, dry_run=dry_run): failed = True if not failed: - print("Import completed successfully.") + print("Dry run completed successfully." if dry_run else "Import completed successfully.") else: - print("Import completed with errors.") + print("Dry run completed with errors." if dry_run else "Import completed with errors.") sys.exit(1) @@ -157,9 +317,13 @@ 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", "") + args = sys.argv[1:] + dry_run = "--dry-run" in args + args = [a for a in args if a != "--dry-run"] + + operation = args[0] if args else None + api_token = args[1] if len(args) > 1 else os.environ.get("CLOUDFLARE_API_TOKEN", "") + zone_id = args[2] if len(args) > 2 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.") @@ -168,7 +332,7 @@ def main(): if operation == "export": export_rules(api_token, zone_id) elif operation == "import": - import_rules(api_token, zone_id) + import_rules(api_token, zone_id, dry_run=dry_run) else: usage() diff --git a/cloudflare/cloudflare_rules/dns_records.json b/cloudflare/cloudflare_rules/dns_records.json new file mode 100644 index 0000000..fe97801 --- /dev/null +++ b/cloudflare/cloudflare_rules/dns_records.json @@ -0,0 +1,272 @@ +{ + "result": [ + { + "id": "010ff13a3227891727529ac477d9ff71", + "name": "blog.doaj.org", + "type": "A", + "content": "18.171.142.0", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": "Switchover to new blog 2024-03-27 doajPM issue 3846, proxied 2025-04-29", + "tags": [], + "created_on": "2024-03-27T15:36:53.800738Z", + "modified_on": "2025-07-09T12:39:38.660026Z", + "comment_modified_on": "2025-04-29T10:20:21.335214Z" + }, + { + "id": "fb21fa6cb2e87eeb25a7579796f17655", + "name": "doaj.org", + "type": "A", + "content": "46.101.64.14", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-27T22:26:14.802855Z", + "modified_on": "2019-02-27T22:26:14.802855Z" + }, + { + "id": "4fc4850bf40ccd8039d32d34632704c8", + "name": "www.doaj.org", + "type": "A", + "content": "46.101.64.14", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": "Used to do cloudflare www redirect via bulk redirect list", + "tags": [], + "created_on": "2019-02-27T22:26:18.0461Z", + "modified_on": "2025-01-13T21:02:09.249335Z", + "comment_modified_on": "2025-01-13T11:09:09.64401Z" + }, + { + "id": "8f98d18d384f9a4729b2b8f0df353bbb", + "name": "email.doaj.org", + "type": "CNAME", + "content": "mailgun.org", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": { + "flatten_cname": false + }, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.248173Z", + "modified_on": "2019-02-26T15:02:43.248173Z" + }, + { + "id": "fd2dafef4fe11b8d78159dff95376fdf", + "name": "doaj.org", + "type": "MX", + "content": "alt2.aspmx.l.google.com", + "priority": 5, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.373301Z", + "modified_on": "2019-02-26T15:02:43.373301Z" + }, + { + "id": "1c0dd5cff95b2dc44953f9bba40d8dfd", + "name": "doaj.org", + "type": "MX", + "content": "smtp1.speednames.com", + "priority": 10, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.361906Z", + "modified_on": "2019-02-26T15:02:43.361906Z" + }, + { + "id": "24b7d3f6451151ab963679a6fa8aec19", + "name": "doaj.org", + "type": "MX", + "content": "aspmx2.googlemail.com", + "priority": 10, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.342434Z", + "modified_on": "2019-02-26T15:02:43.342434Z" + }, + { + "id": "47952c2e566b0b2a3b7dacc719bfa15a", + "name": "doaj.org", + "type": "MX", + "content": "alt1.aspmx.l.google.com", + "priority": 5, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.33171Z", + "modified_on": "2019-02-26T15:02:43.33171Z" + }, + { + "id": "d8a8b6884e97e1171cad843538fbece8", + "name": "doaj.org", + "type": "MX", + "content": "smtp2.speednames.com", + "priority": 20, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.319384Z", + "modified_on": "2019-02-26T15:02:43.319384Z" + }, + { + "id": "2af6bb3bf11ef9ef0eeb7945851ada48", + "name": "doaj.org", + "type": "MX", + "content": "aspmx.l.google.com", + "priority": 1, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.308062Z", + "modified_on": "2019-02-26T15:02:43.308062Z" + }, + { + "id": "4523184de998c52bc87c1987f18e1543", + "name": "doaj.org", + "type": "MX", + "content": "aspmx3.googlemail.com", + "priority": 10, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.296877Z", + "modified_on": "2019-02-26T15:02:43.296877Z" + }, + { + "id": "71180160f87c9d642dc302c6f57a2d01", + "name": "_acme-challenge.blog.doaj.org", + "type": "TXT", + "content": "pcZDkDRi5AtIJtbAHSmpTKP9fTwc6EmAQaY4eKZeCy4", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": "doajPM issue 3486", + "tags": [], + "created_on": "2024-03-27T15:16:36.475316Z", + "modified_on": "2024-03-27T15:16:36.475316Z", + "comment_modified_on": "2024-03-27T15:16:36.475316Z" + }, + { + "id": "480212615d3b0a2985eaa78e965a0e2d", + "name": "_acme-challenge.blog.doaj.org", + "type": "TXT", + "content": "bf1DyW8R3lyBaOr5VVezL7qZdJqiLe8GMfOsg3E7fM4", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": "doajPM issue 3486", + "tags": [], + "created_on": "2024-03-21T10:30:25.829805Z", + "modified_on": "2024-03-27T15:11:56.224944Z", + "comment_modified_on": "2024-03-21T10:30:25.829805Z" + }, + { + "id": "b142b1fd166956cfa6021156eabd5806", + "name": "doaj.org", + "type": "TXT", + "content": "google-site-verification=hvwBB9ZfLa1qG4dzyFBmhdJnyjoJbe9urPyo5ZQgxGY", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-04-03T06:08:47.124548Z", + "modified_on": "2019-04-03T06:08:47.124548Z" + }, + { + "id": "5cc6d84b226699c0dcc36153bd3d7252", + "name": "doaj.org", + "type": "TXT", + "content": "v=spf1 include:_spf.google.com include:mailgun.org ~all", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.266635Z", + "modified_on": "2019-02-26T15:02:43.266635Z" + }, + { + "id": "ec1e560ad017cbb26da6b5731ff2bdff", + "name": "google._domainkey.doaj.org", + "type": "TXT", + "content": "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCFP2HR8yqu8RdKyJ/JOglCsFzTYtlwZX3vpEnuJkJPEtNoS7ambJuj2HsHF542suy+6UROUklDeIQJQhZqeigHDC8yFRWZXv9oqVmysHud/I8ZL+6BiifqjSm90GxJRqSkqVVsmtj1Tll18Id6kSfTI5Cx95Mx06AiGZcovKf5fQIDAQAB", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.386761Z", + "modified_on": "2019-02-26T15:02:43.386761Z" + }, + { + "id": "cc74bdea3fcbcdadd11a0006cd889495", + "name": "krs._domainkey.doaj.org", + "type": "TXT", + "content": "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3mdBqN0OAyWHLzimgkV3b06/iRJa1EgsQ8tUsO/+jTuftaYfUBpQCM/0aUcPR+4ctKU4RRlI6dVCyV6j4XbSrAxxbitTKsxi8Nuo2pZkHJAfZGlBJlpiZ1+i9XD4tEJK4htnOma0Tinl6+S/LjK9hC+VGDm8IcJtlMTNOPzbP7QIDAQAB", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2019-02-26T15:02:43.399323Z", + "modified_on": "2019-02-26T15:02:43.399323Z" + } + ] +} \ No newline at end of file diff --git a/cloudflare/cloudflare_rules/zone_settings.json b/cloudflare/cloudflare_rules/zone_settings.json new file mode 100644 index 0000000..7b5d3e0 --- /dev/null +++ b/cloudflare/cloudflare_rules/zone_settings.json @@ -0,0 +1 @@ +{"result":[{"id":"0rtt","value":"off","modified_on":null,"editable":true},{"id":"advanced_ddos","value":"on","modified_on":null,"editable":false},{"id":"always_online","value":"off","modified_on":null,"editable":true},{"id":"always_use_https","value":"off","modified_on":null,"editable":true},{"id":"automatic_https_rewrites","value":"off","modified_on":null,"editable":true},{"id":"brotli","value":"on","modified_on":null,"editable":true},{"id":"browser_cache_ttl","value":14400,"modified_on":null,"editable":true},{"id":"browser_check","value":"on","modified_on":null,"editable":true},{"id":"cache_level","value":"aggressive","modified_on":null,"editable":true},{"id":"challenge_ttl","value":1800,"modified_on":null,"editable":true},{"id":"ciphers","value":[],"modified_on":null,"editable":true},{"id":"cname_flattening","value":"flatten_at_root","modified_on":null,"editable":true},{"id":"development_mode","value":"off","modified_on":"2024-06-18T12:54:30.102680Z","time_remaining":0,"editable":true},{"id":"early_hints","value":"off","modified_on":null,"editable":true},{"id":"ech","value":"off","modified_on":null,"editable":true},{"id":"edge_cache_ttl","value":7200,"modified_on":null,"editable":true},{"id":"email_obfuscation","value":"off","modified_on":"2025-11-20T13:12:40.394201Z","editable":true},{"id":"filter_logs_to_cloudflare","value":"off","modified_on":null,"editable":true},{"id":"hotlink_protection","modified_on":null,"value":"off","editable":true},{"id":"http2","value":"on","modified_on":null,"editable":true},{"id":"http3","value":"off","modified_on":null,"editable":true},{"id":"ip_geolocation","value":"on","modified_on":null,"editable":true},{"id":"ipv6","value":"on","modified_on":"2019-02-26T15:02:28.015186Z","editable":true},{"id":"log_to_cloudflare","value":"on","modified_on":null,"editable":true},{"id":"long_lived_grpc","value":"off","modified_on":null,"editable":false},{"id":"max_upload","value":100,"modified_on":null,"editable":true},{"id":"min_tls_version","value":"1.0","modified_on":null,"editable":true},{"id":"minify","value":{"css":"off","html":"off","js":"off"},"modified_on":null,"editable":true},{"id":"mirage","value":"off","modified_on":null,"editable":true},{"id":"mobile_redirect","value":{"status":"off","mobile_subdomain":null,"strip_uri":false},"modified_on":null,"editable":true},{"id":"opportunistic_encryption","value":"on","modified_on":null,"editable":true},{"id":"opportunistic_onion","value":"on","modified_on":null,"editable":true},{"id":"orange_to_orange","value":"off","modified_on":null,"editable":true},{"id":"origin_error_page_pass_thru","value":"off","modified_on":null,"editable":false},{"id":"polish","value":"off","modified_on":null,"editable":true},{"id":"pq_keyex","value":"on","modified_on":null,"editable":true},{"id":"prefetch_preload","value":"off","modified_on":null,"editable":false},{"id":"privacy_pass","value":"on","modified_on":null,"editable":true},{"id":"proxy_read_timeout","value":"100","modified_on":null,"editable":false},{"id":"pseudo_ipv4","value":"off","modified_on":null,"editable":true},{"id":"replace_insecure_js","value":"off","modified_on":null,"editable":true},{"id":"response_buffering","value":"off","modified_on":null,"editable":false},{"id":"rocket_loader","value":"off","modified_on":null,"editable":true},{"id":"security_header","modified_on":null,"value":{"strict_transport_security":{"enabled":false,"max_age":0,"include_subdomains":false,"preload":false,"nosniff":false}},"editable":true},{"id":"security_level","value":"medium","modified_on":"2021-11-03T13:34:20.508035Z","editable":true},{"id":"server_side_exclude","value":"on","modified_on":null,"editable":true},{"id":"sort_query_string_for_cache","value":"off","modified_on":null,"editable":false},{"id":"ssl","value":"full","modified_on":"2019-02-26T15:16:46.197069Z","certificate_status":"active","validation_errors":[],"editable":true},{"id":"tls_1_2_only","value":"off","modified_on":null,"editable":true},{"id":"tls_1_3","value":"on","modified_on":null,"editable":true},{"id":"tls_client_auth","value":"off","modified_on":null,"editable":true},{"id":"true_client_ip_header","value":"off","modified_on":null,"editable":false},{"id":"visitor_ip","value":"on","modified_on":null,"editable":true},{"id":"waf","value":"off","modified_on":"2024-11-13T10:51:29.586282Z","editable":true},{"id":"webp","value":"off","modified_on":null,"editable":true},{"id":"websockets","value":"on","modified_on":null,"editable":true}],"success":true,"errors":[],"messages":[]} \ No newline at end of file