Guides

fail2ban for Jellyfin

Jellyfin logs every rejected login with the client's IP, which is all fail2ban needs. The catch is that Jellyfin starts a new log file every day — this guide shows a setup that doesn't go blind at midnight.

Updated 6 min readChecked against fail2ban 1.1.1

To protect Jellyfin with fail2ban, create a filter that matches Authentication request for … has been denied (IP: "<ADDR>"), then enable a jail that reads Jellyfin's log — either the daily files /var/log/jellyfin/log_*.log (native) or /config/log/log_*.log (Docker), or better the systemd journal, which avoids missing the new log file Jellyfin starts every day. For Docker, ban in the DOCKER-USER chain.

/etc/fail2ban/filter.d/jellyfin.conf
[Definition]
failregex = ^.*Authentication request (?:for .+ |without username )has been denied \(IP: "?<ADDR>"?\)\.\s*$
/etc/fail2ban/jail.d/jellyfin.local
[jellyfin]
enabled      = true
backend      = systemd
journalmatch = _SYSTEMD_UNIT=jellyfin.service
filter       = jellyfin
port         = 8096,8920
maxretry     = 5
findtime     = 1h
bantime      = 1d

That jail is for a native install (Debian/Ubuntu package or the official install script) running as jellyfin.service. Docker and file-based variants follow below.

What Jellyfin logs on a failed login#

Jellyfin's user manager logs rejected logins at the Information level. In the log files the user name and IP are quoted:

log
[2026-10-25 18:36:10.412 +00:00] [INF] [57] Jellyfin.Server.Implementations.Users.UserManager: Authentication request for "admin" has been denied (IP: "203.0.113.42").
[2026-10-25 18:36:11.002 +00:00] [INF] [57] Jellyfin.Server.Implementations.Users.UserManager: Authentication request without username has been denied (IP: "2001:db8::25").

