Note 018 min read
We’re Being Attacked — And Here’s How We Stopped It
- Author
- David Oyewale
- Revised
- Note
- 01 / 04
- Status
- Released
I want to tell you something that most SaaS founders don’t talk about publicly.
The moment you deploy your server to the internet before you’ve run a single ad, before your first user signs up, before you’ve even told anyone the product exists, something finds you.
Not a person. A bot.
Within hours of deploying SwiftDocu’s infrastructure, I was staring at server logs that made my stomach drop. Nearly 1,000 requests from unique IP addresses, all probing for the same things: .env files containing database credentials, .php vulnerabilities from ancient WordPress installations, SSH login endpoints. None of these things exist on SwiftDocu’s server. But the bots didn’t know that and they didn’t care. They were fishing in the dark, casting the same net at every server on the internet hoping something bites.
This is the quiet war that runs 24 hours a day on every public-facing server in the world. Most users never see it. Most founders don’t talk about it. But it’s real, it’s constant, and if you ignore it, it will eventually cost you either in performance, in a breach, or in a cloud bill that makes no sense until you trace it back to bot traffic you never blocked.
This is the story of how we hardened SwiftDocu’s infrastructure to fight back.
Why This Matters More Than You Think
Before I get into the technical details, I want to explain why I’m writing this at all. SwiftDocu handles professional documents like CVs, legal contracts, NDAs & more. When someone uses SwiftDocu, they’re entering their name, their work history and their personal information. That’s a serious trust relationship. The moment I saw those logs, I understood that security wasn’t a “future problem” to solve after we hit our first 1,000 users. It was a day one responsibility.
But there’s also a practical performance reason that’s easy to overlook.
SwiftDocu is built first for the Nigerian market a market where traffic can spike fast, especially when content goes viral on TikTok or gets shared in WhatsApp groups. If your server is already burning CPU and RAM processing thousands of junk bot requests, and then a legitimate traffic spike hits, you don’t just slow down, you crash. Real users trying to download a CV they just paid for get errors. That’s unacceptable.
So we needed to fix this. Not just to feel secure, but to keep the product fast and reliable for real people.
The Strategy: Stop Problems as Far Away From the App as Possible
The instinct when you first see bot traffic is to write application-level code to handle it. Maybe add some IP blocking logic to your API, or check user agents, or rate limit certain endpoints. This instinct is wrong or at least, it’s the last line of defense, not the first.
The principle we applied is called defense in depth. The idea is simple: build multiple layers of protection, each one catching what the previous layer missed, and position each layer as far away from your core application as possible. Every request that gets blocked at the outer edge is a request that never touches your app server, never consumes your resources, never becomes your problem.
We built three layers.
Layer One: Nginx as the First Filter
If you’re not familiar with how web servers work, here’s the short version: when a request comes into SwiftDocu, it first hits Nginx a reverse proxy that sits in front of the actual application. Nginx’s job is to receive incoming requests and route them to the right place. What most people don’t realize is that Nginx can also be a remarkably effective security filter before anything reaches the app.
The bots hitting our server were requesting paths that simply don’t exist things like /wp-admin, /.env, /config.php, /admin/login. These are legitimate paths on other platforms, but on SwiftDocu’s infrastructure they’re nonsense. Before our hardening work, Nginx would dutifully pass these requests through to the application layer, which would then have to process them, realize they don’t exist, and return an error. That processing costs CPU time and RAM. Multiply that by a thousand requests from a thousand IPs and suddenly your app is working hard doing absolutely nothing useful.
The fix was to reconfigure Nginx to define our routes strictly. We configured Nginx to strictly validate requests and reject anything undefined, which tells Nginx: if a request comes in for something that isn’t a defined API route or a known static file, kill it yourself right here with a hard 404. Don’t pass it to the app. Don’t even think about it. Just return a 404 and move on.
The result was immediate. The application server process went quiet. It stopped spending any energy on requests it had no business receiving. Nginx became a filter, not just a router and most of the junk traffic started getting killed at the outermost layer before it ever got close to our business logic.
Layer Two: Fail2Ban as the Automated Enforcer
Killing junk requests with a 404 is good. But there’s a more aggressive response available for IPs that keep trying and that’s where Fail2Ban came in.
Fail2Ban is a tool that watches your log files and automatically bans IP addresses that show suspicious patterns. Out of the box it works well for things like SSH brute-force attempts, but we needed something more tailored: we needed it to watch Nginx’s access logs looking for repeated invalid or suspicious request patterns, the signature of a bot scanning for vulnerabilities.
So we wrote a custom jail and filter. The filter uses a regular expression to identify lines in the Nginx access log that correspond to 404 and 405 responses. The jail defines the rules: after a small number of repeated suspicious requests within a short time window, Fail2Ban drops the hammer.
Writing that regular expression took longer than I expected. The challenge is surgical precision: you need an expression that catches bots reliably but doesn’t accidentally ban a real user who made a typo in a URL. Getting that balance right involved testing the regex against real log samples until I was confident it was identifying exactly what I wanted bot-pattern requests, not human mistakes.
We also ran into a technical issue during setup: Fail2Ban was defaulting to monitoring the system journal rather than polling the Nginx log files directly. The practical consequence was that bans were being applied with a delay sometimes several minutes after the suspicious activity started. we tuned Fail2Ban to respond in near real-time. The moment suspicious activity crosses our threshold, the clock stops.
Layer Three: Kernel-Level Blocking — The Brick Wall
Here’s where it gets satisfying.
When Fail2Ban bans an IP, it doesn’t just tell Nginx to ignore that address. It communicates directly with the Linux kernel’s firewall IPTables, managed through UFW on our setup and instructs it to drop all packets from that IP at the kernel level.
What this means in practice is beautiful in its completeness: the banned attacker sends a packet to our server. The Linux kernel receives it and drops it without a word. No response. No acknowledgment. No Nginx involved. No application involved. The server doesn’t talk to the attacker at all. From the attacker’s perspective, the server might as well not exist.
This is the most resource-efficient possible response to an attack: consuming essentially zero server resources while completely shutting out the bad actor. The packet arrives, the kernel drops it, the packet is gone. That’s it.
The Other Thing We Fixed: Root Access
While we were hardening the infrastructure, I also took the opportunity to address something I’d been meaning to do since early in the build: eliminating root SSH access.
The root user is the single most targeted SSH login in the world. Every bot that runs SSH brute-force attacks tries root first because it’s the highest-privilege account on any Linux system. If an attacker gets root access, the game is over, they have full control of everything.
The fix is straightforward but important: create a limited user account with sudo privileges for specific tasks, disable root SSH login entirely, and use SSH key authentication instead of passwords. Without a password to brute-force and without root login enabled, this entire attack vector closes.
It’s one of those changes that takes thirty minutes and eliminates a category of attack permanently.
What This Means for SwiftDocu Users
I didn’t write this blog post to show off technical work. I wrote it because I think it’s important for people using SwiftDocu to understand how seriously we take the responsibility of handling their documents and their data.
The performance improvement is real: by filtering roughly 75% of junk traffic at the Nginx level before it reaches the application, SwiftDocu runs faster for legitimate users. Every resource that used to go toward processing bot requests now goes toward generating your CV or contract faster.
The security improvement is equally real: your documents and your personal information are hosted on an infrastructure with multiple hardened layers between it and the internet. Fail2Ban is watching. The kernel firewall is watching. Nginx is watching. The application server doesn’t see any of this fight — it just focuses on doing its job.
And from a cost perspective: bot-driven resource consumption on cloud infrastructure is a real problem that inflates bills unpredictably. By controlling what reaches the application layer, we keep infrastructure costs stable and predictable, which means we can keep SwiftDocu’s per-document pricing fair.
The Bigger Lesson
The most important thing I learned from this process has nothing to do with Nginx or Fail2Ban specifically. It’s a principle: application code should never be responsible for infrastructure security.
I’ve seen codebases where rate limiting, IP blocking, and bot detection are handled inside the application itself in the same layer where business logic lives. This is a mistake. It couples two completely separate concerns, makes both harder to reason about, and means your app has to process a request before it can decide to reject it. That’s wasteful and fragile.
Infrastructure security belongs at the infrastructure layer. Application code belongs in the application layer. Keeping those concerns separate what engineers call separation of concerns makes both simpler, more reliable, and easier to improve independently.
That principle, more than any specific tool or configuration, is what made this hardening work effective.
SwiftDocu is a product built to solve a real problem for professionals getting access to professional, affordable documents without the stress and cost of traditional methods. Building it well means not just making it work, but making it trustworthy. This was a step toward that.
If you’ve been thinking about trying SwiftDocu, now you know a little more about what’s running under the hood.