DiyMediaServer
Featured image of post How to Install and Configure Fail2Ban on your Jellyfin LXC

How to Install and Configure Fail2Ban on your Jellyfin LXC

Stop brute-force attacks on your Jellyfin server with Fail2Ban and not lock yourself out

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 start stuffing passwords, trying to guess their way in.

Ask me how I know.

When I first stood up Jellyfin and exposed it to the internet, I left the default jellyfin username in place. Bots found the login page almost immediately and hammered the account around the clock. I only noticed because Jellyfin’s per-user lockout kept tripping and I couldn’t log in to watch anything. I knew better. And there I was, staring at thousands of failed login attempts from IPs scattered across multiple countries.

So I changed the default username. That stopped the account lockouts. But the server itself was still getting probed every single day. I needed another layer. Fail2Ban was the obvious answer.

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.

💭
TL;DR:

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 you haven’t set it on a user account, an attacker can throw thousands of passwords at it per hour without consequence.

Jellyfin's Lockout setting

Fail2Ban operates globally. 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, failed login attempts look the same in the logs. That’s actually good news. One Fail2Ban config works for both scenarios.

Pre-Deployment Checklist

Answer these before touching any 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 --version and sudo 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?
📝
Note:

I’ll be configuring this on an unprivileged LXC running Debian 13 with nftables.

All examples below assume that setup.

Checking Your Firewall Backend

Modern Debian systems often run 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.
Get this wrong and 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 survives 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
MINISFORUM MS-01 Mini Workstation: Why it fits this post: This mini workstation is ideal for running Jellyfin in an LXC container and experimenting with Fail…
MINISFORUM MS-01 Mini Workstation This mini workstation is ideal for running Jellyfin in an LXC container and experimenting with Fail2Ban, offering enough power and networking for a secure, flexible homelab setup.
Amazon Price: Loading...
Availability: Checking...
Contains affiliate links. I may earn a commission at no cost to you.

The hardware doesn’t really matter for Fail2Ban itself. It runs happily on anything that can host the LXC. What matters next is the filter that tells it what a failed login actually looks like.

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 =
⚠️
Warning: Jellyfin log formats can change between versions. I learned this the hard way after an update when bans mysteriously stopped working. Always test your filter after upgrades.

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").

Zero matches means your regex is wrong or your log path is incorrect.

💡
Tip: Don’t skip this step. I’ve wasted hours debugging jails that would never work because the filters never matched anything.

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

That config means: 5 failures within 10 minutes earns a 20-minute ban. Tune to taste. Don’t make maxretry too low or bantime too long. You’ll lock yourself out the first time you mistype a password while trying to watch something away from home.

💡
Tip: backend = polling is needed because Jellyfin creates a new log file every 24 hours with a unique name. This lets fail2ban read these files.

That’s the basics of Fail2Ban when you’re exposing Jellyfin directly to the internet via port forwarding. I strongly recommend a reverse proxy rather than exposing Jellyfin directly.

I’ll cover the reverse-proxy setup in a follow-up post.

RaspberryPi 4GB: The Raspberry Pi is a budget-friendly, low-power option if you are wanting to test Fail2Ban and Jellyfin in a lightweight, isolated environment before deploying to larger servers.
RaspberryPi 4GB The Raspberry Pi is a budget-friendly, low-power option if you are wanting to test Fail2Ban and Jellyfin in a lightweight, isolated environment before deploying to larger servers.
Amazon Price: Loading...
Availability: Checking...
Contains affiliate links. I may earn a commission at no cost to you.

A spare Pi is a great place to break things before you touch the LXC running your real library. Once the jail behaves there, copy the configs over with confidence.

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 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 bantime finite. Infinite bans sound appealing, but they’re risky. You can 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?
Yes, if you exceed 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?
Yes, but verify the backend configuration matches your system. Modern Debian uses nftables, but Fail2Ban might still default to iptables compatibility mode. Check your jail config.
➤ Is Fail2Ban useful for local-only Jellyfin?
Not really. It matters when Jellyfin is exposed beyond your LAN.

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.

Understand your network topology, configure the correct jail and action, and monitor bans periodically. That’s how you turn Jellyfin from a soft target into a more secure service. This is the third step in hardening your Jellyfin LXC.

You should have already:

I learned this by watching bad actors 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

Protectli FW6C/FW6D: This dedicated firewall appliance can help you by implementing network-level protections and monitor traffic, complementing Fail2Ban's application-level security for a more robust homelab.
Protectli FW6C/FW6D This dedicated firewall appliance can help you by implementing network-level protections and monitor traffic, complementing Fail2Ban’s application-level security for a more robust homelab.
Amazon Price: Loading...
Availability: Checking...
Contains affiliate links. I may earn a commission at no cost to you.

Was this useful?

Last updated on May 20, 2026 06:56 MDT