DEV Community

疏影
疏影

Posted on

Why I rate-limit every self-hosted service (and you should too)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Install fail2ban on every public-facing host
  2. Configure SSH jail + 1-2 service-specific jails
  3. Put Cloudflare in front of anything user-facing
  4. Enable 2FA on every account
  5. 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)