Case Study: Media Streaming Defense (Jellyfin & Plex)
This case study demonstrates how to safely expose self-hosted media servers (Jellyfin, Plex, or Emby) to friends, family, and mobile clients over the public internet while shielding critical administrative dashboards, API keys, and user management interfaces from internet scanners.
The Threat Model
Self-hosted media servers offer native streaming apps across iOS, Android, Apple TV, Roku, and modern smart TVs. To enable seamless playback outside your home, the media server port must be accessible over the internet (e.g. media.example.com).
However, exposing media servers directly creates substantial risk:
- Administrative Console Exposure: Jellyfin exposes
/admin,/System/Configuration,/Plugins, and API key generation portals (/web/index.html#!/apikeys.html). - Unauthorized User Creation: Probing bots attempt to register new accounts via
/Users/New. - API Key Extraction & Enumeration: Vulnerability scanners target unauthenticated metadata and server status endpoints.
- Credential Brute-Forcing: Attackers launch dictionary attacks against
/Users/AuthenticateByName.
The Objective
- Public Streaming Traffic: Friends, family, and TV apps can stream audio and video, browse libraries, and load album art (
/Videos/*,/Items/*,/Images/*,/Audio/*,/Sessions/Playing). - Administrative Lockdown: Administrative, user creation, and plugin management APIs are strictly blocked and cloaked with
404 Not Foundfor all public clients. - Internal / VPN Bypass: You maintain full unrestricted administration when connected through your internal LAN, Tailscale (
100.64.0.0/10), or WireGuard VPN.
1. Jellyfin Configuration (Port 8096)
Select your gateway below to inspect the Jellyfin production configuration. Diff highlights show RouteWarden rules:
# dynamic_conf.ymlhttp: middlewares: media-shield: plugin: routewarden: enabled: true methods: - GET - POST - PUT - DELETE - PATCH - HEAD enableDefaultPatterns: true # Shield administrative settings, user provisioning, and plugin managers blockPatterns: - '(?i)^/(admin|System|Plugins|Users/New)(/.*)?$' - '(?i)^/web/index\.html#!/dashboard.*$' - '(?i)^/web/index\.html#!/apikeys.*$' # Trusted Home LAN and Tailscale / WireGuard VPN subnets allowedIps: - "10.0.0.0/8" # Internal Home LAN - "100.64.0.0/10" # Tailscale CGNAT range - "192.168.1.0/24" # Home Office Subnet - "127.0.0.1" # Localhost response: mode: json statusCode: 404 body: '{"error":"Not Found","message":"The requested resource was not found"}' routers: media-router: rule: "Host(`media.example.com`)" entryPoints: - websecure middlewares: - media-shield service: media-service2. Plex Configuration (Port 32400)
Select your gateway below to inspect the Plex Media Server production configuration. Diff highlights show RouteWarden rules:
# dynamic_conf.yml - Plex Media Serverhttp: middlewares: plex-shield: plugin: routewarden: enabled: true methods: - GET - POST - PUT - DELETE - PATCH - HEAD enableDefaultPatterns: true blockPatterns: - '(?i)^/(system|updater|activities|diagnostics|manage)(/.*)?$' - '(?i)^/web/index\.html#!/settings.*$' - '(?i)^/servers(/.*)?$' allowedIps: - "10.0.0.0/8" - "100.64.0.0/10" - "192.168.1.0/24" - "127.0.0.1" response: mode: json statusCode: 404 body: '{"error":"Not Found","message":"The requested resource was not found"}' routers: plex-router: rule: "Host(`plex.example.com`)" entryPoints: - websecure middlewares: - plex-shield service: plex-serviceCrucial Implementation Notes
1. HTTP Methods: Why methods is Mandatory
By default, RouteWarden inspects only GET requests (methods: ["GET"]).
However, administrative operations in Jellyfin and Plex—such as creating users (POST /Users/New), installing plugins (POST /Plugins/*), and updating system configurations—are submitted via POST and PUT. Without declaring methods, external bots could issue POST requests directly to backend administration handlers. Always include methods GET POST PUT DELETE PATCH HEAD.
2. Response Mode Recommendation
Always use mode: json (or mode: html) with statusCode: 404 for public media ingress.
WARNING
If your reverse proxy is placed behind an edge proxy or CDN (such as Cloudflare, Traefik edge, AWS ALB, or an NGINX reverse proxy), avoid using mode: silentDrop.
Abruptly terminating the TCP connection under silentDrop causes edge proxies to return an HTTP 502 Bad Gateway error page to clients. Using mode: json with statusCode: 404 cleanly cloaks administrative routes as non-existent.
3. Caddy Directive Ordering
When using Caddy, you must register RouteWarden before Caddy's built-in reverse_proxy directive in the global options block:
# Caddyfile Global Options{ order routewarden before reverse_proxy}4. Architectural Note: Selective Shielding vs. Zero-Trust Allowlisting
In architectures like Immich (where external visitors only need /share/*) or Home Assistant (where only /api/webhook/* is exposed), public access is strictly bounded to a few discrete paths. In those scenarios, Zero-Trust Allowlisting (blockPatterns: ['(?i)^/.*$']) is recommended to completely eliminate root / redirect bypasses.
However, self-hosted media servers (Jellyfin and Plex) are designed for full interactive multi-device streaming:
- Media players across Smart TVs (Apple TV, Roku, Android TV), gaming consoles, and mobile apps communicate across dozens of dynamic APIs (
/Items,/Videos,/Audio,/Images,/Sessions,/DisplayPreferences,/Branding,/web/*). - Enforcing a default-deny catch-all (
blockPatterns: ['(?i)^/.*$']) would break device playback and subtitle streaming unless hundreds of application routes were continuously maintained.
For media streaming servers, Selective Administrative Shielding (blockPatterns targeting /admin, /System, /Plugins, /Users/New) paired with VPN bypass (allowedIps) provides the ideal balance: public users enjoy seamless media playback while privileged controls remain strictly invisible to the internet.
Verification Matrix & Curl Tests
Inspect live curl verification commands and offline RouteWarden CLI tests:
# 1. Public Media Playback / Cover Art (Allowed Traffic)curl -i https://media.example.com/Items/abc12345/Images/Primary# Response: HTTP/2 200 OK (Content-Type: image/jpeg)# 2. Public Administrative Probe (Blocked & Cloaked)curl -i https://media.example.com/System/Configuration# Response: HTTP/2 404 Not Found# {"error":"Not Found","message":"The requested resource was not found"}# 3. Public User Registration Probe (Blocked)curl -i -X POST https://media.example.com/Users/New \ -H "Content-Type: application/json" \ -d '{"Name":"hacker","Password":"password123"}'# Response: HTTP/2 404 Not Found# 4. VPN / Internal LAN Administration (Allowed Bypass)curl -i https://media.example.com/System/Configuration# When connecting from 100.64.0.0/10 or 192.168.1.0/24:# Response: HTTP/2 200 OK| Route & Intent | Path & Method | Public Internet | Home LAN / VPN (allowedIps) |
|---|---|---|---|
| Media Playback | GET /Videos/{id}/stream | ✅ Allowed | ✅ Allowed |
| Cover Art & Thumbnails | GET /Items/{id}/Images/* | ✅ Allowed | ✅ Allowed |
| Playback Session Sync | POST /Sessions/Playing | ✅ Allowed | ✅ Allowed |
| System Configuration | GET /System/Configuration | ❌ Blocked (404) | ✅ Allowed |
| User Creation | POST /Users/New | ❌ Blocked (404) | ✅ Allowed |
| Plugin Management | POST /Plugins/* | ❌ Blocked (404) | ✅ Allowed |