Skip to content

Account Protection

ZL154 edited this page Jun 15, 2026 · 2 revisions

Account Protection

Layered defenses against credential attacks: account lockout, per-IP brute-force banning, impossible-travel detection, per-user IP allowlists, empty-password blocking, and hardened self-service factor changes.


Account lockout

After N failed attempts, an account is temporarily locked.

  • Counts both wrong passwords and failed 2FA codes (since v2.5.10 — #55).
  • The lock is enforced before the password is checked, and the login page shows a clear "temporarily locked" message.
  • Administrators are exempt by default (toggle in Settings) so lockout can't be turned into a denial-of-service against the admin account — the per-IP brute-force ban still protects them.

Configure: Settings → Security → "Max failed attempts before lockout" + lockout duration (default 5 attempts → 15-minute cool-down).


Empty-password blocking

When enabled, the plugin refuses empty/whitespace passwords for every sign-in (not just OIDC/passkey users).

  • Toggle: Settings → Security → Block sign-in with an empty password (admin-UI toggle since v2.5.10; previously config-file-only).
  • Protects pre-existing password-less accounts, and closes the auto-created-OIDC-user empty-password path (#68).

Auto-created OIDC users are hardened with a 256-bit random password at creation, so Jellyfin's default provider stops accepting "any password" for them.


Disable password sign-in (#69, v2.5.11)

Refuse username+password sign-in entirely so users go through your identity provider (or Quick Connect) — for OIDC-only deployments. OIDC sign-in and Quick Connect are never blocked, and the plugin's own /TwoFactorAuth/Login page always works.

Configure: Settings → Security → Disable password sign-in. On the login page the username/password fields are hidden, leaving a discreet "Sign in with a password instead" link.

Three independently-toggleable escape hatches stop a dead IdP from locking everyone out:

  • Administrators can always use a password (default on).
  • Allow password sign-in from the LAN (default on) — effectively "disable for remote users only".
  • Exempt IP ranges (CIDR) — explicit addresses that may always use a password (e.g. your home IP).

⚠ Keep at least one escape hatch enabled. If you disable them all and your IdP becomes unreachable, use the always-available /TwoFactorAuth/Login page to get back in.


Brute-force IP banning

Auto-bans source IPs that hammer the login endpoint — Fail2Ban-style, entirely in-process, no external service.

Configure: Settings → "Brute-Force Protection":

  • Failure threshold (default 10)
  • Window (default 10 min)
  • Ban duration (default 24 h)
  • Exempt CIDRs (never banned — e.g. your office IP)

Always exempt: LAN-bypass CIDRs, trusted-proxy CIDRs, anything in the exempt list.

Manage bans: IP Bans tab lists active bans with expiry. Click Unban to clear, or manually ban an IP. Bans persist across restarts via <config>/plugins/configurations/TwoFactorAuth/ip-bans.json.

Behind a reverse proxy you must set Trust X-Forwarded-For + Trusted Proxy CIDRs, or every request looks like it comes from the proxy and all clients share one ban bucket. See First-Time Setup → Reverse proxy.


Impossible-travel detection

Flags sign-ins where geographic distance vs. elapsed time exceeds commercial-jet cruise speed (~900 km/h default). London → Tokyo in 30 minutes ≈ Mach 20 → notification fires.

Requires: MaxMind GeoLite2-City.mmdb. Free signup → download the City DB → drop it in /config/geoip/ → paste the path in Settings → Impossible-Travel Detection.

Fires through the same notification channels (ntfy, Gotify, webhook, admin emails) with distance, duration, inferred speed, and country hop. Off by default.


Per-user IP allowlist

Pin a user account to specific CIDRs. Empty = no restriction (default). Useful for admin accounts where lateral exposure hurts most.

  • User self-service: Setup page → IP Allowlist card → one CIDR per line → Save.
  • Admin, per user: PUT /TwoFactorAuth/IpAllowlist/User/{userId} (full UI not wired yet; edit the user JSON or use the API).

Self-lockout risk: a typo'd CIDR can lock you out. Recover by editing /config/plugins/configurations/TwoFactorAuth/users/<your-guid>.json and clearing IpAllowlistCidrs.


Step-up authentication (admin actions)

Re-prompts the admin for a fresh 2FA challenge before sensitive operations — defends against a hijacked or unattended session.

Configure: Settings → Hardening → Step-up level:

Level What re-prompts
Off Nothing (default — opt in deliberately).
Destructive Deleting users, wiping 2FA state, rebuilding the audit chain, removing OIDC providers.
AllConfigChanges All of Destructive, plus toggling settings and editing SMTP / push / brute-force / impossible-travel config.
Everything All of AllConfigChanges, plus viewing the audit log, listing IP bans, exporting config.

Step-up tokens are single-use; you re-prompt next time. RequireTwoFactorToDisable additionally re-prompts before a user can disable their own 2FA.


Hardened self-service factor changes

Closes the stolen-session takeover path: before this, a hijacked session could silently enroll the attacker's own authenticator. Now a proof-of-factor is required before adding/replacing TOTP, recovery codes, an app password, or a passkey, or toggling email OTP.

Setting: Settings → Hardening → Hardened security for users (factor changes) — tri-state:

  • Off — change factors without a current code (legacy).
  • User choice — per-user opt-in toggle on the Setup page.
  • Forced (default) — everyone must submit a current factor before changing any.

Proof of factor accepts any of: a current TOTP code, an unused recovery code, a passkey assertion, an emailed 8-digit step-up code, or an OIDC re-auth. Step-up tokens are single-use, 60-second TTL, bound to the requesting user.


Indefinite device trust (opt-in)

Lets a user mark a specific trusted browser/paired device as "trusted forever" instead of 30 days.

  • Admin gate (default off): Settings → Hardening → AllowIndefiniteTrust. When off, the user-side toggle is hidden entirely.
  • User opt-in: Setup → Trusted/Paired Devices card → toggle Indefinite trust per device.

⚠ An indefinite-trust device is your weakest link — if it's stolen, that browser stays signed in until you revoke it. Don't enable on shared/borrowed machines.

Clone this wiki locally