| name | jellyfin-security-plugin |
| description | Add native two-factor authentication, passkeys, SSO, brute-force protection, and audit logging to Jellyfin media servers via server-side plugin. |
| triggers | ["add two-factor authentication to jellyfin","set up jellyfin totp or passkeys","configure jellyfin sso with oidc","protect jellyfin with brute force ip banning","implement jellyfin device pairing for tv clients","enable jellyfin lan bypass for local networks","audit jellyfin login attempts and security events","integrate jellyfin with authentik or authelia"] |
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)
- Open Jellyfin → Admin Dashboard → Plugins → Repositories
- Add repository:
- Name:
JellyfinSecurity
- URL:
https://raw.githubusercontent.com/ZL154/JellyfinSecurity/main/manifest.json
- Go to Catalog → search "Security" → Install
- Restart Jellyfin server
Manual Installation
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"
systemctl restart jellyfin
Verify installation:
journalctl -u jellyfin | grep "JellyfinSecurity"
Configuration
Admin Dashboard Settings
Navigate to Dashboard → Plugins → JellyfinSecurity → Settings.
Core Authentication
TOTPEnabled: true
PasskeysEnabled: true
EmailOTPEnabled: false
RecoveryCodesEnabled: true
OIDCProviders:
- Name: "Authentik"
ClientID: "jellyfin-client"
ClientSecret: "${OIDC_CLIENT_SECRET}"
Authority: "https://auth.example.com/application/o/jellyfin/"
Scopes: "openid profile email groups"
AllowPrivateEndpoints: true
Brute-Force Protection
BruteForceEnabled: true
MaxFailedAttempts: 5
LockoutDuration: 900
PermanentBanThreshold: 20
LAN Bypass
LANBypassEnabled: true
LANBypassCIDRs:
- "192.168.0.0/16"
- "10.0.0.0/24"
- "172.16.0.0/12"
- "fd00::/8"
TrustedProxyCIDRs:
- "10.0.1.5/32"
⚠️ 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
Step-Up Authentication
StepUpLevel: AllConfigChanges
Usage Patterns
End-User Enrollment (TOTP)
After installing the plugin, users enroll via Jellyfin web UI:
- User → Profile → Two-Factor Authentication
- Click Enable TOTP
- Scan QR code with Authy/Google Authenticator
- Enter 6-digit code to confirm
- 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:
POST /JellyfinSecurity/PairDevice
{
"DeviceName": "Living Room Roku",
"DeviceId": "roku-device-12345"
}
{
"pairingCode": "AB12-CD34",
"qrCodeUrl": "/JellyfinSecurity/PairDeviceQR?code=AB12-CD34",
"expiresAt": "2026-06-12T12:05:00Z"
}
GET /JellyfinSecurity/CheckPairingStatus?code=AB12-CD34
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
GET /JellyfinSecurity/OIDC/Authorize?providerId=authentik
GET /JellyfinSecurity/OIDC/Callback?code=...&state=...
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
AllowedIPs:
- "203.0.113.0/24"
- "198.51.100.42/32"
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)
APIKeyBypassEnabled: true
Services using X-Emby-Token header are exempt from 2FA. Generate API key in Jellyfin Dashboard → API 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:
curl -X POST https://jellyfin.example.com/JellyfinSecurity/PairDevice \
-H "Content-Type: application/json" \
-d '{"DeviceName":"Sonarr","DeviceId":"sonarr-instance-1"}'
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;
}
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
};
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)
{
return await _hibp.IsPasswordPwnedAsync(password);
}
}
Common Workflows
Scenario: User Locked Out (Forgot TOTP Device)
Admin recovery via Dashboard:
- Dashboard → JellyfinSecurity → Users
- Select locked-out user → Reset 2FA
- User can log in with password only (2FA disabled)
- User re-enrolls TOTP from profile page
User self-recovery (if recovery codes saved):
- Login page → Use Recovery Code
- Enter one of the 8 saved codes (single-use)
- After login, user can disable TOTP or generate new QR
Scenario: Impossible Travel Alert
ImpossibleTravelEnabled: true
ImpossibleTravelThreshold: 500
When detected:
- Login blocked automatically
- Admin email notification sent (if
AdminEmailAlerts: true)
- Audit log entry:
AuditEventType.ImpossibleTravel
- Admin must manually unban IP via Dashboard → Banned IPs
Scenario: SSO-Only Deployment (Hide Built-In Login)
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:
journalctl -u jellyfin | grep "SEC-H3"
Fix:
-
If behind reverse proxy, ensure X-Forwarded-For header is set:
# Nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
http:
middlewares:
jellyfin-headers:
headers:
customRequestHeaders:
X-Forwarded-For: ""
-
If direct LAN access (no proxy), tighten TrustedProxyCIDRs:
TrustedProxyCIDRs:
- "192.168.0.0/16"
TrustedProxyCIDRs:
- "192.168.1.5/32"
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:
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:
curl -s http://localhost:8096/System/Configuration | jq -r '.BaseUrl'
Fix:
-
Ensure BaseUrl matches public-facing URL in Jellyfin Dashboard → Networking:
BaseUrl: "https://jellyfin.example.com"
-
Register exact redirect URI in IdP:
https://jellyfin.example.com/JellyfinSecurity/OIDC/Callback
-
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:
ls -lh /config/data/jellyfinsecurity/verified-tokens.json
TOTP Code Rejected (Time Sync Issue)
Symptom: Valid TOTP code from Authy/Google Authenticator shows "Invalid code."
Diagnosis:
timedatectl
journalctl -u jellyfin | grep "TOTP time skew"
Fix:
-
Enable NTP on Jellyfin server:
timedatectl set-ntp true
-
Increase skew window (admin settings):
TOTPTimeSkew: 2
High CPU Usage from Audit Log
Symptom: jellyfin process consuming high CPU after enabling audit logging.
Fix:
-
Enable log rotation:
AuditLogEnabled: true
AuditLogRotation: true
AuditLogMaxSize: 104857600
AuditLogMaxAge: 30
-
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;
}
}
services:
jellyfin:
image: jellyfin/jellyfin:latest
read_only: true
tmpfs:
- /tmp
- /var/tmp
volumes:
- /path/to/config:/config
- /path/to/media:/media:ro
environment:
- JELLYFIN_PublishedServerUrl=https://jellyfin.example.com
Testing
The plugin includes 254 xUnit tests covering security-critical paths:
git clone https://github.com/ZL154/JellyfinSecurity.git
cd JellyfinSecurity
dotnet test --logger "console;verbosity=detailed"
dotnet test --filter "Category=Crypto"
dotnet test --filter "Category=Middleware"
dotnet test --filter "Category=OIDC"
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
| Endpoint | Method | Description |
|---|
/JellyfinSecurity/OIDC/Authorize | GET | Initiate OIDC flow |
/JellyfinSecurity/OIDC/Callback | GET | OIDC redirect callback |
/JellyfinSecurity/PairDevice | POST | Request TV pairing code |
/JellyfinSecurity/CheckPairingStatus | GET | Poll for pairing approval |
/JellyfinSecurity/VerifyTOTP | POST | Submit TOTP code |
/JellyfinSecurity/VerifyPasskey | POST | Complete WebAuthn ceremony |
/JellyfinSecurity/SendEmailOTP | POST | Request email OTP |
Admin-Only Endpoints (Require Step-Up if Enabled)
| Endpoint | Method | Description |
|---|
/JellyfinSecurity/Configuration | GET/POST | Plugin settings |
/JellyfinSecurity/Users | GET | List users with 2FA status |
/JellyfinSecurity/Users/{id} | GET/PUT | User-specific config |
/JellyfinSecurity/Users/{id}/ResetTOTP | POST | Disable user's 2FA |
/JellyfinSecurity/PendingPairs | GET | List awaiting approval |
/JellyfinSecurity/ApprovePair | POST | Approve TV pairing |
/JellyfinSecurity/DenyPair | POST | Reject TV pairing |
/JellyfinSecurity/AuditLog | GET | Query security events |
Further Resources
For security issues, email the maintainer directly (see SECURITY.md) or file a private advisory via GitHub Security.