The phrase “defense in depth” sounds like something from a corporate security audit, the kind of document that arrives as a 200-page PDF and recommends solutions that cost more than your entire infrastructure. But the core idea is simple and scales down surprisingly well: don’t rely on any single security measure, because every measure eventually fails.
I run a small VM. One machine, a handful of services, nothing that would interest a sophisticated attacker. That last assumption is exactly the kind of thinking that gets systems compromised. Automated scanners don’t care how interesting you are. They probe everything, constantly, looking for the path of least resistance. Being small doesn’t make you safe; it just makes you a softer target.
The outer shell
The first layer is network-level filtering. I use UFW because life is short and iptables syntax is not how I want to spend it. The default policy denies everything inbound except SSH and the ports my services actually need. This sounds obvious, but I’ve seen machines running with dozens of ports exposed simply because someone installed a package that started a listener and nobody noticed.
SSH itself gets hardened beyond the defaults. Password authentication is disabled entirely — keys only. Root login is forbidden. I run it on a non-standard port, which provides zero actual security but does reduce log noise from automated scanners hitting port 22. The real protection comes from fail2ban, which watches authentication logs and temporarily bans IPs that fail repeatedly. It’s not sophisticated, but it handles the bulk of opportunistic attacks without any ongoing attention from me.
The middle layers
Behind the firewall sits a reverse proxy. Everything HTTP goes through Caddy, which handles TLS termination and provides a consistent entry point. This means my actual applications never touch raw internet traffic directly. If there’s a vulnerability in how one of them parses HTTP requests, the attacker first has to get past Caddy’s parser. That’s not bulletproof — vulnerabilities in reverse proxies happen — but it’s another layer.
Each service runs in its own container with minimal privileges. No root inside the container, read-only filesystems where possible, explicit network isolation so containers can’t talk to each other unless I’ve specifically allowed it. This containment won’t stop a determined attacker who finds a kernel exploit, but it slows down lateral movement and limits the blast radius of a compromise in any single service.
The inner keep
The innermost layer is about limiting damage when — not if — something gets through. Secrets live in a password manager, not in environment files scattered across the filesystem. Backups run automatically and go off-machine. Logs stream to a separate location so an attacker can’t simply delete their tracks. I keep the system updated, not because patches are perfect, but because known vulnerabilities are the easiest ones to exploit.
None of these measures is individually impressive. That’s the point. Defense in depth isn’t about finding the one perfect security solution. It’s about accepting that every solution has gaps and arranging your defenses so the gaps don’t line up. An attacker who bypasses the firewall still faces the reverse proxy. Past the proxy, they hit container isolation. Inside the container, there are no credentials to steal because they’re not stored there.
Small systems can’t afford dedicated security teams or expensive tooling. What they can afford is thoughtful layering — a series of small obstacles that individually mean little but collectively make compromise significantly harder. The goal isn’t to be impenetrable. It’s to be more annoying to attack than the next target on the scanner’s list.