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

  1. Scanners find you immediately. Within hours of any public deployment, automated tools will probe your site. This is normal. Plan for it.

  2. Default configurations are public knowledge. Every framework default (/admin/, /dashboard/, etc.) is in scanner wordlists. Changing them costs nothing and eliminates most noise.

  3. Rate limiting is essential infrastructure. Not just for DOS protection โ€” for keeping your analytics clean and your logs readable.

  4. 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.

  5. 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.