← Harden & defendone hardened front door with nginx and a waf

one hardened front door with nginx and a waf

one door in, everything else locked

most people building their first app just... run it. node app on port 3000, flask on 5000, whatever, wide open to the internet with zero armor around it. that's fine for a weekend project, it's not fine for anything that touches real users or real data. the fix isn't complicated: put one hardened thing in front of everything else and never let traffic touch your apps directly.

that's what a reverse proxy setup with nginx does. it becomes the single front door. everything knocks there first, gets inspected, and only then gets let inside. your actual apps stop being reachable from the outside world at all.

tls termination, aka nginx handles the encryption headache

tls termination just means nginx is the thing that holds your ssl certificate and does the encrypt/decrypt work, not your app. your app never has to deal with certs, ciphers, or renewals. nginx takes the encrypted traffic from the internet, decrypts it, and passes clean http traffic to your app over localhost where nobody outside can see it anyway.

this matters for defense because cert management is a common place things rot. one nginx config to keep certs current (hello, certbot auto-renew) beats trying to keep tls current across five different apps written in three different languages.

load balancing so one app doesn't become one point of failure

if you're running more than one instance of an app, nginx can spread requests across them. from a security angle this isn't just about performance, it's about resilience. if one backend instance gets overwhelmed, crashes, or is mid-restart after a patch, nginx just routes around it instead of the whole site going down.

the waf is your actual bouncer

this is the part people skip and shouldn't. modsecurity paired with the owasp core rule set (crs) sits inside nginx and inspects every request before it reaches your app. it's looking for the classics: sql injection attempts, xss payloads, path traversal, weird encoded garbage trying to sneak past input validation.

a basic modsecurity block in your nginx config looks like this:

load_module modules/ngx_http_modsecurity_module.so;

server {
    listen 443 ssl;
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

the modsecurity on line flips the waf on for that server block. modsecurity_rules_file points at your ruleset, this is where the owasp crs rules live. everything under location / is what gets proxied through after inspection. malicious requests get blocked or logged before your app ever sees them, which means bugs in your own code matter a lot less if the attack never arrives.

apps only listen on localhost, period

this is the single biggest win in this whole setup and it costs nothing. your app binds to 127.0.0.1, not 0.0.0.0. that means it is physically unreachable from outside the machine, full stop. nginx talks to it over the loopback interface, nobody else can. even if your app has a nasty unpatched vuln, an attacker can't reach it directly because there's no route in except through the proxy.

check what your app is actually bound to right now:

sudo ss -tulpn | grep LISTEN

if you see your app's port bound to 0.0.0.0 instead of 127.0.0.1, that's an open door you didn't mean to leave open.

headers and rate limiting, the low effort high value stuff

security headers cost you a few lines in nginx and close off entire categories of browser based attacks. things like strict-transport-security, x-content-type-options, and a sane content-security-policy stop clickjacking, mime sniffing tricks, and a chunk of xss vectors before they even start.

rate limiting is your defense against brute force logins and basic scraping/dos attempts, using nginx's limit_req_zone to cap how many requests an ip can fire at your login page per minute.

the takeaway

the whole point here is reducing your attack surface down to one well guarded, well configured thing instead of a pile of loosely secured apps each fending for themselves. audit your own setup: check what's bound to a public interface, check if you have any waf inspecting requests at all, check if your headers are actually being sent. one hardened front door beats five unlocked ones every time.

watch the reel ↗
the weekly drop

one command a week that makes you harder to hack.

a single tool, explained in plain english, every week. straight to your inbox.

no spam. one email a week. unsubscribe anytime.