Lion Cipher Lab
Rate-limit a login endpoint with nginx limit_req, and measure what it really stops
- Cyber / Security Engineer
- Intermediate
- ~60 min
- Free · no account required
Why it matters
Every login page on the internet gets hammered by scripts trying passwords, and the cheapest control you can add sits in front of the app: a rate limit at the reverse proxy. nginx's limit_req does it in about four lines, and it is in nearly every hardening guide. But four lines you copied are not a control you understand. This lab makes you measure it. First you send a burst of logins at an unprotected backend and count how many arrive. Then you switch the limit on and count again: how many got a 429, how many still got through, and how much waiting the burst queue added. That turns rate,…
What you'll learn
- putting nginx (official Docker image) in front of a backend as a reverse proxy on a private Docker network, published only on 127.0.0.1
- measuring a baseline honestly: counting requests at the BACKEND, not at the client (real run: 200 of 200 wrong-password logins reached it in 2.33s, about 86 per second)
- configuring ngx_http_limit_req_module: limit_req_zone keyed on $binary_remote_addr, rate in r/m, burst, nodelay, delay=N, and limit_req_status 429 instead of the default 503
- reading the leaky bucket from your own numbers: with rate=60r/m burst=5, six requests get through a burst and then one per second (real 20-second run: 25 reached the backend out of 2,221 sent)
- telling nodelay from the default delay with latencies: nodelay forwarded the six at once (0.01-0.03s); the default queued them at 0.03, 1.01, 2.01, 3.00, 3.99, 4.98 seconds
- proving the shared-NAT failure: every request arrives from one address, so one noisy client makes the next innocent request get a 429
- showing the X-Forwarded-For trap (realip trusting 0.0.0.0/0 let a spoofing client through 200 of 200) and fixing it with set_real_ip_from limited to the proxy you actually run
- placing a proxy rate limit correctly next to account lockout (pam_faillock), credential-stuffing defences plus MFA, and log-driven bans (fail2ban)
Steps in this lab
- What you are building, and what a rate limit is (and is not)
- Set up: Docker running, a lab folder, a terminal
- Write the login backend and the burst client
- Start the backend and an unprotected nginx in Docker
- Measure BEFORE: how fast can a script hit the backend?
- Turn on limit_req (nodelay) and measure AFTER
- nodelay vs the default delay, in numbers
- Honest negative 1: one address, one bucket (shared NAT) and distributed sources
- Honest negative 2: trusting X-Forwarded-For lets the client pick its own key -- then fix it
- Honest negative 3: a rate limit is not account lockout, MFA, or a ban
- Do it better, then clean up