Jellyfin Security Hardening 2026: Beyond Fail2Ban
If your Jellyfin server is accessible from the internet, it is being attacked. Right now. Automated bots scan the entire IPv4 space in hours looking for exposed services.
Fail2Ban is a good start - but it is reactive (bans after attacks) and isolated (each server learns independently). In 2026, better tools exist.
This guide covers a layered security approach that goes far beyond basic password protection.
The Security Stack
| Layer | Tool | What it does |
|---|---|---|
| 1. Network | GeoIP blocking | Block entire countries you never access from |
| 2. Edge | Rate limiting | Prevent brute-force at the reverse proxy |
| 3. Detection | CrowdSec | Collaborative IPS with crowd-sourced blocklists |
| 4. Authentication | 2FA + SSO | Second factor for all accounts |
| 5. Container | Docker hardening | Minimize attack surface |
| 6. Monitoring | JellyWatch + alerts | Real-time visibility |
Layer 1: GeoIP Country Blocking
If you only access your server from 2-3 countries, block everything else. This eliminates 80%+ of automated attacks instantly.
With Caddy + MaxMind GeoIP
# Caddyfile with geoip module
jellyfin.yourdomain.com {
@blocked {
not maxmind_geolocation {
db_path /etc/caddy/GeoLite2-Country.mmdb
allow_countries FR US DE GB CA
}
}
respond @blocked 403
reverse_proxy jellyfin:8096
}
With Nginx + ngx_http_geoip2_module
# In http block
geoip2 /etc/nginx/GeoLite2-Country.mmdb {
auto_reload 24h;
$geoip2_country_code country iso_code;
}
# In server block
if ($geoip2_country_code !~ ^(FR|US|DE|GB|CA)$) {
return 403;
}
With Cloudflare (easiest)
If you use Cloudflare Tunnel or proxy:
- Security → WAF → Custom Rules → Create Rule
- Expression:
(not ip.geoip.country in {"FR" "US" "DE" "GB" "CA"}) - Action: Block
Zero server-side configuration needed.
Layer 2: Rate Limiting
Prevent brute-force login attempts at the reverse proxy level - before they even reach Jellyfin.
Caddy rate limiting
jellyfin.yourdomain.com {
rate_limit {
zone jellyfin_login {
key {remote_host}
events 5
window 1m
}
}
reverse_proxy jellyfin:8096
}
Nginx rate limiting
# In http block
limit_req_zone $binary_remote_addr zone=jellyfin_login:10m rate=5r/m;
# In location block for login endpoint
location /Users/AuthenticateByName {
limit_req zone=jellyfin_login burst=3 nodelay;
proxy_pass http://jellyfin:8096;
}
This limits login attempts to 5 per minute per IP - legitimate users never hit this limit.
Layer 3: CrowdSec - The Modern Fail2Ban
CrowdSec is an open-source, collaborative intrusion prevention system. Unlike Fail2Ban:
- Crowd-sourced blocklists - when one CrowdSec user detects an attacker, ALL users benefit
- Behavioral detection - not just regex on logs, but scenario-based analysis
- Multi-service - one agent protects Jellyfin, SSH, Nginx, and more
- Remediation components - block at firewall, reverse proxy, or Cloudflare level
Docker Compose setup
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
environment:
- COLLECTIONS=crowdsecurity/linux crowdsecurity/nginx LePresidente/jellyfin
- GID=1000
volumes:
- ./crowdsec/config:/etc/crowdsec
- ./crowdsec/data:/var/lib/crowdsec/data
- /var/log:/var/log:ro
- ./jellyfin/config/log:/var/log/jellyfin:ro
restart: unless-stopped
crowdsec-bouncer:
image: fbonalair/traefik-crowdsec-bouncer:latest
container_name: crowdsec-bouncer
environment:
- CROWDSEC_BOUNCER_API_KEY=your_bouncer_key
- CROWDSEC_AGENT_HOST=crowdsec:8080
restart: unless-stopped
Install the Jellyfin collection
docker exec crowdsec cscli collections install LePresidente/jellyfin
This installs:
- Parser: understands Jellyfin log format
- Scenario: detects brute-force login attempts
- Blocklist: community-shared attacker IPs
How CrowdSec protects Jellyfin
- CrowdSec reads Jellyfin logs (
/var/log/jellyfin/) - Detects patterns: 5 failed logins from same IP in 1 minute
- Creates a decision: ban this IP for 4 hours
- The bouncer (at your reverse proxy) enforces the ban
- The attacker IP is shared with the CrowdSec community
- Other CrowdSec users preemptively block this IP
CrowdSec vs Fail2Ban
| Feature | CrowdSec | Fail2Ban |
|---|---|---|
| Detection | Behavioral scenarios | Regex on logs |
| Sharing | Crowd-sourced blocklists | Isolated per server |
| Performance | Go binary, fast | Python, slower |
| Multi-service | One agent, many services | One jail per service |
| Remediation | Multiple bouncers | iptables only |
| Dashboard | Yes (web console) | No |
| Docker native | Yes | Requires log access |
Layer 4: Authentication Hardening
Beyond 2FA (covered in our dedicated guide):
Disable password authentication for admin
Use SSO (Authelia/Authentik) as the only login method for admin accounts. Even if someone guesses the password, they cannot log in without the SSO provider.
API key rotation
Rotate your Jellyfin API keys quarterly:
- Dashboard → API Keys → Create new key
- Update all integrations (JellyWatch, Radarr, Sonarr, etc.)
- Delete the old key
Session timeout
Dashboard → Networking → set reasonable session timeouts for inactive users.
Layer 5: Docker Container Hardening
Run as non-root
jellyfin:
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
Limit capabilities
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
Network isolation
networks:
frontend: # Only reverse proxy connects here
backend: # Internal services only
Layer 6: Monitoring and Alerting
JellyWatch push notifications
JellyWatch alerts you on:
- Failed login attempts
- New device connections
- Unusual session patterns
- Server offline detection
CrowdSec dashboard
Access the CrowdSec web console to see:
- Blocked IPs in real time
- Attack patterns and origins
- Community threat intelligence
Uptime Kuma
Monitor your Jellyfin /health endpoint. Get alerted the moment your server goes down.
The Complete Hardened docker-compose.yml
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
networks:
- frontend
restart: unless-stopped
jellyfin:
image: jellyfin/jellyfin:latest
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
- /cache
security_opt:
- no-new-privileges:true
volumes:
- ./jellyfin/config:/config
- /mnt/media:/media:ro
devices:
- /dev/dri:/dev/dri
networks:
- frontend
restart: unless-stopped
crowdsec:
image: crowdsecurity/crowdsec:latest
environment:
- COLLECTIONS=crowdsecurity/linux LePresidente/jellyfin
volumes:
- ./crowdsec/config:/etc/crowdsec
- ./crowdsec/data:/var/lib/crowdsec/data
- ./jellyfin/config/log:/var/log/jellyfin:ro
networks:
- backend
restart: unless-stopped
networks:
frontend:
backend:
internal: true
volumes:
caddy_data:
Security Checklist
| Layer | Tool | Status |
|---|---|---|
| HTTPS | Caddy/Nginx + Let's Encrypt | ☐ |
| GeoIP blocking | Cloudflare or reverse proxy | ☐ |
| Rate limiting | Reverse proxy config | ☐ |
| IPS | CrowdSec + Jellyfin collection | ☐ |
| 2FA | TOTP plugin or Authelia | ☐ |
| Container hardening | non-root, read-only, no-new-privileges | ☐ |
| Monitoring | JellyWatch + Uptime Kuma | ☐ |
| API key rotation | Quarterly schedule | ☐ |
| Updates | Watchtower or manual weekly | ☐ |
FAQ
Is CrowdSec free? Yes. The Security Engine is open source and free. The community blocklists are free. Premium blocklists (more comprehensive) are paid.
Does GeoIP blocking affect VPN users? Yes. If a user connects via a VPN in a blocked country, they will be blocked. Whitelist your VPN provider's IP ranges if needed.
Can I use CrowdSec AND Fail2Ban? You can, but it is redundant. CrowdSec replaces Fail2Ban with better detection and crowd-sourced intelligence.
Does this slow down Jellyfin? No. GeoIP and rate limiting happen at the reverse proxy (before Jellyfin). CrowdSec reads logs asynchronously. Zero impact on playback.
Your server is hardened - now monitor it from your pocket. Download JellyWatch on Google Play - failed login alerts, session monitoring, and server health for Jellyfin.
On Emby? Download EmbyWatch on Google Play




Comments 2
Switched from Fail2Ban to CrowdSec last month. The crowd-sourced blocklists are the killer feature. My server was getting hit by IPs that other CrowdSec users had already flagged. Blocked before they even tried my login page.
GeoIP blocking at the Cloudflare level is the easiest win. I only access my server from 3 countries. Blocking everything else eliminated 90% of my failed login attempts overnight. Zero server-side configuration needed.
Leave a comment