Skip to main content

nginx-log-review

Review recent production nginx/openresty access logs, identify scanner/exploit traffic not already blocked, and update config/openresty/conf/blot-blogs.conf accordingly. Use when asked to analyze prod nginx/access logs, find new paths to block, or audit openresty for scanner traffic.

Jump to install

Source facts

Repository
blotcms/blot
Last source activity
September 12, 2026 at 09:31
Detected SKILL.md language
English
Stars
1,980
Forks
98

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
nginx-log-review
description
Review recent production nginx/openresty access logs, identify scanner/exploit traffic not already blocked, and update config/openresty/conf/blot-blogs.conf accordingly. Use when asked to analyze prod nginx/access logs, find new paths to block, or audit openresty for scanner traffic.
# Nginx access log review → openresty blocklist update This skill reproduces the workflow used in PR #1821 / issue #1822: pull real production traffic, find scanner/exploit patterns that are falling through existing rules, add narrowly-scoped blocks, and flag anything found that isn't a block (e.g. app-level bugs inflating cache misses) as a separate GitHub issue instead of bundling it into the openresty change. ## 1. Get the logs SSH host is `blot` (production EC2 box, configured in `~/.ssh/config`). Openresty access logs live at `/var/instance-ssd/logs/access.log` (current) and `/var/instance-ssd/logs/access.log-YYYYMMDD` (yesterday's rotated file). Combine both for a full ~24-48h window: ```bash ssh blot "cat /var/instance-ssd/logs/access.log-$(date -d yesterday +%Y%m%d) /var/instance-ssd/logs/access.log > /tmp/combined_access.log; wc -l /tmp/combined_access.log" ``` Log line format (space-separated, so `cut -d ' ' -fN` / `awk '{print $N}'` work): ``` [time] request_id status request_time bytes_in:bytes_out url cache=X ip=X st=X lrs=X ua=X 1 2 3 4 5 6 (7 is empty due to double space before cache=) ``` Concretely: `$4` = status code, `$7` = full URL (scheme+host+path+query), `ip=`, `ua=`, `cache=` are grep/`grep -oE` targets, not fixed fields. **Always confirm with the user before running anything against production**, and stick to read-only commands. Clean up `/tmp/combined_access.log` on the remote host when done (`ssh blot "rm -f /tmp/combined_access.log"`). ## 2. Aggregate Status code breakdown: ```bash ssh blot "awk '{print \$4}' /tmp/combined_access.log | sort | uniq -c | sort -rn" ``` Top 404/403/444 paths (strip domain and query string so variants collapse): ```bash ssh blot "grep ' 404 ' /tmp/combined_access.log | cut -d ' ' -f7 | sed -E 's|https?://[^/]+||' | sed -E 's/\?.*//' | sort | uniq -c | sort -rn | head -100" ``` Repeat for ` 403 ` and ` 444 ` to see what's already being caught (444s are already blocked with a fast fail2ban ban; don't re-block those). Check whether the traffic is IP-concentrated or diffuse — if a domain is proxied through Cloudflare, `ip=` will be CF edge IPs, not the real client, which makes IP/fail2ban blocking useless and confirms **path-based blocking at the openresty layer is the right lever**: ```bash ssh blot "grep -oE 'ip=[0-9.]+' /tmp/combined_access.log | sort | uniq -c | sort -rn | head -15" ``` ## 3. Diff against existing blocks Read the current rules before proposing new ones — don't duplicate or narrow what's already covered: - `config/openresty/conf/blot-blogs.conf` — path/extension-based `location` blocks (`.env`, `.git`, wp-*, php extensions, sensitive JSON files, etc.) This is the file that's actually deployed to production (see `proxy/README.md` — `proxy/` is a separate work-in-progress containerization fork, **not yet wired to production**; don't edit it for a live-traffic fix). - `config/openresty/conf/server.conf` — the `$bad_bot` user-agent map (`restrict-bot-uas.conf` returns 403 for matches). Scanner traffic usually spoofs a real browser UA, so this rarely helps for the paths found here — check `ua=` values for the offending paths before assuming a UA-based rule would work. - `config/openresty/fail2ban/jail.local` — IP-banning jails keyed off 404/403/429/444 rates. Complementary to path blocks, not a substitute. For each candidate pattern, grep its current count and check whether it's already caught by an existing regex (e.g. anything ending in `.php` is already 403'd by the generic extension rule — no need for a `shell.php`- specific rule). ## 4. Identify unambiguously blockable patterns A pattern is safe to add only if it has **no plausible legitimate use** on a Blot blog. Before adding any pattern, sample its actual matched URLs and check for false positives — a substring that's too generic will catch real blog content: ```bash ssh blot "grep -E 'PATTERN' /tmp/combined_access.log | cut -d ' ' -f7 | sort | uniq -c | sort -rn | head -15" ``` Known trap from prior review: a bare `geoserver` substring match caught a real blog's post slugs (`/geoserver-1-7-2-continues-improvements...`) — had to anchor to `^/geoserver/` instead. Apply the same scrutiny to any short/generic word (`passwd`, `cmd=`, `admin`, `login`, `console`, `info`, `about`, `proxy`) — these are common real blog paths/slugs and should generally NOT be blocked outright. Categories worth checking each time (volumes shift, but these have recurred): `.env` in any position (not just as a suffix — scanners try hundreds of prefix/suffix permutations), `.git` anywhere in the path, `/@fs/` and `/proc/self/` (dev-server/path-traversal probes), cloud credential/key JSON filenames (`service-account.json`, `firebase-adminsdk.json`, `gcp-*`, `application_default_credentials.json`), SSH keys / `.npmrc` / `.htpasswd` / `.s3cfg` / `terraform.tfstate` / `.dockerenv`, `.svn/`, Spring Boot `/actuator/`, `phpinfo` in any form, Kubernetes serviceaccount tokens, and CVE-specific RCE probe paths (`/_ignition/`, `/geoserver/`, etc. — check what's currently trending). ## 5. Update the openresty config Edit `config/openresty/conf/blot-blogs.conf`. Follow the existing convention in that file: - `return 444;` for requests that are **clearly malicious** (faster fail2ban ban than a 403 — see the comment at the top of the file's block-rules section). - `return 403 '403';` for requests that are only **plausibly** malicious. - Every block includes `limit_req zone=bots;` before the return. - Add new broad/generic rules near the top of the file and specific exact-path rules can stay further down — nginx evaluates regex `location` blocks in file order and stops at the first match, so a broad rule placed early will shadow (and can make redundant) a narrower rule for the same pattern later in the file. Remove any rule that becomes fully shadowed rather than leaving dead code. ## 6. Validate There's no local nginx/openresty binary and the full mustache-based build needs network access (fetches BunnyCDN IPs) and repo secrets, so don't try to run the real build pipeline locally. Instead, sanity-check just the new `location` block syntax against a real openresty binary in Docker: ```bash docker run --rm -v /path/to/snippet-nginx.conf:/etc/nginx/nginx.conf:ro \ openresty/openresty:alpine openresty -t ``` Wrap the new/changed `location { ... }` blocks in a minimal `events{} http { limit_req_zone $binary_remote_addr zone=bots:10m rate=1r/s; server { listen 8080; ...; location / { return 200; } } }` skeleton for this check. This catches syntax errors but not deployment behavior — the repo's `proxy` GitHub Actions workflow (a separate, not-yet-production fork) does full build+boot validation; per this project's convention, prefer letting GitHub CI do heavier validation over elaborate local Docker setups. ## 7. Ship it - Commit only `config/openresty/conf/blot-blogs.conf` (and this skill file if it's being updated). - Push a branch and open a PR. In the PR description: state the log window analyzed, a table of pattern → approximate daily request count, note which existing rules already covered adjacent cases (so reviewers see what's genuinely new), call out any false-positive traps you checked for and how the regex was anchored to avoid them, and confirm whether IP-based blocking would or wouldn't help (Cloudflare-fronted domains usually mean it wouldn't). - If the log review surfaces something that **isn't** an openresty block — e.g. an app/template bug generating a pathological amount of traffic, a slow endpoint, a caching gap — don't fold it into the openresty PR. File it as a separate `gh issue create`, referencing the PR for the underlying data.
View on GitHub