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.

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:

HTTP status code distribution in the first 48 hours after launch
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.

Key finding

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:

Configuration files probed for credential exposure
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 .env files one directory above the web root, never inside it
  • Add .env*, docker-compose*.yml, and *.conf to .gitignore before the first commit
  • Never deploy with a .git/ directory inside the web root — use a CI pipeline or rsync --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 *.php if 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:

WordPress hardening actions mapped to scanner probe targets
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-IP in 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

Pre-launch web security checklist — ordered by risk reduction impact
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
Bottom line

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.