Skip to content

Instantly share code, notes, and snippets.

@erikhansen
Last active June 29, 2026 18:21
Show Gist options
  • Select an option

  • Save erikhansen/fb4be22198af0bc19725874ac068282d to your computer and use it in GitHub Desktop.

Select an option

Save erikhansen/fb4be22198af0bc19725874ac068282d to your computer and use it in GitHub Desktop.
Connecting Claude Code to New Relic to assist with DDoS/traffic troubleshooting on an Adobe Commerce Cloud environment

Overview

This document is intended to help with fighting off DDoS/bad traffic targeting Adobe Commerce Cloud sites.

With Adobe Commerce Cloud, all server access logs are streamed from the web server(s) to New Relic. This means it's not possible to pull the log files directly from the server an analyze them like you would be able to on a traditional hosting environment.

To overcome this, you can use a combination of New Relic MCP and a New Relic User API Key to download all traffic data and then use Claude Code to analyze the traffic for problematic patterns.

Note: If you have New Relic sitting in front of Adobe Commerce Cloud, make sure you are forwarding user IP addresses.

This document assumes you also have Cloudflare sitting in front of your site, but if not, then replace the Cloudflare recommendations with whatever tool you're using (Fastly, manual rules, etc).

Steps

  1. Connect your Claude Code instance with the Oauth New Relic MCP: https://docs.newrelic.com/docs/agentic-ai/mcp/setup/#claude-code (note: it may not be strictly necessary to us the New Relic MCP, but that's at least what I initially did in order to get Claude Code to build a script to download the log data)
  2. In New Relic, go to "Administration > API Keys" and create a new User key.
  3. Send Claude a prompt like the 2_initial_prompt.md file below
  4. Once that is done, you can ask Claude Code to analyze the traffic using a prompt like 3_analyze_traffic.md

I need you to write a resumable Bash script that downloads raw access-log entries from New Relic and writes them to a local file, for offline analysis.

Fill in these placeholders before running

  • [NEW_RELIC_ACCOUNT_ID] — the New Relic account ID to query (integer)
  • [PROJECT_ID] — the platform/project identifier used in the log path
  • [LOG_FILE_PATH] — the full filePath value, e.g. /var/log/platform/[PROJECT_ID]/access.log
  • [TIME_WINDOW] — how far back to pull (default: last 7 days)
  • [OUTPUT_FILE] — local output file, e.g. access_log.log

Source

  • New Relic account ID: [NEW_RELIC_ACCOUNT_ID]
  • The logs are New Relic Log events where filePath = '[LOG_FILE_PATH]'
  • Format is standard Apache combined log, stored in the message field, e.g.: IP - - [29/Jun/2026:04:09:49 +0000] "GET /path HTTP/1.1" 200 1234 "referer" "user-agent"
  • Volume can be large (a busy site is millions of entries/day). First run a SELECT count(*) FROM Log WHERE filePath = '[LOG_FILE_PATH]' SINCE [TIME_WINDOW] to size the job before pulling.

