When the Scanners Came Knocking: A Production Security Retrospective
July 29, 2026 ยท Ahmad M. Waddah ยท 56 reads
๐ Stack: Django ยท Sentry ยท Render
Two days after putting my Django portfolio site live, I did what any responsible engineer should do: I checked the analytics.
What I found made me stop scrolling.
๐ The Numbers
| Metric | Value |
|---|---|
| Total page views in 48 hours | ~380 |
| Unique visitors | 22 |
| Automated scanners identified | 6 distinct IPs |
| Most aggressive scanner | 270 requests in 40 seconds |
The scanners weren't subtle. They were ruthlessly efficient โ dictionary attacks on common sensitive paths, rotating user agents, probing every .env permutation imaginable, checking for .git/config exposure, phpinfo leaks, cloud credential files, framework-specific paths. They didn't find anything useful because Django doesn't serve those paths, but the signal was clear: your site is on the radar within hours of going live.
๐จ The Response
I treated this as a real incident. Not because data was breached (it wasn't), but because the gaps it revealed were worth fixing immediately.
1๏ธโฃ Obscure All Private URLs
The default /admin/ path is in every scanner's wordlist. Same goes for /dashboard/, /analytics/, /stats/, etc. I moved all staff-only paths to non-obvious alternatives. This won't stop a targeted attacker, but it eliminates the vast majority of automated noise.
Lesson: Default paths are free intel for scanners. Change them.
2๏ธโฃ Rate Limiting Middleware
The 270-request scanner finished its run in 40 seconds โ nearly 7 requests per second. With rate limiting, it would have been cut off at 50 requests and blocked for the rest of the minute.
I implemented a simple Django middleware that tracks request timestamps per IP in memory and returns 429 Too Many Requests when the threshold is exceeded. It's configurable via settings (RATE_LIMIT_REQUESTS and RATE_LIMIT_WINDOW), and staff users are exempt.
Key design decisions: - Runs before the analytics middleware so blocked requests aren't logged - Uses in-memory tracking (no Redis dependency for a portfolio site) - Static files and staff sessions are whitelisted - Returns a proper HTTP 429 status code
3๏ธโฃ Removed .env.example from the Repo
Why would a scanner ask for /.env.example? Because many projects accidentally leave real credentials in their example files. Even though ours only contained placeholders, removing it eliminates a variable entirely.
Lesson: If it exists on disk and contains any hint of your stack, scanners will find it. Keep your repo lean.
4๏ธโฃ Fixed HSTS Headers
While auditing the production settings, I found a bug I'd introduced: SECURITY_HSTS_SECONDS instead of Django's correct SECURE_HSTS_SECONDS. One letter off meant no HSTS header was being sent โ browsers weren't being told to always use HTTPS.
A one-character typo that nullified an entire security feature. This is why code review exists.
5๏ธโฃ Updated robots.txt
The existing robots.txt allowed full crawl and pointed to a stale sitemap URL. I updated it to:
- Disallow all private paths
- Point to the correct sitemap at amw.work.gd
Polite crawlers respect robots.txt. Malicious ones ignore it. But there's no reason to make either group's job easier.
6๏ธโฃ Full Security Settings Audit
I reviewed every security-related setting in the Django configuration:
- โ
SECURE_SSL_REDIRECTโ HTTPS enforced - โ
SECURE_HSTS_SECONDSโ 1 year (now correctly named) - โ
X_FRAME_OPTIONS = "DENY"โ clickjacking protection - โ
CSRF_COOKIE_SECURE/SESSION_COOKIE_SECUREโ cookies over HTTPS only - โ
SECURE_CONTENT_TYPE_NOSNIFFโ MIME-type sniffing disabled - โ
SECURE_PROXY_SSL_HEADERโ correct proxy header for Render - โ
CSRF_TRUSTED_ORIGINSโ restricted to known domains
โ What Didn't Change
Some things worked exactly as designed and needed no adjustment:
- Django's security middleware handled every malicious path as a 404. No data leaked, no errors logged.
- Sentry monitoring was already in place and would have alerted on any real exceptions.
- Staff IP exclusion from analytics meant admin browsing wasn't polluting the data โ the scanner traffic was easy to spot because it wasn't mixed with my own.
๐ก Takeaways
-
Scanners find you immediately. Within hours of any public deployment, automated tools will probe your site. This is normal. Plan for it.
-
Default configurations are public knowledge. Every framework default (
/admin/,/dashboard/, etc.) is in scanner wordlists. Changing them costs nothing and eliminates most noise. -
Rate limiting is essential infrastructure. Not just for DOS protection โ for keeping your analytics clean and your logs readable.
-
Audit your production settings before launch. One wrong character can disable an entire security feature. Review your settings with fresh eyes, or better yet, have someone else review them.
-
Incident response doesn't require a breach. A near-miss is the cheapest education you'll ever get. Use it.
The site is more secure now than it was 48 hours ago. But this isn't a "set and forget" situation โ it's an ongoing practice. Next up: integrating a Web Application Firewall and setting up automated security scanning as part of the CI pipeline.