Blog — Web Security
Your site gets scanned
before you finish deploying.
Within 24 hours of going live, this server received 1,203 automated probe requests — 91% of all traffic. None of them were human visitors. This article breaks down exactly what the scanners looked for, where the traffic came from, and gives you a concrete hardening checklist to close every door before your first deploy.
Published: — Based on real access log data from nikokp.com — 7 min read
01 What the Logs Showed
This site runs nginx behind a Cloudflare Tunnel — no ports exposed publicly, a fairly typical small-site stack. It went live on . By , the access log had 1,368 total requests. Here's the status code breakdown:
| Status | Count | Share | What it means |
|---|---|---|---|
200 OK |
118 | 9% | Legitimate content served |
404 Not Found |
1,203 | 88% | Automated probes for non-existent files |
| Other | 47 | 3% | Redirects, bot crawlers |
The top source IPs all resolved to Microsoft Azure — a common pattern because Azure free-tier accounts are cheap and easy to abuse for scanning. One IP fired 166 requests in under 3 seconds, working through a wordlist of PHP filenames at machine speed. A second cluster of IPs with no user-agent string probed exclusively WordPress paths.
Scanners do not target you specifically. They sweep every reachable IP and domain continuously. Being new, small, or obscure provides zero protection. The question is whether anything they find is exploitable.
02 The Three Attack Categories
Category 1 — PHP webshell probes
The highest-volume attack. Scanners request hundreds of random PHP filenames looking for webshells — malicious PHP scripts (like alfa.php, wso.php, c99.php) that attackers previously uploaded to compromised servers. If a webshell responds with HTTP 200, the attacker now has a remote command interface on your server.
Examples seen in logs: /ioxi-o.php (12 hits), /adminfuns.php, /classwithtostring.php, /bless.php, /alfa.php, /VzlaTeam.php.
These are completely harmless against a server that never ran PHP — every request returns 404. But if you ever had a misconfigured PHP file accidentally placed at any of these paths, or if your server executes arbitrary PHP from the web root, you're exposed.
Category 2 — WordPress vulnerability probes
WordPress powers ~43% of the web, which makes its attack surface well-known and heavily automated. Scanners probe for:
/xmlrpc.php— a legacy XML-RPC endpoint used for brute-force amplification attacks/wp-login.php— brute-force credential stuffing/wp-admin/— admin panel enumeration/wp-content/plugins/[name]/— known vulnerable plugin files/wp-config-sample.php— configuration file with database credentials
You don't need to run WordPress to receive these probes. They're sent to every IP. If you do run WordPress, each of these paths is a real attack vector.
Category 3 — Credential and configuration harvesting
The most dangerous category if successful. Scanners look for accidentally exposed secrets:
| Path probed | What it exposes if found | Risk |
|---|---|---|
/.env |
API keys, database passwords, secrets | Critical |
/.git/config |
Remote repo URL, sometimes credentials in URL | Critical |
/docker-compose.yml |
Service topology, environment variables, ports | High |
/Dockerfile |
Build steps, base images, sometimes embedded secrets | High |
/nginx.conf |
Internal routing, backend service addresses | High |
/wp-config-sample.php |
Database host, user, password template | High |
/.env.local, /.env.backup, /.env.example |
Secrets in backup or example files left in web root | Critical |
A /.git/ directory in your web root is particularly severe: it allows downloading your entire commit history, including any secrets that were ever committed — even if later deleted from the codebase.
03 Pre-Launch Hardening Checklist
Every item below addresses a specific attack category observed in the logs above. Work through these before your first deploy, not after.
nginx configuration hardening
These rules belong in your server {} block. They drop or block the most common scanner patterns at the web server level — before any application logic runs.
# Block PHP execution if you don't serve PHP
location ~* \.php$ {
return 444; # Close connection, no response
}
# Block access to hidden files and directories
location ~ /\. {
deny all;
return 404;
}
# Block access to common config/secret files
location ~* \.(env|git|yml|yaml|conf|config|ini|log|bak|backup|sql|sh)$ {
deny all;
return 404;
}
# Disable directory listing
autoindex off;
# Hide nginx version from error pages and Server header
server_tokens off;
Security response headers
These headers are evaluated by browsers and tell them how to treat your content. They stop a class of attacks that 404 blocking cannot — like cross-site scripting and clickjacking.
# Add to your server {} or http {} block
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
# Content Security Policy — tighten based on your actual asset sources
add_header Content-Security-Policy "default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; script-src 'none';" always;
# HSTS — only add after confirming HTTPS works correctly
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Keep secrets out of the web root
This is the most important rule and costs nothing to follow. Your web root is the directory nginx serves files from. Nothing that isn't meant to be publicly readable should ever be in it.
- Store
.envfiles one directory above the web root, never inside it - Add
.env*,docker-compose*.yml, and*.confto.gitignorebefore the first commit - Never deploy with a
.git/directory inside the web root — use a CI pipeline orrsync --exclude='.git' - Audit before launch:
find /var/www/html -name ".env" -o -name "*.yml" -o -name ".git" -type d
Cloudflare WAF rules (if using Cloudflare)
If your stack includes Cloudflare (free tier is sufficient), these managed rulesets block scanner traffic upstream — before it reaches your server at all.
- Enable Cloudflare Managed Ruleset (Security → WAF → Managed rules)
- Enable Cloudflare OWASP Core Ruleset — covers PHP injection, path traversal, credential exposure
- Create a custom WAF rule to block requests to
*.phpif your site doesn't use PHP:(http.request.uri.path matches "\.php$")→ Block - Set Security Level to Medium or High for the first weeks after launch
- Enable Bot Fight Mode (free) to challenge known scanner ASNs
Rate limiting
The Azure-based scanner in this server's logs fired 166 requests in 3 seconds. A basic rate limit would have cut the connection after the first few. At the nginx level:
# In http {} block — define a rate limit zone
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/m;
# In server {} block — apply it
limit_req zone=general burst=10 nodelay;
limit_req_status 429;
Cloudflare's free tier also includes rate limiting rules under Security → WAF → Rate limiting rules.
File permissions on the server
Even if a scanner can't read a secret file via HTTP, a misconfigured server with world-readable files can leak them through other paths. Correct permissions for a static site:
# Web root directory — readable but not writable by the web server user
chmod 755 /var/www/html
find /var/www/html -type f -exec chmod 644 {} \;
find /var/www/html -type d -exec chmod 755 {} \;
# Confirm the nginx worker process user (usually www-data or nginx)
# does NOT own the web root files
chown -R root:root /var/www/html
WordPress-specific hardening
If you do run WordPress, every path the scanners probed is real. Apply these before launch:
| Scanner target | Hardening action |
|---|---|
/xmlrpc.php |
Block at nginx: location = /xmlrpc.php { deny all; } — unless you need it for Jetpack |
/wp-login.php |
Restrict by IP or add HTTP Basic Auth in front of it at nginx level |
/wp-admin/ |
Same as wp-login — IP allowlist or Basic Auth wrapper |
/wp-config.php |
Move one directory above the web root — WordPress supports this natively |
| Plugin vulnerabilities | Audit with WPScan before launch; remove unused plugins; keep all plugins updated |
User enumeration (/?author=1) |
Block with: if ($query_string ~ "author=[0-9]") { return 403; } |
Log monitoring from day one
You cannot respond to attacks you can't see. Set up access log monitoring before launch, not weeks later when something breaks.
- GoAccess — open-source, real-time log analyzer. Runs as a Docker container, outputs a live HTML dashboard. Zero external dependencies.
- Set a cron job or alerting rule to notify you when 404 rates spike above your baseline
- Log the real visitor IP, not the proxy IP — with Cloudflare, use
real_ip_header CF-Connecting-IPin nginx - Retain logs for at least 30 days so you can reconstruct attack timelines
04 What Not to Waste Time On
IP banning
The Azure IP ranges used for scanning rotate constantly across millions of addresses. Blocking individual IPs is a losing race. Block by behaviour (rate limiting, WAF rules) not by address.
Security through obscurity
Choosing an unusual domain, using a non-standard port, or avoiding search engine indexing does not reduce scanner traffic. Scanners work by sweeping entire IP ranges — they find your server before any search engine does. This site was probed within hours of DNS propagating.
Panicking at 404s
A scanner getting 404s is the correct, safe outcome. The goal of hardening is to ensure that nothing the scanner probes for actually exists on your server — and that even if it did, file permissions and nginx rules would prevent it from being served. 404s in your logs are information, not incidents.
05 Pre-Launch Checklist Summary
| Action | Attacks blocked | Effort |
|---|---|---|
No secrets in web root (.env, .git, config files) |
Credential harvesting | Low |
Block .php at nginx (if not needed) |
Webshell execution | Low |
| Block hidden files and config extensions at nginx | Config exposure | Low |
Set server_tokens off |
Version fingerprinting | Low |
| Add security headers (CSP, HSTS, X-Frame-Options) | XSS, clickjacking, MIME sniffing | Low |
Disable directory listing (autoindex off) |
File enumeration | Low |
| Add nginx rate limiting | Brute force, rapid scanning | Medium |
| Enable Cloudflare WAF managed rulesets | OWASP Top 10, bot traffic | Medium |
| Set correct file permissions (644 files, 755 dirs) | Local privilege escalation | Medium |
| Set up GoAccess or log monitoring before launch | Detection and response | Medium |
Block /xmlrpc.php and protect /wp-login.php (WordPress) |
Credential brute force, amplification | Medium |
The first five items in this table are zero-cost configuration choices. They close the doors that scanners most commonly find open — and they take under an hour to implement. Do them before you point DNS at your server, not after.
Published: — Data source: nginx access logs, nikokp.com, –