Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
aradotso avatar

Jellyfin Security Plugin

  • 1 installs
  • 10 repo stars
  • Updated August 4, 2026
  • aradotso/security-skills

Jellyfin Security Plugin is a Claude skill for installing and configuring a server-side Jellyfin plugin that adds 2FA, passkeys, SSO, brute-force protection, and audit logging.

About

Jellyfin Security Plugin adds authentication and hardening to Jellyfin media servers (10.11+) through a server-side plugin. It provides TOTP, passkeys, email OTP, OIDC single sign-on, IP-based brute-force protection, TV device pairing, LAN bypass, and audit logging. A developer uses it to install and configure the plugin so security is enforced server-side across all Jellyfin clients and integrations.

  • Adds 2FA, passkeys, and SSO to Jellyfin media servers via a server-side plugin
  • Brute-force IP banning, device pairing, and audit logging
  • Server-side enforcement works across all Jellyfin clients

Jellyfin Security Plugin by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #1,834 of 2,203 Security skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
At a glance

jellyfin-security-plugin capabilities & compatibility

Free plugin; requires a self-hosted Jellyfin server and optional external OIDC provider.

Capabilities
authentication · security audit
Use cases
security audit
Platforms
Linux
Pricing
Free
From the docs

What jellyfin-security-plugin says it does

JellyfinSecurity is a comprehensive authentication and hardening plugin for Jellyfin media servers (10.11+).
SKILL.md
npx skills add https://github.com/aradotso/security-skills --skill jellyfin-security-plugin

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars10
Last updatedAugust 4, 2026
Repositoryaradotso/security-skills

What it does

Add two-factor authentication, SSO, and brute-force protection to a Jellyfin media server via a server-side plugin.

Who is it for?

Hardening a self-hosted Jellyfin media server with 2FA, SSO, and brute-force protection enforced server-side.

Skip if: Non-Jellyfin applications or code-level security auditing.

When should I use this skill?

You want to add 2FA, passkeys, SSO, or brute-force protection to a Jellyfin server.

What you get

A hardened Jellyfin server with multi-factor auth, SSO, IP banning, and audit logging enforced for all clients.

  • Installed JellyfinSecurity plugin
  • Configured 2FA/SSO/brute-force settings
  • Login and config-change audit log

By the numbers

  • 8 defined trigger phrases
  • requires Jellyfin 10.11+

Files

SKILL.mdMarkdownGitHub ↗

Jellyfin Security Plugin

Skill by ara.so — Security Skills collection.

What It Does

JellyfinSecurity is a comprehensive authentication and hardening plugin for Jellyfin media servers (10.11+). It adds:

  • Multi-factor authentication: TOTP (Authy/Google Authenticator), passkeys (WebAuthn/FIDO2), email OTP, and recovery codes
  • Single Sign-On: OIDC provider integration (Authentik, Authelia, Keycloak, Pocket ID, etc.)
  • Brute-force protection: IP-based rate limiting and banning with configurable thresholds
  • Device management: TV device pairing via QR code, trusted browser tokens
  • Network controls: LAN bypass, per-user IP allowlist, impossible-travel detection
  • Audit logging: Full login/logout/config-change event tracking
  • Step-up authentication: Re-verify TOTP/passkey/IdP before sensitive admin actions

Server-side enforcement works with all Jellyfin clients (web, Android, iOS, Roku, Fire TV, Kodi) and service integrations (Sonarr, Radarr, Tautulli).

---

Installation

Via Jellyfin Admin Dashboard (Recommended)

1. Open Jellyfin → Admin DashboardPluginsRepositories 2. Add repository:

  • Name: JellyfinSecurity
  • URL: https://raw.githubusercontent.com/ZL154/JellyfinSecurity/main/manifest.json

3. Go to Catalog → search "Security" → Install 4. Restart Jellyfin server

Manual Installation

# Download latest release
RELEASE_URL="https://github.com/ZL154/JellyfinSecurity/releases/latest/download/JellyfinSecurity.zip"
PLUGIN_DIR="/var/lib/jellyfin/plugins/JellyfinSecurity"

mkdir -p "$PLUGIN_DIR"
curl -L "$RELEASE_URL" -o /tmp/jellyfin-security.zip
unzip /tmp/jellyfin-security.zip -d "$PLUGIN_DIR"
chown -R jellyfin:jellyfin "$PLUGIN_DIR"

# Restart Jellyfin
systemctl restart jellyfin

Verify installation:

# Check plugin loaded
journalctl -u jellyfin | grep "JellyfinSecurity"
# Should see: "JellyfinSecurity v2.5.8 loaded"

