· Jesse Edwards · Case Studies · 4 min read
The Single Server Fortress: Pragmatic Defense in Depth
How to build professional grade security layers on a lean, single node stack using Rails and Kamal.

The Single Server Fortress: Pragmatic Defense in Depth
Building professional grade security on a lean, single node stack.
The Philosophy: Layers, Not Just Walls
In my time at JP Morgan Chase, we had the luxury of sprawling VPCs and dedicated security teams. In the startup world, I bring all the experience I have learned over the years to lock down the fort while staying lean.
Defense in Depth (DiD) is about ensuring that even if one layer has a gap, the next layer catches the threat. For RenovationRoute, we’ve built a “concentric circle” strategy that treats our single server like a fortress.
Layer 1: Network Locality (The “Ghost” Database)
The Reality: We run our App, Database, and Redis on a single production server.
The Defense:
- Zero Public Exposure: Our Postgres and Redis ports are not open to the internet. By not exposing these ports in our firewall and binding them internally, the database is “invisible” to anyone outside the server.
- Docker Internal Networking: The Rails app communicates with the database over a private Docker bridge network using internal hostnames (
rr-db).
The Result: Unless an attacker has a shell on the actual production hardware, they cannot even attempt to handshake with the database.
Layer 2: Host Hardening (The Foundation)
The Threat: Brute force SSH attacks or “Zero-Day” exploits in OS level packages.
Our Solution: Key-Only SSH, Whitelisted IPs through VPN, Fail2Ban
- No Passwords: We have disabled SSH password authentication entirely. No key, no entry.
- Whitelisted IP: Only one IP can communicate with the server.
- Fail2Ban: This is our OS level “bouncer.” If an IP starts sniffing for open ports or attempting failed handshakes, Fail2Ban drops their traffic at the firewall level before they even touch our application memory.
Layer 3: The Rails Gateway (Identity & Authentication)
The Threat: API scraping or unauthenticated users poking around sensitive endpoints.
Our Solution: Global Authentication Lockdown
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# We treat every request as hostile until proven otherwise.
before_action :authenticate_user!
endThe Impact: We treat Identity as the first barrier. By default, unauthenticated users see nothing, effectively hiding our data structure from the public.
Layer 4: Application Logic (The IDOR Killer)
The Threat: IDOR (Insecure Direct Object Reference). Example is when user A, who does not own a payment tries to access user B’s payment by guessing a URL ID.
Our Solution: UUIDs + Pundit Policies
UUIDs: We use random UUIDs instead of sequential integers (/payouts/1). This makes “walking the database” mathematically impossible.
Pundit: We use explicit Policy objects to verify every action.
# app/controllers/payouts_controller.rb
def approve
@payout = Payout.find(params[:id])
# Just because you're logged in doesn't mean you have access.
# We check: "Does this specific user have the appropriate role for this project?"
authorize @payout
@payout.release_funds!
endThe Full Stack Defense Table
| Layer | Component | Defense Mechanism | Purpose |
|---|---|---|---|
| Network | Docker Bridge | No Public Port Mapping | Isolate DB from the Internet |
| Host | Linux/EC2 | SSH Keys Only | Prevent Server Takeover |
| App Framework | Rails | Strong Params & UUIDs | Prevent SQLi & Discovery |
| Business Logic | Pundit | Policy-Based Auth | Prevent IDOR / Multi-tenant leaks |
| Identity | Devise | Lockable Accounts | Prevent Credential Stuffing |
What We’re NOT Doing (And Why)
Complex VPC Multi Node Networking
Reason: When your database and app live on the same box, a VPC adds latency and cost without a significant security gain for a small scale SaaS. We prefer the Network Locality of a single node setup until we need to scale horizontally. When We scale, will discuss the appropriate VPC and multi node architecture.
MFA (In Testing)
Reason: We are testing hardware key support. In security, a poorly implemented MFA is worse than no MFA, so we are taking the time to get the recovery flows right and test all of the edge cases before rolling it out to production. Last thing we want is to lock out legitimate users or create a bypass.
Conclusion: The Architect’s Mindset
Defense in Depth isn’t about having a 50 person security team. It’s about knowing where your risks are and layering defenses to cover them.
By keeping our database ports off the public internet and using explicit authorization policies, we’ve built a platform that is secure by design.
All systems are different, and the right security measures depend on the specific risks and architecture of your application. In my case part of my design goals for the architecture was to make it as secure as possible. I am dealing with real transactions.



