Blog
On My Blog Getting Scanned: Common Web Attack Patterns
Even a static site never lacks /wp-login.php, /.git/config, or /phpmyadmin in the OpenResty access log. In July I bucketed several consecutive weeks of logs by pattern — not because I was compromised, but because getting scanned is the default state of being on the public internet. Treat this post as an attack-pattern field guide: the paths and signals come from aggregated real logs, and you can line them up against your own Top Paths. By the end you should be able to quickly tell "noise" from "time to get out of bed."
How to extract the "field guide" from your logs
# Top paths
awk '{print $7}' access.log | sort | uniq -c | sort -nr | head -40
# Status code x path
awk '{print $9, $7}' access.log | sort | uniq -c | sort -nr | head -40
# Suspicious query strings and traversal traces
awk -F\" '{print $2}' access.log | grep -E '(\.\./|\.env|wp-)' | head
First look at the peaks in the sea of 404s, then double-check 200s and 403s. Scanner noise can be ignored; an unexpected 200 is what should get your heart rate up. Keep a weekly or monthly snapshot of your Top Paths so you can spot "new dictionaries" appearing rather than going on vibes that "things have been quiet lately." If Cloudflare sits in front of you, and origin logs don't match edge logs, treat the edge as the truth about who's knocking — the origin only sees origin-fetch traffic. Track "paths that are new this month and look exploit-y" as a single list; that's more sustainable than an ever-growing blacklist.
Pattern 1: CMS and panel sniping
Signature paths: /wp-login.php, /xmlrpc.php, /wp-admin/, /user/login, /administrator/, /phpmyadmin/, /adminer.php, plus assorted panel-probing paths.
Intent: credential stuffing, known remote code execution, weak passwords. A static blog just needs to answer these with 404 or 444 forever. If you actually run a panel, it shouldn't be on the public internet at all — tunnel-only at most.
Log profile: the same address hammering dictionary-style paths in a short window; the UA is often curl, python, empty, or a spoofed ancient browser. Intervals can be extremely short, or deliberately stretched to dodge simple rate limits. In my logs, the single biggest share of noise is almost always WordPress-related — even though this site has never run PHP.
Pattern 2: keys and environment files
Signature: /.env, /.env.bak, /.git/config, /.git/HEAD, private-key filenames, compressed backup bundles, config.yml and friends.
Intent: walk off with database passwords, cloud keys, and source code. A repo or backup bundle sitting in the web root is a serious ops incident, not "aggressive scanning." The scanner is just measuring your attack surface; the real fault is in your publish and packaging pipeline.
Response: confirm none of these files exist on disk; 444 the prefixes at the rule layer; keep CI artifacts and backups off web-visible paths; add a release check that the docroot contains no .git or .env. When probes like these hit a real file, the damage lands faster than CMS credential stuffing.
Pattern 3: path traversal and probing-style remote execution
Signature: URL-encoded ../ like %2e%2e/, suspicious query parameters such as cmd and exec, template-injection-flavored curly-brace fragments, half-finished log4j or JNDI strings. Scripts cast their nets even when your stack is completely unrelated.
Intent: find arbitrary file reads and old-framework vulnerabilities. Mostly harmless against a static site — but a reverse proxy that naively concatenates root paths can pass traversal through to an upstream filesystem or internal service.
Response: normalize URIs; don't expose arbitrary-file APIs upstream; log obvious payloads and drop the connection outright. The priority is checking whether any successful response ever appeared, not out-dictionarying the scanner. A single probe mixed into normal crawler traffic is worth logging only; the same source rotating payloads nonstop is worth rate-limiting or banning.
Pattern 4: exploit kits and bulk fingerprinting
Signature: a fixed UA family, a highly consistent request order (root path first, then a string of CVE paths), specific header combos. Sometimes it hides inside "normal" crawler traffic, so you have to judge by frequency and path diversity.
Intent: automated malware dropping and botnet expansion. A personal site has low business value, but it still burns bandwidth and log volume, and it buries the traffic you actually want to read.
Response: rate-limit globally; block a wave at the edge; catch what's left at the origin. Don't chase a complete signature database — maintain a small, explainable rule set you can roll back fast when it misfires. Kit fingerprints change; "the attack surface is small to begin with" beats "my rules are always up to date."
Pattern comparison table
| Pattern | Typical path or signal | Expected response on a static site | Escalation trigger |
|---|---|---|---|
| CMS sniping | wp-login, phpmyadmin | 404 / 444 | 200 — investigate immediately |
| Key files | .env, .git | Same as above | File present on disk — that's an incident |
| Traversal and probes | ../, cmd, jndi | Same as above | Unexpected 200 upstream |
| Kit scanning | dictionary order + odd UA | Rate-limit and drop | Bandwidth or 5xx anomalies |
Mitigation layers (static-site edition)
Drop dead paths outright, add IP rate limiting on top, make sure no repos or backups live in the web root, keep apps and panels updated, and pull admin entry points into the VPN. Escalation can stay simple: pure scanning with all-failed responses gets trend-logged only; a clear exploit attempt tightens the rules and triggers a check for successful responses; signs of actual intrusion mean disconnect, snapshot, and run the incident procedure.
You can write the rules on both sides (edge and origin), but a thin, explainable set beats copying an entire commercial ruleset you'd never dare touch. Being scanned is not being compromised; being scanned means your attack surface is being continuously measured. The point of the field guide: next time the same paths show up in your logs, you know whether to keep sipping coffee or open the incident checklist.
If you only do one thing: physically separate the web root from the backup directory, and drop requests for dotfiles and dot-git paths outright. That lowers the odds of a real incident more than studying the latest scanner UA. The field guide is a classification tool; subtraction is the defense itself.