---

Configuration

Admin Dashboard Settings

Navigate to DashboardPluginsJellyfinSecuritySettings.

Core Authentication
# Enable/disable 2FA methods
TOTPEnabled: true               # TOTP (Authenticator apps)
PasskeysEnabled: true           # WebAuthn/FIDO2 hardware keys
EmailOTPEnabled: false          # Email one-time passwords
RecoveryCodesEnabled: true      # Backup codes (auto-generated with TOTP)

# OIDC/SSO providers
OIDCProviders:
  - Name: "Authentik"
    ClientID: "jellyfin-client"
    ClientSecret: "${OIDC_CLIENT_SECRET}"  # Use environment variable
    Authority: "https://auth.example.com/application/o/jellyfin/"
    Scopes: "openid profile email groups"
    AllowPrivateEndpoints: true  # For LAN-only IdPs
Brute-Force Protection
BruteForceEnabled: true
MaxFailedAttempts: 5            # Attempts before temporary ban
LockoutDuration: 900            # Seconds (15 minutes)
PermanentBanThreshold: 20       # Failed attempts → permanent ban
LAN Bypass
LANBypassEnabled: true
LANBypassCIDRs:                 # RFC1918 + local ranges
  - "192.168.0.0/16"
  - "10.0.0.0/24"               # Be specific to avoid SEC-H3 guard
  - "172.16.0.0/12"
  - "fd00::/8"                  # IPv6 ULA

TrustedProxyCIDRs:              # Your reverse proxy IPs only
  - "10.0.1.5/32"               # Nginx/Traefik container IP

⚠️ Trusted Proxy Pitfall: Do NOT set broad ranges like 10.0.0.0/8 in TrustedProxyCIDRs — the SEC-H3 guard will refuse LAN bypass for direct clients if their IP falls within a trusted-proxy range but no X-Forwarded-For header is present. Use /32 (single IP) or tight /24 subnets for your actual reverse proxy.

Device Pairing (TV Clients)
TVPairingEnabled: true
PairingCodeExpiration: 300      # Seconds (5 minutes)
Step-Up Authentication
StepUpLevel: AllConfigChanges   # Re-verify TOTP/passkey before:
                                # - Plugin settings changes
                                # - User permission changes
                                # - IP allowlist modifications

---

Usage Patterns

End-User Enrollment (TOTP)

After installing the plugin, users enroll via Jellyfin web UI:

1. UserProfileTwo-Factor Authentication 2. Click Enable TOTP 3. Scan QR code with Authy/Google Authenticator 4. Enter 6-digit code to confirm 5. Save 8 recovery codes (required for account recovery)

No user code changes needed — the plugin intercepts /Users/AuthenticateByName at the middleware level.

TV Device Pairing (Roku, Fire TV, etc.)

For clients without keyboard input:

// TV app requests pairing code
POST /JellyfinSecurity/PairDevice
{
  "DeviceName": "Living Room Roku",
  "DeviceId": "roku-device-12345"
}

// Response contains code + QR URL
{
  "pairingCode": "AB12-CD34",
  "qrCodeUrl": "/JellyfinSecurity/PairDeviceQR?code=AB12-CD34",
  "expiresAt": "2026-06-12T12:05:00Z"
}

// User approves via Admin Dashboard → Pending Pairs
// TV polls for approval:
GET /JellyfinSecurity/CheckPairingStatus?code=AB12-CD34

// After approval, use returned token in X-Device-Token header

Trusted Browser Token (Web Client)

After successful 2FA login, the plugin sets a signed cookie:

Set-Cookie: JellyfinSecurity-TrustedDevice=<hmac-signed-token>; 
            HttpOnly; Secure; SameSite=Strict; Max-Age=7776000

Subsequent logins from the same browser skip 2FA for 90 days (configurable). The token is bound to User-Agent + IP subnet (configurable prefix length).

OIDC Sign-In Flow

// Plugin auto-registers endpoints at startup
// User clicks "Sign in with Authentik" button on login page

// 1. Initiate OIDC flow
GET /JellyfinSecurity/OIDC/Authorize?providerId=authentik

// 2. User redirects to IdP, signs in, returns to callback
GET /JellyfinSecurity/OIDC/Callback?code=...&state=...

// 3. Plugin exchanges code for tokens, creates/links Jellyfin user
// 4. Sets Jellyfin auth token cookie, redirects to /web/index.html

Userinfo claim mapping (auto-merged):

  • preferred_username or email → Jellyfin username
  • email → Jellyfin email
  • name → Jellyfin display name
  • groups → Jellyfin user policies (if SyncGroupsEnabled: true)