On the console (what systemd's journal and docker logs capture), Jellyfin uses a different template that prints the same values without quotes:

log
[18:36:14.002] [INF] [57] Jellyfin.Server.Implementations.Users.UserManager: Authentication request for admin has been denied (IP: 192.0.2.15).

The filter's "? makes the quotes optional, so one filter fits both. It's anchored to the end of the line, where Jellyfin puts the real address: a user name crafted to contain a fake has been denied (IP: …) can't redirect the ban to someone else — we tested exactly that case with fail2ban-regex, along with both formats above. Successful logins (has succeeded) and disabled-account messages don't match.

The log level must stay at Information (the default in logging.json); at Warning or Error these lines are not written.

The daily log file trap#

Jellyfin writes to log_YYYYMMDD.log and starts a new file every day (keeping three by default). A jail with logpath = /var/log/jellyfin/log_*.log looks right, but fail2ban expands the wildcard only when the jail starts. The next day Jellyfin writes to a file fail2ban isn't watching, and the jail silently stops counting. Three ways around it:

  1. Read the journal (native installs): the jail at the top of this page. No files, no rotation problem. Needs python3-systemd (sudo apt install python3-systemd). See backends and the systemd journal.
  2. Docker with the journald log driver: shown below.
  3. Log files plus a daily reload: keep the file-based jail and reload it shortly after midnight so the new file is picked up:
/etc/fail2ban/jail.d/jellyfin.local
[jellyfin]
enabled  = true
backend  = auto
filter   = jellyfin
logpath  = /var/log/jellyfin/log_*.log
port     = 8096,8920
maxretry = 5
findtime = 1h
bantime  = 1d
/etc/cron.d/fail2ban-jellyfin
5 0 * * * root /usr/bin/fail2ban-client reload jellyfin >/dev/null

The time in the cron line is the host's local time; Jellyfin starts the new file at its own midnight, so keep both on the same timezone.

Jellyfin in Docker#

In the official jellyfin/jellyfin and the LinuxServer images, logs are in /config/log/ inside the container, so on the host they're under whatever directory you mounted at /config. The cleanest option is again to skip the files and send the container's console output to the journal:

compose.yaml
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    restart: unless-stopped
    ports:
      - "8096:8096"
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /srv/media:/media:ro
    logging:
      driver: journald
/etc/fail2ban/jail.d/jellyfin.local
[jellyfin]
enabled      = true
backend      = systemd
journalmatch = CONTAINER_NAME=jellyfin
filter       = jellyfin
banaction    = iptables-allports
chain        = DOCKER-USER
maxretry     = 5
findtime     = 1h
bantime      = 1d

chain = DOCKER-USER is required because Docker forwards the published port to the container and the default ban in INPUT never sees that traffic; iptables-allports avoids having to know the container port. If you prefer the log files, use logpath = /opt/jellyfin/config/log/log_*.log with backend = auto and the daily reload from above. Background: fail2ban with Docker containers.

Jellyfin behind a reverse proxy#

Behind nginx, Caddy, Traefik or Nginx Proxy Manager, Jellyfin logs the proxy's address unless you tell it to trust the proxy. In the Jellyfin dashboard, go to Networking and add the proxy's IP (or the Docker network) to Known proxies; Jellyfin then uses the X-Forwarded-For header the proxy sends. Check a new denied-login line: it must show a public address, not 127.0.0.1 or 172.x.x.x. More on this in fail2ban bans the proxy IP.

When the proxy terminates TLS, the ban must cover the proxy's ports (80 and 443) rather than 8096. With iptables-allports that's automatic; with a port-based action set port = http,https. Jails on the proxy's own access log (like the 4xx filter in the nginx guide) can complement this, but Jellyfin's log is the precise source for failed logins.

Test it#

bash
# file-based setup
sudo fail2ban-regex /var/log/jellyfin/log_$(date +%Y%m%d).log /etc/fail2ban/filter.d/jellyfin.conf

# journal setup (native or Docker)
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/jellyfin.conf -m 'CONTAINER_NAME=jellyfin'

sudo fail2ban-client status jellyfin

For a native install use -m '_SYSTEMD_UNIT=jellyfin.service'. Then sign in with a wrong password from a phone on mobile data until sudo tail /var/log/fail2ban.log shows [jellyfin] Ban. TVs and apps with a stale saved password can trigger bans for your own household, so put your LAN in ignoreip if Jellyfin is also used locally.

Jellyfin itself can also disable a user account after a number of failed logins (a per-user setting). That protects the password but lets an attacker lock real users out; fail2ban blocks the attacker's address before it gets that far.

FAQ#

What is the fail2ban filter for Jellyfin?#

fail2ban doesn't ship one. Create /etc/fail2ban/filter.d/jellyfin.conf with failregex = ^.*Authentication request (?:for .+ |without username )has been denied \(IP: "?<ADDR>"?\)\.\s*$. It matches Jellyfin's denied-login lines in both the log files (quoted IP) and console output (unquoted).

Where are the Jellyfin logs?#

Native Linux installs write daily files to /var/log/jellyfin/ (named log_YYYYMMDD.log); Docker images use /config/log/ inside the container. Console output also goes to the systemd journal (journalctl -u jellyfin) or to docker logs, which fail2ban can read with the systemd backend.

Why does my Jellyfin jail stop banning after a day?#

Jellyfin starts a new log file every day, and fail2ban expands log_*.log only when the jail starts. Read the systemd journal instead (backend = systemd, journalmatch = _SYSTEMD_UNIT=jellyfin.service or CONTAINER_NAME=jellyfin), or reload the jail daily with fail2ban-client reload jellyfin.

How do I use fail2ban with Jellyfin in Docker?#

Run fail2ban on the host, give the container the journald log driver and use journalmatch = CONTAINER_NAME=jellyfin, or mount /config and point logpath at config/log/log_*.log. Ban with banaction = iptables-allports and chain = DOCKER-USER, because Docker-forwarded traffic bypasses the INPUT chain.

Why does Jellyfin log 127.0.0.1 or a Docker IP for failed logins?#

A reverse proxy is in front and Jellyfin doesn't trust it. Add the proxy's IP or network under Dashboard → Networking → Known proxies so Jellyfin reads the client address from X-Forwarded-For. Otherwise fail2ban would ban your own proxy.

Spotted something wrong or out of date? Tell us — every page is checked against the current fail2ban release.