I learned this the hard way: an exposed Jellyfin or Vaultwarden instance without rate-limiting is a free buffet for credential stuffing bots.
This is the setup I now apply to every public-facing self-hosted service.
What rate-limiting actually prevents
- Brute-force login attacks: bots trying common passwords on /login endpoints
- API abuse: scrapers hitting your Immich or Paperless APIs at 100 req/s
- Credential stuffing: stolen username/password lists from other breaches
- Resource exhaustion: a single client (or botnet) eating your RAM and CPU
For me, the biggest win was preventing my Vaultwarden instance from appearing in Have I Been Pwned's "exposed" lists.
The tool: fail2ban
I use fail2ban on every public-facing service. It watches log files, detects failed login patterns, and bans the offending IP at the firewall level.
For Synology DSM users: fail2ban is available as a Docker container. Bind-mount /var/log from your DSM and run a customized jail that watches Synology's auth log.
For Linux homelabs: install fail2ban via apt (apt install fail2ban), write a jail.local with your services, done.
Generic jail config that works for most things
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
This catches SSH brute force and basic HTTP auth abuse. Specific apps need their own jails.
App-specific jails I run
Jellyfin — write a filter that matches "Failed login attempt" log lines, ban after 3 attempts.
[jellyfin]
enabled = true
port = 8096
filter = jellyfin
logpath = /var/log/jellyfin/jellyfin.log
maxretry = 3
bantime = 24h
Vaultwarden — already has built-in rate limiting, but I layer fail2ban on top for the /api endpoint.
Plex — Plex has its own brute-force protection now. If you reverse-proxy through Nginx, add limit_req to the location block to cap requests per second.
Nextcloud — Nextcloud has built-in bruteforce protection (configurable threshold). Disable fail2ban's Nextcloud-specific jail if you enable Nextcloud's built-in one (don't double-ban legitimate users).
Reverse-proxy level: Nginx limit_req
Beyond fail2ban, I rate-limit at Nginx too. Defense in depth.
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /api/auth {
limit_req zone=login burst=10 nodelay;
proxy_pass http://backend;
}
}
This caps login attempts at 5 per minute per IP, with burst of 10. Bots trying 1000 passwords/sec get throttled immediately.
The Cloudflare middleman
For anything I really care about, I put it behind Cloudflare. Free tier includes:
- DDoS protection
- Bot management (challenge suspicious IPs)
- Rate limiting rules (5 failed logins from same IP in 1 minute → JS challenge)
- Hide origin IP from public DNS
You don't need to pay for Cloudflare Workers. The free plan handles most homelab abuse.
Drawback: depending on your threat model, Cloudflare becomes a single point of failure (and sees all your traffic). For purely personal stuff this is fine. For whistleblowers or activists, run your own reverse proxy with no Cloudflare.
What I monitor
I log every ban event to a centralized Loki/Grafana setup. If I see a sustained attack pattern from a /16 IP range, I escalate the bantime to 24h or block the entire range with iptables.
Quick check that this is working:
sudo fail2ban-client status
sudo fail2ban-client status sshd
This shows the jail status, currently banned IPs, and total ban count.
What this DOESN'T protect against
- Zero-day exploits in your service (e.g., a Vaultwarden CVE you haven't patched)
- Compromised credentials that pass brute-force check (e.g., the actual password is "Summer2024!")
- Attacks via Tailscale/WireGuard VPN (where the attacker is "inside" already)
For zero-days, the answer is "patch faster." For compromised credentials, the answer is 2FA on every account. For VPN-only exposure, rate-limiting matters less because VPN auth is already gated.
What's the cost of doing this?
- fail2ban: $0, 30 minutes to set up
- Nginx limit_req: $0, 1 hour to configure per service
- Cloudflare free tier: $0
- Loki/Grafana monitoring: $0 if you have spare RAM
Total: $0 and ~3 hours one-time setup. The benefit is that my services don't get pwned. The cost-benefit ratio is absurd.
TL;DR for the time-constrained
- Install fail2ban on every public-facing host
- Configure SSH jail + 1-2 service-specific jails
- Put Cloudflare in front of anything user-facing
- Enable 2FA on every account
- Patch your services monthly (Watchtower auto-updates Docker containers)
If you do just those 5 things, you're already safer than 95% of self-hosters.
Top comments (0)