Per-User IP Allowlist

# Admin Dashboard → JellyfinSecurity → Users → Edit User
AllowedIPs:
  - "203.0.113.0/24"    # Office network
  - "198.51.100.42/32"  # Home static IP

User cannot authenticate from any IP outside this list. Leave empty to disable IP restrictions for that user.

Programmatic API Access (Sonarr, Radarr, etc.)

Option 1: API Key Bypass (recommended for service integrations)

# Admin → JellyfinSecurity → Settings
APIKeyBypassEnabled: true

Services using X-Emby-Token header are exempt from 2FA. Generate API key in Jellyfin DashboardAPI Keys.

curl -H "X-Emby-Token: ${JELLYFIN_API_KEY}" \
     https://jellyfin.example.com/Users/Me

Option 2: Device Token

Pair device once, then include token in every request:

# Pair device (one-time)
curl -X POST https://jellyfin.example.com/JellyfinSecurity/PairDevice \
  -H "Content-Type: application/json" \
  -d '{"DeviceName":"Sonarr","DeviceId":"sonarr-instance-1"}'

# Approve pairing via Admin UI, retrieve token from response

# Use token in subsequent requests
curl -H "X-Device-Token: ${DEVICE_TOKEN}" \
     -H "X-Emby-Token: ${JELLYFIN_API_KEY}" \
     https://jellyfin.example.com/Library/Movies

---

Code Examples

Custom Middleware Integration (C#)

If you're building a separate Jellyfin plugin that needs to hook into JellyfinSecurity's verified-session state:

using JellyfinSecurity.Services;
using Microsoft.AspNetCore.Http;

public class CustomAuthMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ISessionVerifier _sessionVerifier;

    public CustomAuthMiddleware(
        RequestDelegate next,
        ISessionVerifier sessionVerifier)
    {
        _next = next;
        _sessionVerifier = sessionVerifier;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var authToken = context.Request.Headers["X-Emby-Token"].FirstOrDefault();
        
        if (string.IsNullOrEmpty(authToken))
        {
            context.Response.StatusCode = 401;
            return;
        }

        // Check if this token passed 2FA verification
        if (!_sessionVerifier.IsVerified(authToken))
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("2FA verification required");
            return;
        }

        await _next(context);
    }
}