Auth

  • Use a New Relic User API key (format NRAK-...) via NerdGraph (POST https://api.newrelic.com/graphql, header API-Key: $NEW_RELIC_API_KEY).
  • The script must read the key from the NEW_RELIC_API_KEY env var — never hard-code it in the file.

The core constraints (these dictate the design — don't fight them)

  1. NRQL caps results at 5,000 rows per query (LIMIT MAX = 5000). There is no offset/deep pagination, so you must paginate with a timestamp cursor.
  2. Do the pagination as a curl loop in Bash that appends each batch straight to a file on disk — do NOT route batches through the model's context (that's the bottleneck that makes this infeasible any other way). Use jq to extract fields from each JSON response.

Required script behavior

  • Accept a time window (default: [TIME_WINDOW]). Pin absolute epoch-ms START/END once at the start so the window doesn't drift across a multi-hour run (persist them to a file).
  • Paginate newest→oldest: each query is SELECT timestamp, messageId, message FROM Log WHERE filePath = '[LOG_FILE_PATH]' SINCE <START_ms> UNTIL <cursor_ms> ORDER BY timestamp DESC LIMIT MAX
  • After each batch, set the next cursor to min(timestamp in batch) + 1 ms.
  • Dedup across the millisecond boundary by messageId: many rows share the same timestamp ms, so re-including that ms (cursor = min_ts+1) will re-fetch boundary rows — track the messageIds at the boundary ms and skip them in the next batch so no line is written twice.
  • Write the raw message (the combined log line) one per line to [OUTPUT_FILE].
  • Checkpoint the cursor to disk every batch so the script is resumable — re-running continues from where it left off rather than restarting.
  • Stop when a batch returns fewer than 5,000 rows (reached the bottom of the window).
  • Retry transient NerdGraph errors a few times with backoff; log per-batch progress (batch #, rows, running total) to a run log. Add a small sleep between calls to respect rate limits.
  • For large windows, run it in the background (nohup) since multi-million-row pulls can take hours.

Notes / caveats to bake in

  • If the site sits behind a CDN/reverse proxy (e.g. Cloudflare), field 1 of each log line may be the edge/proxy IP, not the true client IP. Verify with head -1 of an early batch; if true client IPs are needed, check for an X-Forwarded-For / CF-Connecting-IP field (which may not be in this log).
  • Output ends up newest-first (per descending pagination); note this so downstream analysis can sort by timestamp if chronological order is needed.
  • Sanity-check at the end: line count, first/last timestamps span the requested window, no empty lines.

Build the script, do a small smoke test (e.g. cap at 2 batches in an isolated dir to confirm pagination

  • dedup work), then launch the full run in the background and tell me how to monitor it.

You are analyzing a web server access log to find bot / scraper / DDoS traffic that is overloading the origin (and, for Magento specifically, driving up backend/API cost), then to produce concrete Cloudflare mitigations. Work methodically and report conclusions backed by specific numbers — do NOT dump raw log lines into your replies.

Input

  • Log file: [LOG_FILE] (e.g. ./access_log.log)
  • It is an Apache/Nginx "combined" format log: IP - - [10/Oct/2026:13:55:36 +0000] "GET /path?q=1 HTTP/1.1" 200 1234 "referer" "user-agent"
  • The file may be very large (millions of lines / multiple GB). Stream it with grep/awk; never load the whole thing into context. For rotated .gz logs, gunzip -k first.

STEP 0 — Orient before analyzing (do this first, every time)

  1. head -1 [LOG_FILE] and confirm the field layout, especially that field 1 is the client IP and the timestamp timezone (usually +0000 = UTC).
  2. CRITICAL CAVEAT — CDN edge IPs: if the site is behind a CDN/proxy (Cloudflare, Fastly, Akamai), field 1 is the edge IP, not the true visitor. Check whether field-1 IPs fall in Cloudflare ranges (104.16-31.x, 162.158.x, 172.64-71.x, 173.245.x, 188.114.x, 131.0.72.x) etc. If so:
    • Per-IP fingerprinting and ASN/geo attribution from this log are UNRELIABLE — say so explicitly.
    • Pivot to user-agent, path/param, and request-rate signals, which still work.
    • Note that true client IP / ASN / country requires edge analytics (e.g. Cloudflare GraphQL httpRequestsAdaptiveGroups) or a logged CF-Connecting-IP / X-Forwarded-For field.
  3. TIMEZONE: if you were given a "site was slow at TIME" window from a monitoring tool, that's usually local time; the log is UTC. Convert before building any time regex (e.g. PDT 4:54 AM = 11:54 UTC). A wrong conversion makes you analyze empty traffic and wrongly conclude "nothing's there."

The core insight (what you're hunting for)

Origin overload is almost always a few identifiable traffic patterns, not mysterious load. On Magento, three recur:

  1. Faceted-navigation crawl (the usual root cause). Clean URLs (/cat.html) are served from full-page cache. The moment a filter query string is appended (/cat.html?manufacturer=X&caliber=Y) the request is uncacheable and falls through to PHP+MySQL (and any search/recommendation API), running a fresh layered-navigation query EVERY time. A crawler walking filter permutations = tens of thousands of uncacheable DB/API hits. This produces intermittent spikes.
  2. Admin-token brute-force — POST flood to /rest/V1/integration/admin/token (expensive bcrypt by design).
  3. REST API scraping/abuse — high volume to /rest/.../products, /rest/.../orders, GraphQL, etc. The driver is usually a distributed datacenter/VPS bot fleet (HostRoyale, Vultr, DigitalOcean, Zenlayer, OVH, etc.) and/or offshore residential proxies, hiding behind rotating realistic browser user-agents. Real shoppers on a US store skew mobile/residential; a surge of desktop UAs or datacenter ASNs hitting filtered category pages is the tell.

STEP 1 — Whole-file profile (one efficient pass)

Run a single streaming awk pass to get: total requests; per-day volume; status-code distribution; method distribution; cacheable (no ?) vs uncacheable (has ?) split; counts for GraphQL, /rest/, admin-token, search, and faceted .html? requests; the ampersand distribution of .html? URLs; the top filter parameter names; bot-classified share; and the top user-agents. (Write the aggregates to a small file and sort down to top-N so nothing huge hits context.)

STEP 2 — Profile any suspicious window

If there's a slow window or a day that spikes, build a TIME_REGEX for it and run window_traffic.sh (below) to see the per-minute spike shape and the top source IPs/UAs in that window. Compare a "during" window to a "before" window and compute which user-agents / paths / params GREW.

STEP 3 — Fingerprint top suspects

For each top IP (or, if IPs are CDN-masked, each top UA), run ip_profile.sh (below). Tells:

  • UA rotation — one IP emitting dozens of slightly-varied browser UAs = scraper using a rotation library. Decisive bot signal a static blocklist can't catch.
  • Datacenter origin — cloud/VPS ASNs for a retail site are almost never real shoppers.
  • Status mix — 401/429 floods = brute-force being rate-limited; 503/504 = origin already shedding load.
  • 96%+ filtered category views, or systematic category-tree walking = crawler, not human.

STEP 4 — Hunt the Magento vectors

Run facet_scan.sh and magento_vectors.sh (below).

  • On the admin-token endpoint, check the status mix: ANY 200 means a credential SUCCEEDED — treat as a breach and tell me to rotate that credential. All-401/429 = failed + rate-limited (no breach).
  • The ampersand distribution matters for Cloudflare rule tuning (see mitigations).

STEP 5 — Correlate

Line up suspect volume + 503/504 timeline against the slow window. A clean story: suspects jump from ~10 req/hr to thousands exactly when errors spike. Distinguish CHRONIC load (steady all day) from ACUTE bursts (what caused this incident).

STEP 6 — Recommend Cloudflare mitigations

Prefer rate-limit or Managed Challenge over hard block for GET traffic to product/category pages (a hard block on a faceted URL can catch a real shopper who clicked a filter). Reserve hard Block for unambiguous abuse (admin-token flood; a pure-datacenter ASN; a non-browser automation UA).

Produce concrete, copy-paste, Expression-Builder-safe rules. The Builder rejects nested AND (a OR b OR c) — flatten so each OR term is self-contained.

  1. Faceted-nav rate limit (match PARAM NAMES, not ampersand count). A common rule like http.request.uri.query wildcard r"*&*&*&*" only matches 3+ ampersands (4+ params) and MISSES single-parameter facet URLs (?manufacturer=X, zero ampersands) — often the bulk of the attack. Derive the real param names from facet_scan.sh output and match them directly. Exclude pagination and ad params (p=, is_scroll, utm_, gclid, fbclid). Threshold low (~20 req/min/IP), action Managed Challenge.
  2. Datacenter / cloud ASN challenge or block (when traffic concentrates in hosting ASNs). Confirm ASNs with whois. not cf.client.bot spares verified good bots. Example: (ip.geoip.asnum in {AS1 AS2 ...} and http.request.method eq "GET" and not ip.src in $allowlist)
  3. Admin-token lockdown: http.request.method eq "POST" and http.request.uri.path eq "/rest/V1/integration/admin/token" → Block or rate-limit ~5/min; better, IP-allowlist to known integrations.
  4. REST/GraphQL scraping: rate-limit /rest/V1/orders, /rest/V1/products, graphql per IP.
  5. Geo (if the store ships to one country): Managed Challenge non-domestic traffic to .html. Also note per-IP rate limits are weak against a DISTRIBUTED fleet (each IP stays under threshold) — ASN/UA/geo matching matters more there.

Defense-in-depth (origin side): robots.txt: Disallow: /*?; review Magento layered-nav crawl settings / add rel="nofollow" to filter links; raise cache-hit on clean category pages; add %v (vhost) to the Apache LogFormat if this is a multi-store server (you often CANNOT attribute a request to a domain otherwise — be honest about that limitation).

Report structure (end with this)

Findings: <window/scope>

Verdict — one-paragraph plain answer: what the traffic is and whether it's a concern.

Attack/load vectors — each: what it is, evidence (counts, status, timeline), chronic vs acute.

Top offenders — IPs/UAs/ASNs with counts + UA-rotation evidence + what they targeted.

Timeline correlation — suspect volume + errors vs the window.

Recommended mitigations — copy-paste Cloudflare rules, priority order, + origin-side steps.

Lead with the single most important number (e.g. "47% of /bsearch/ requests 504'd" or "one VPS ASN = 4.1M category requests in 2 days").

Analysis scripts — create these and run them (combined-format; field 1 = client IP, may be CDN edge)

window_traffic.sh — per-minute rate + top IPs in a time window

#!/usr/bin/env bash set -euo pipefail LOG="${1:?usage: window_traffic.sh LOGFILE TIME_REGEX [TOP_N]}" RE="${2:?need a time regex, e.g. '22/Jun/2026:(11:5[4-9]|12:[01][0-9]|12:20)'}" TOPN="${3:-20}" TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT grep -E "$RE" "$LOG" > "$TMP" || true echo "=== Window total: $(wc -l < "$TMP") ===" echo "=== Requests per minute (spike shape) ===" grep -oE '[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}:[0-9]{2}' "$TMP" | sort | uniq -c echo "=== Top $TOPN client IPs (field 1) ===" awk '{print $1}' "$TMP" | sort | uniq -c | sort -rn | head -"$TOPN"

ip_profile.sh — fingerprint one IP: UA-rotation, paths, params, status, methods

#!/usr/bin/env bash set -euo pipefail LOG="${1:?usage: ip_profile.sh LOGFILE IP}"; IP="${2:?need an IP}" ipesc=$(printf '%s' "$IP" | sed 's/[].[^$*\/]/\&/g'); SEL="^${ipesc} " echo "=== $IP : $total requests ==="; [ "$total" -eq 0 ] && exit 0 echo "Distinct user-agents: $(grep -E "$SEL" "$LOG" | sed -E 's/." "([^"])"$/\1/' | sort -u | wc -l) (many = rotation bot)" echo "--- methods ---"; grep -E "$SEL" "$LOG" | sed -E 's/."([A-Z]+) ./\1/' | sort | uniq -c | sort -rn echo "--- status ---"; grep -E "$SEL" "$LOG" | sed -E 's/." ([0-9]{3}) ./\1/' | sort | uniq -c | sort -rn echo "--- top paths ---";grep -E "$SEL" "$LOG" | sed -E 's/."(GET|POST|HEAD|PUT|DELETE) ([^ ?"])[^"]"./\2/' | sort | uniq -c | sort -rn | head -10 echo "--- top query params ---"; grep -E "$SEL" "$LOG" | grep -oE '?[^" ]' | grep -oE '[a-zA-Z_]+=' | sort | uniq -c | sort -rn | head -10 echo "--- top 5 UAs (rotation sample) ---"; grep -E "$SEL" "$LOG" | sed -E 's/." "([^"]*)"$/\1/' | sort | uniq -c | sort -rn | head -5

facet_scan.sh — faceted-nav volume, ampersand distribution (for CF rule tuning), cloud-fleet sizing

#!/usr/bin/env bash set -euo pipefail LOG="${1:?usage: facet_scan.sh LOGFILE}"

Extend with the cloud/VPS prefixes you actually find via whois on top offenders:

CLOUD_RE='^(34.|35.|3.|13.|18.|52.|54.|20.|40.|104.196.|130.211.|146.148.)' echo "=== Filter-parameter volume (whole file) ===" grep -oE '?[^" ]' "$LOG" | grep -oE '[a-zA-Z_]+=' | sort | uniq -c | sort -rn | head -20 echo "=== Ampersand distribution of .html?... (CF rule tuning) ===" grep -oE '"GET [^"].html?[^"]"' "$LOG" | awk -F'&' '{print NF-1}' | sort -n | uniq -c
| awk '{printf " %8d requests with %s ampersand(s) (%d params)\n",$1,$2,$2+1}' single=$(grep -cE '"GET [^"]
.html?[a-zA-Z_]+=[^&"]"' "$LOG" || true) echo " -> single-param .html (MISSED by a &&&* wildcard): $single" echo "=== Datacenter/cloud fleet sizing (only meaningful if field 1 is the REAL client IP) ===" echo " distinct cloud IPs: $(grep -E "$CLOUD_RE" "$LOG" | awk '{print $1}' | sort -u | wc -l)" echo " cloud requests: $(grep -cE "$CLOUD_RE" "$LOG" || true)" echo " total requests: $(wc -l < "$LOG")"

magento_vectors.sh — admin-token brute-force, REST/GraphQL abuse, 5xx timeline

#!/usr/bin/env bash set -euo pipefail LOG="${1:?usage: magento_vectors.sh LOGFILE}" echo "=== [1] Admin-token brute-force: /rest/V1/integration/admin/token ===" tok=$(grep -c 'integration/admin/token' "$LOG" || true); echo "Total: $tok" if [ "$tok" -gt 0 ]; then echo "Status mix (ANY 200 = credential SUCCESS = treat as breach):" grep 'integration/admin/token' "$LOG" | sed -E 's/." ([0-9]{3}) ./\1/' | sort | uniq -c | sort -rn echo "Per-hour:"; grep 'integration/admin/token' "$LOG" | grep -oE '[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}' | sort | uniq -c echo "Top source IPs:"; grep 'integration/admin/token' "$LOG" | awk '{print $1}' | sort | uniq -c | sort -rn | head -5 fi echo "=== [2] REST surface by endpoint ===" grep -oE '/rest/[A-Za-z0-9_/-]+/V1/[a-zA-Z]+|/rest/V1/[a-zA-Z]+' "$LOG" | sort | uniq -c | sort -rn | head -15 echo "Top IPs hitting /rest/.../orders (sensitive):" grep -E '/rest/[^"]*orders' "$LOG" | awk '{print $1}' | sort | uniq -c | sort -rn | head -5 || true echo "=== [3] 5xx timeline (when origin shed load) ===" for code in 502 503 504; do n=$(grep -c "" $code " "$LOG" || true); echo "$code total: $n" [ "$n" -gt 0 ] && grep "" $code " "$LOG" | grep -oE '[0-9]{2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}' | sort | uniq -c done

Start by running STEP 0 and STEP 1, show me the headline numbers, then proceed through the remaining steps and end with the structured report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment