If you’ve exposed Jellyfin to the internet, whether through port forwarding or a reverse proxy, you’re already being scanned. Bots see an open login page and immediately start stuffing passwords trying to guess their way in.
Ask me how I know.
I had the default username jellyfin when I first setup and exposed Jellyfin to the internet. Once I did this that the account was constantly being hammered by bots that found the login page. I found it because the account would get locked by Jellyfin and I couldn’t login to watch anything. I knew better but, here I was looking at thousands of failed login attempts from IPs spread across multiple countries.
I immediately changed the default username. This helped the account was no longer getting locked out. However, the Jellyfin server was still being assaulted by bots on a daily basis. So, I knew I needed an additional layer of security. Fail2Ban was the easy choice.
This guide covers installing and configuring Fail2Ban for a Jellyfin LXC without banning yourself. We’ll cover direct exposure, reverse proxy setups, Docker-in-LXC gotchas, and monitoring bans over time.
Fail2Ban watches Jellyfin logs for failed login attempts and automatically blocks abusive IPs. With the right jail, filter, and network configuration, you can stop brute-force attacks in their tracks even behind a reverse proxy all without locking yourself out.
Quick Start for Experienced Users:
Create filter /etc/fail2ban/filter.d/jellyfin.conf
Create jail /etc/fail2ban/jail.d/jellyfin.local
Verify firewall backend (iptables or nftables)
Configure reverse proxy trust if needed
Test with fail2ban-regex
Why Fail2Ban Protects Your Jellyfin Server
Here’s the thing: Jellyfin has authentication and its rate limit lockout is per user not global. If it isn’t set on a user account an attacker can try thousands of passwords per hour without consequence.
Fail2Ban does this globally, and you don’t have to set it per user.
Fail2Ban does this by:
- Watching log files for suspicious patterns
- Counting repeated failures from the same IP
- Adding firewall rules to block that IP for a set time
For context, the Jellyfin default HTTP port is 8096 and the default HTTPS port is 8920. Whether you expose these directly or run Jellyfin behind a reverse proxy on port 443 (HTTPS), failed login attempts look the same in the logs. That’s actually good news. It means one Fail2Ban config works for both scenarios.
Pre-Deployment Checklist
Answer these before touching any of your configs. Trust me, five minutes now saves an hour of troubleshooting later:
- Exposure method: Direct port forwarding or reverse proxy?
- Jellyfin port: Using default 8096 or custom?
- Firewall backend: iptables or nftables? (Check with
sudo iptables --versionandsudo nft --version) - Container setup: Jellyfin directly in LXC or Docker-in-LXC?
- Reverse proxy: If yes, are real client IPs logged?
- Trusted networks: What IPs should never be banned?
I’ll be configuring this on an unprivileged LXC running Debian 13 with nftables.
So, all of my examples will be for this setup.
Checking Your Firewall Backend
Modern Debian systems often use nftables with iptables as a compatibility layer.
You need to know which one Fail2Ban will actually use:
# Check what's actually running
sudo systemctl status nftables
sudo systemctl status netfilter-persistent
Fail2Ban works with both, but the actions you configure will differ.
Getting this wrong means bans won’t actually happen.
Installing Fail2Ban on Debian/Ubuntu LXC
Alright, let’s get Fail2Ban installed:
sudo apt update
sudo apt install fail2ban
sudo systemctl status fail2ban
If you see active (running), you’re golden. Fail2Ban starts automatically and will survive reboots.
Example:
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
Active: active (running) since Wed 2025-12-31 11:22:01 MST; 20h ago
Invocation: 9a7ce4195aaa4dc48dc750363bb9b954
Docs: man:fail2ban(1)
Main PID: 329267 (fail2ban-server)
Tasks: 13 (limit: 7051)
Memory: 15.7M (peak: 24M)
CPU: 4min 37.380s
CGroup: /system.slice/fail2ban.service
└─329267 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
Dec 31 11:22:01 racknerd-ea37d8f systemd[1]: Started fail2ban.service - Fail2Ban Service.
Dec 31 11:22:01 racknerd-ea37d8f fail2ban-server[329267]: Server ready
Creating the Jellyfin Fail2Ban Filter
Filters live in /etc/fail2ban/filter.d/ and tell Fail2Ban what a “failed login” looks like in your logs.
sudo nano /etc/fail2ban/filter.d/jellyfin.conf
Add this:
[Definition]
failregex = ^.*Authentication request for .* has been denied \(IP: "<ADDR>"\)\.
ignoreregex =
Test the Filter
Before going any further:
- Make sure to have some failed logins today
- Test the filter against your actual logs
- REPLACE: YYYYMMDD with today’s date
sudo fail2ban-regex /var/log/jellyfin/jellyfinYYYYMMDD.log /etc/fail2ban/filter.d/jellyfin.conf --print-all-matched
You should see matched lines with IP addresses highlighted.
Example:
| [2025-12-31 07:22:27.102 -07:00] [INF] Authentication request for "test@test" has been denied (IP: "140.32.72.74").
| [2025-12-31 07:22:28.787 -07:00] [INF] Authentication request for "test@test" has been denied (IP: "140.32.72.74").
| [2025-12-31 07:41:08.539 -07:00] [INF] Authentication request for "test@test" has been denied (IP: "140.32.72.74").
| [2025-12-31 07:41:10.368 -07:00] [INF] Authentication request for "test@test" has been denied (IP: "140.32.72.74").
If you see zero matches, your regex is wrong or your log path is incorrect.
Creating the Jellyfin Jail Configuration
Jails define what happens when the filter finds matches.
sudo nano /etc/fail2ban/jail.d/jellyfin.local
Base Configuration (Without a Reverse Proxy)
[jellyfin]
enabled = true
filter = jellyfin
logpath = /var/log/jellyfin/jellyfin*.log
backend = polling
maxretry = 5
findtime = 10m
bantime = 20m
port = 8096,8920
protocol = tcp
action = nftables[type=multiport]
ignoreip = 127.0.0.1/8 ::1
# Optional: add YOUR LAN subnet to ignoreip (recommended)
# Examples (pick the one that matches your network, or use the exact /24 you use)
# 10.0.0.0/8
# 172.16.0.0/12
# 192.168.0.0/16
This means: 5 failures within 10 minutes results in a 20-minute ban. Adjust these to taste, but don’t make maxretry too low or the bantime too long, you could lock yourself out when you mistype your password while trying to watch something away from home.
backend = polling is needed because Jellyfin creates a new log file every 24 hours with a unique name. This allows fail2ban to read these files.
This is basics of Fail2Ban used when you are exposing your Jellyfin directly to the internet via port forwarding. I highly Recommend a Reverse Proxy rather than exposing Jellyfin directly to the internet.
I will cover more advanced setups like reverse proxies at a later date.
Monitoring and Maintenance
Watch Ban Activity
Want to see bans happen in real-time? This is oddly satisfying:
sudo tail -f /var/log/fail2ban.log
You’ll see IPs getting banned as they trip your thresholds. After the first few, you realize just how many bots are constantly probing your server.
Unban Yourself
Locked yourself out? It happens. From the LXC console:
sudo fail2ban-client set jellyfin unbanip 192.168.1.50
Replace with your actual IP. You’ll be unbanned immediately.
Best Practices for Fail2Ban Management
- Keep
bantimefinite. Infinite bans sound appealing, but they’re risky. You could ban yourself permanently - Test filters after every Jellyfin update. Log formats change
- Review ban logs monthly for patterns. If you’re getting hammered from specific countries, consider additional firewall rules
- Document your unban procedure somewhere you can access when locked out
Troubleshooting Common Fail2Ban Issues
No bans occur: Wrong filter regex, wrong logpath, or Jellyfin’s logging level is too low. Use fail2ban-regex to debug. Also check that Fail2Ban is actually reading the log file, permissions matter.
Locked out completely: Access your LXC console (not SSH—that’s blocked too), unban manually, then add your IP to ignoreip in the jail config.
FAQs
➤ Will Fail2Ban ban my own IP?
maxretry. Use ignoreip for your home network or keep the unban command handy. I’ve locked myself out more times than I’d like to admit.➤ What's the difference between bantime and findtime?
findtime is the detection window, how far back Fail2Ban looks for failures. bantime is how long the ban lasts. So with findtime = 600 and maxretry = 3, you get banned if you fail 3 times within 10 minutes. The ban then lasts for bantime seconds.➤ Does Fail2Ban work with nftables?
➤ Is Fail2Ban useful for local-only Jellyfin?
Conclusion: Harden Your Jellyfin Installation
If your Jellyfin instance is reachable from the internet, Fail2Ban is essential. Brute-force attempts are constant and invisible until you check your logs. I was shocked when I first looked, thousands of attempts per day from IPs all over the world.
By understanding your network topology, configuring the correct jail and action, and monitoring bans periodically, you turn Jellyfin from a soft target into a more secure service. This is just the third step in hardening your Jellyfin LXC.
You should have already:
- Be using an unprivileged LXC - LXC Guide for Jellyfin
- Set Firewall Rules
- Secured your SSH - How to secure SSH
- Now Fail2Ban
I learned this by watching bad actors trying to brute-force my server for days. Now they get five tries and a timeout. Yours should too.
Next steps:
- Pair Fail2Ban with HTTPS and strong passwords
- Add a reverse proxy like NGINX or Caddy
Resources