Audit Log Query (C#)

using JellyfinSecurity.Data;
using JellyfinSecurity.Models;

public class AuditLogService
{
    private readonly IAuditLogStore _auditLog;

    public async Task<List<AuditEvent>> GetFailedLoginsAsync(
        DateTime since,
        int limit = 100)
    {
        return await _auditLog.QueryAsync(
            eventType: AuditEventType.LoginFailed,
            startDate: since,
            limit: limit
        );
    }

    public async Task<List<AuditEvent>> GetUserActionsAsync(Guid userId)
    {
        return await _auditLog.QueryByUserAsync(userId);
    }
}

Email OTP Service Configuration (C#)

using JellyfinSecurity.Configuration;

var smtpConfig = new EmailOTPConfiguration
{
    Enabled = true,
    SMTPHost = "smtp.gmail.com",
    SMTPPort = 587,
    UseTLS = true,
    Username = "noreply@example.com",
    Password = Environment.GetEnvironmentVariable("SMTP_PASSWORD"),
    FromAddress = "noreply@example.com",
    FromName = "Jellyfin Security",
    CodeExpiration = 300  // 5 minutes
};

HIBP Password Check (C#)

The plugin includes k-anonymity HIBP integration for password breach detection:

using JellyfinSecurity.Services;

public class PasswordValidator
{
    private readonly IHIBPService _hibp;

    public async Task<bool> IsPasswordCompromisedAsync(string password)
    {
        // Sends only first 5 chars of SHA-1 hash to HIBP
        // (k-anonymity model — password never leaves server in plaintext)
        return await _hibp.IsPasswordPwnedAsync(password);
    }
}

---

Common Workflows

Scenario: User Locked Out (Forgot TOTP Device)

Admin recovery via Dashboard:

1. DashboardJellyfinSecurityUsers 2. Select locked-out user → Reset 2FA 3. User can log in with password only (2FA disabled) 4. User re-enrolls TOTP from profile page

User self-recovery (if recovery codes saved):

1. Login page → Use Recovery Code 2. Enter one of the 8 saved codes (single-use) 3. After login, user can disable TOTP or generate new QR

Scenario: Impossible Travel Alert

# Admin → Settings
ImpossibleTravelEnabled: true
ImpossibleTravelThreshold: 500  # km/hour (flags physically impossible logins)

When detected: 1. Login blocked automatically 2. Admin email notification sent (if AdminEmailAlerts: true) 3. Audit log entry: AuditEventType.ImpossibleTravel 4. Admin must manually unban IP via DashboardBanned IPs

Scenario: SSO-Only Deployment (Hide Built-In Login)

# Admin → Settings
ShowBuiltIn2FAButton: false
ShowBuiltInPasskeyButton: false

OIDCProviders:
  - Name: "Corporate SSO"
    ClientID: "${OIDC_CLIENT_ID}"
    ClientSecret: "${OIDC_CLIENT_SECRET}"
    Authority: "https://sso.corp.example.com"

Login page shows only "Sign in with Corporate SSO" button.

---

Troubleshooting

LAN Bypass Not Working

Symptom: Local clients (192.168.x.x) still prompted for 2FA despite LANBypassEnabled: true.

Diagnosis:

# Check Jellyfin logs for SEC-H3 guard message
journalctl -u jellyfin | grep "SEC-H3"

# Example output:
# "SEC-H3: Client IP 192.168.1.100 is within trusted proxy range 192.168.0.0/16 
#  but no X-Forwarded-For header present — refusing LAN bypass"

Fix: 1. If behind reverse proxy, ensure X-Forwarded-For header is set:

   # Nginx
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   # Traefik (dynamic config)
   http:
     middlewares:
       jellyfin-headers:
         headers:
           customRequestHeaders:
             X-Forwarded-For: ""  # Traefik auto-populates

2. If direct LAN access (no proxy), tighten TrustedProxyCIDRs:

   # BEFORE (too broad):
   TrustedProxyCIDRs:
     - "192.168.0.0/16"  # Includes LAN clients!
   
   # AFTER (proxy IP only):
   TrustedProxyCIDRs:
     - "192.168.1.5/32"  # Nginx container IP

Step-Up Modal Not Appearing

Symptom: Admin clicks Save Settings but nothing happens (v2.5.7 and earlier).

Fix: Upgrade to v2.5.8+. The admin UI now uses step-up-aware fetch wrapper:

// Fixed in v2.5.8 — PluginConfigPage.js
async function saveConfiguration() {
    await stepUpAwareFetch('/JellyfinSecurity/Configuration', {
        method: 'POST',
        body: JSON.stringify(config)
    });
}

OIDC Redirect URI Mismatch

Symptom: IdP returns invalid_redirect_uri error.

Diagnosis:

# Check Jellyfin base URL
curl -s http://localhost:8096/System/Configuration | jq -r '.BaseUrl'

Fix: 1. Ensure BaseUrl matches public-facing URL in Jellyfin DashboardNetworking:

   BaseUrl: "https://jellyfin.example.com"  # Must include https:// if behind SSL proxy

2. Register exact redirect URI in IdP:

   https://jellyfin.example.com/JellyfinSecurity/OIDC/Callback

3. If using Docker + reverse proxy, verify X-Forwarded-Proto header:

   proxy_set_header X-Forwarded-Proto $scheme;

Device Token Expired After Server Restart

Symptom: All TV clients require re-pairing after docker restart jellyfin (v2.5.6 and earlier).

Fix: Upgrade to v2.5.7+. Verified tokens are now persisted to verified-tokens.json:

# Check persistence file exists
ls -lh /config/data/jellyfinsecurity/verified-tokens.json

# Should see entries like:
# {"TokenHash":"sha256:abcd1234...", "VerifiedAt":"2026-06-12T10:00:00Z"}

TOTP Code Rejected (Time Sync Issue)

Symptom: Valid TOTP code from Authy/Google Authenticator shows "Invalid code."

Diagnosis:

# Check server time
timedatectl

# Check time skew tolerance (default ±1 step = 60 seconds)
journalctl -u jellyfin | grep "TOTP time skew"

Fix: 1. Enable NTP on Jellyfin server:

   timedatectl set-ntp true

2. Increase skew window (admin settings):

   TOTPTimeSkew: 2  # Allow ±2 steps (120 seconds)

High CPU Usage from Audit Log

Symptom: jellyfin process consuming high CPU after enabling audit logging.

Fix: 1. Enable log rotation:

   AuditLogEnabled: true
   AuditLogRotation: true
   AuditLogMaxSize: 104857600  # 100 MB
   AuditLogMaxAge: 30          # Days

2. Exclude high-frequency events:

   AuditLogExcludedEvents:
     - "HeartbeatReceived"
     - "SessionActivity"

---

Security Considerations

What This Plugin Defends Against

  • ✅ Credential stuffing (brute-force IP banning)
  • ✅ Phishing (TOTP/passkeys immune to credential reuse)
  • ✅ Unauthorized LAN access (IP allowlist, impossible travel)
  • ✅ Compromised passwords (HIBP integration)
  • ✅ Session hijacking (token binding to User-Agent + IP)
  • ✅ Privilege escalation (step-up auth for admin actions)

What This Plugin Does NOT Defend Against

  • ❌ Server-side vulnerabilities in Jellyfin core (keep Jellyfin updated)
  • ❌ Client-side XSS (use Content-Security-Policy headers in reverse proxy)
  • ❌ TLS/certificate issues (configure reverse proxy with valid certs)
  • ❌ Physical access to server (encrypt /config volume)
  • ❌ Supply-chain attacks on plugin dependencies (verify release SHA-256)

Recommended Deployment Hardening

# Nginx reverse proxy config
server {
    listen 443 ssl http2;
    server_name jellyfin.example.com;

    ssl_certificate /etc/letsencrypt/live/jellyfin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jellyfin.example.com/privkey.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Pass real client IP
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;

    location / {
        proxy_pass http://jellyfin:8096;
    }
}
# Docker Compose with read-only filesystem
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    read_only: true
    tmpfs:
      - /tmp
      - /var/tmp
    volumes:
      - /path/to/config:/config  # Must be writable for plugin data
      - /path/to/media:/media:ro
    environment:
      - JELLYFIN_PublishedServerUrl=https://jellyfin.example.com

---

Testing

The plugin includes 254 xUnit tests covering security-critical paths:

# Run full test suite
git clone https://github.com/ZL154/JellyfinSecurity.git
cd JellyfinSecurity
dotnet test --logger "console;verbosity=detailed"

# Run specific test categories
dotnet test --filter "Category=Crypto"       # TOTP, passkey, cookie HMAC
dotnet test --filter "Category=Middleware"   # Step-up, LAN bypass, brute-force
dotnet test --filter "Category=OIDC"         # SSO flows, token exchange

Key test coverage:

  • TOTP replay protection (time-step validation)
  • Recovery code PBKDF2 hashing (100k iterations)
  • Trusted browser token HMAC verification
  • CIDR parser edge cases (IPv6, /0, /128)
  • X-Forwarded-For trust-walk (multi-proxy chains)
  • AES-GCM v2 authenticated encryption
  • HIBP k-anonymity hashing (SHA-1 prefix)
  • Step-up challenge consumption (single-use tokens)

---

API Reference

Public Endpoints

EndpointMethodDescription
/JellyfinSecurity/OIDC/AuthorizeGETInitiate OIDC flow
/JellyfinSecurity/OIDC/CallbackGETOIDC redirect callback
/JellyfinSecurity/PairDevicePOSTRequest TV pairing code
/JellyfinSecurity/CheckPairingStatusGETPoll for pairing approval
/JellyfinSecurity/VerifyTOTPPOSTSubmit TOTP code
/JellyfinSecurity/VerifyPasskeyPOSTComplete WebAuthn ceremony
/JellyfinSecurity/SendEmailOTPPOSTRequest email OTP

Admin-Only Endpoints (Require Step-Up if Enabled)

EndpointMethodDescription
/JellyfinSecurity/ConfigurationGET/POSTPlugin settings
/JellyfinSecurity/UsersGETList users with 2FA status
/JellyfinSecurity/Users/{id}GET/PUTUser-specific config
/JellyfinSecurity/Users/{id}/ResetTOTPPOSTDisable user's 2FA
/JellyfinSecurity/PendingPairsGETList awaiting approval
/JellyfinSecurity/ApprovePairPOSTApprove TV pairing
/JellyfinSecurity/DenyPairPOSTReject TV pairing
/JellyfinSecurity/AuditLogGETQuery security events

---

Further Resources

  • GitHub Repository: https://github.com/ZL154/JellyfinSecurity
  • Security Policy: SECURITY.md
  • OpenSSF Scorecard: https://securityscorecards.dev/viewer/?uri=github.com/ZL154/JellyfinSecurity
  • Jellyfin Plugin Portal: https://jellyfin.org/plugins/
  • TOTP RFC 6238: https://datatracker.ietf.org/doc/html/rfc6238
  • WebAuthn Spec: https://www.w3.org/TR/webauthn-2/

For security issues, email the maintainer directly (see SECURITY.md) or file a private advisory via GitHub Security.

Related skills

FAQ

Which Jellyfin version is required?

Jellyfin 10.11 or newer.

Does it work with all clients?

Yes; server-side enforcement works with all Jellyfin clients including web, Android, iOS, Roku, Fire TV, and Kodi.

Securityappsecaudit

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.