Problema
Muchos entusiastas de home‑lab quieren que su servidor de medios (Jellyfin, Plex, Emby, etc.) sea accesible fuera de la red doméstica sin montar una VPN para cada cliente. La solución típica consiste en exponer un puerto HTTP/HTTPS del router y dejar que el propio Jellyfin gestione TLS. Ese enfoque suele generar dos riesgos principales:
- Superficie de ataque ampliada – el puerto de la aplicación queda visible a escáneres automáticos y a ataques de fuerza bruta.
- Lateral movement – si el proceso del servidor se compromete, el atacante puede escalar dentro de la LAN y tocar otros servicios.
El reto es diseñar una arquitectura mínima que reduzca esos riesgos, mantenga la experiencia de usuario (acceso mediante un único nombre DNS y certificado válido) y sea fácil de mantener con herramientas habituales (Caddy, LXC, Cloudflare DNS, etc.).
Causa
Los vectores de compromiso más frecuentes en este tipo de despliegues son:
- Exposición directa del puerto de la aplicación. Jellyfin no está pensado como exposed internet service; su configuración por defecto permite autenticación básica, pero no protege contra enumeración de usuarios o ataques de credential stuffing.
- Contenedores con privilegios excesivos. Un LXC sin restricciones de montaje o sin
read‑onlyen los volúmenes de medios permite que un atacante modifique archivos, crear scripts de arranque o incluso montar dispositivos de red. - Falta de filtrado a nivel de proxy. Caddy, Nginx o Traefik pueden aplicar límites de velocidad, encabezados de seguridad y bloqueos de IP, pero si se confía únicamente en la autenticación de la aplicación, se pierde una capa de defensa en profundidad.
- DNS dinámico sin aislamiento. Un token de API de Cloudflare almacenado en el mismo contenedor que sirve contenido web es un punto único de fallo; si el contenedor se compromete, el atacante puede cambiar registros DNS y redirigir tráfico.
- Redes planas. Colocar el servidor de medios en la misma VLAN que los dispositivos de gestión (Proxmox, SSH, SMB) facilita la escalada lateral una vez que el contenedor es vulnerado.
Solución
Una arquitectura segura y sencilla se basa en tres pilares:
-
Reverse proxy dedicado (Caddy) que sea el único punto de entrada desde Internet.
- Sólo los puertos 80/443 del router se redirigen a Caddy.
- Caddy gestiona TLS con Let’s Encrypt o Cloudflare Origin CA, evitando que Jellyfin maneje certificados.
- Se añaden encabezados de seguridad (
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options) y una política de Rate Limiting para evitar fuerza bruta.
-
Contenedores LXC aislados.
- Jellyfin corre en un LXC sin privilegios, con su raíz de archivos montado como
read‑onlypara la biblioteca de medios. - Sólo se concede permiso de lectura a la carpeta de videos; los directorios de configuración se montan
read‑writeen una zona separada. - Se habilita
apparmoroseccomppara limitar syscalls peligrosos.
- Jellyfin corre en un LXC sin privilegios, con su raíz de archivos montado como
-
Segmentación de red.
- Crear una VLAN o una subred dedicada para los servicios expuestos (Caddy + Jellyfin).
- El resto de la LAN (NAS, Proxmox, dispositivos IoT) permanece en una VLAN distinta con reglas de firewall que solo permiten tráfico established/related desde la VLAN de medios.
- Opcional: usar split‑DNS para que
jf.example.comresuelva a la IP interna cuando el cliente está en casa, evitando el “hairpin” a través del ISP.
Hardening del proxy
# Caddyfile (ubicado en /etc/caddy/Caddyfile)
jf.example.com {
reverse_proxy 10.0.20.5:8096 {
transport http {
read_buffer 64KB
}
}
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "no-referrer"
}
@rate_limit {
remote_ip not 192.168.0.0/16
}
rate_limit @rate_limit 10r/m {
burst 20
}
}
DDNS seguro
# /etc/systemd/system/cloudflare-ddns.service
[Unit]
Description=Actualizar registro A en Cloudflare
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/cloudflare-ddns.env
ExecStart=/usr/local/bin/cf-ddns update jf.example.com $PUBLIC_IP
# /etc/systemd/system/cloudflare-ddns.timer
[Unit]
Description=Timer para Cloudflare DDNS cada 5 minutos
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target
El archivo cloudflare-ddns.env contiene únicamente CLOUDFLARE_API_TOKEN=xxxx con permisos 600 y pertenece al usuario caddy. De esa forma el token nunca sale del contenedor y el proceso se ejecuta con los mínimos privilegios.
Configuración de Jellyfin
- Desactivar public registration y habilitar 2‑factor authentication (si el cliente lo soporta).
- Limitar la creación de usuarios a la cuenta de administrador; los usuarios familiares deben ser restricted y solo pueden reproducir, no crear listas ni modificar la configuración.
- Desactivar metadata download externo si no es necesario; reduce la superficie de ataque a servicios de terceros.
- Configurar IP whitelist dentro de Jellyfin para permitir solo la IP interna del contenedor de Caddy (aunque el proxy ya filtra, es una defensa en profundidad).
Cuándo aplicar esta solución
Aplicable cuando:
- Necesitas acceso remoto a un servidor de medios sin VPN para usuarios domésticos.
- El servidor corre en Linux y puedes usar contenedores LXC o Docker.
- Quieres mantener la complejidad baja (un solo proxy, un solo contenedor de aplicación).
No aplicable si:
- El entorno es una empresa con requisitos de auditoría y segmentación más estricta (se requerirían firewalls de capa 7, IDS/IPS, etc.).
- El servidor de medios necesita exponer puertos UDP para funciones específicas (ej. descubrimiento DLNA) que el proxy HTTP no puede manejar. En ese caso se necesita una solución de túnel o VPN.
Código
# Crear LXC sin privilegios y montar la biblioteca como read‑only
pct create 101 local:vztmpl/debian-12-standard_12.0-1_amd64.tar.gz \
-hostname jellyfin -net0 name=eth0,bridge=vmbr0,ip=10.0.20.5/24 \
-features nesting=0,privileged=0 \
-mp0 /mnt/media,mp=/media,ro=1
# Instalar Caddy en otro LXC
pct create 102 local:vztmpl/debian-12-standard_12.0-1_amd64.tar.gz \
-hostname caddy -net0 name=eth0,bridge=vmbr0,ip=10.0.20.6/24 \
-features nesting=0,privileged=0
# Habilitar firewall entre VLANs (ejemplo con iptables)
iptables -A FORWARD -i vlan10 -o vlan20 -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i vlan20 -o vlan10 -j DROP
Verificación
- TLS – Usa
curl -v https://jf.example.comy verifica que el certificado sea emitido por Let’s Encrypt o Cloudflare Origin CA. - Encabezados de seguridad –
curl -I https://jf.example.comy buscaStrict-Transport-Security,X-Content-Type-Options, etc. - Rate limiting – Simula 20 peticiones en menos de un minuto desde una IP externa; la 21ª debe recibir
429 Too Many Requests. - Aislamiento LXC – Dentro del contenedor de Jellyfin ejecuta
touch /media/test.txt; debería fallar por permiso de solo lectura. - Firewall VLAN – Desde una máquina en la VLAN de gestión intenta
nc -zv 10.0.20.5 8096; la conexión debe ser rechazada.
Notas adicionales
- Actualizaciones automáticas: habilita
unattended-upgradesen ambos contenedores para parchear vulnerabilidades de sistema operativo sin intervención manual. - Monitorización ligera:
caddy metricsexpone métricas Prometheus; combinar connode_exporterpermite detectar picos de tráfico sospechosos. - Backup de configuración: guarda el
Caddyfiley los archivos de configuración de Jellyfin en un repositorio Git privado; facilita la recuperación tras una posible intrusión. - Revisión de permisos Cloudflare: el token debe estar limitado a
Zone.Zone,Zone.DNSy al único registro A que actualiza. Evita permisos de edición de zona completa. - Pruebas de penetración: ejecuta
nmap -sV -p 80,443 <IP pública>y verifica que solo el puerto 443 responde con el banner de Caddy.
Con esta arquitectura, la exposición de Jellyfin a Internet se reduce a un único punto controlado, se limita la capacidad de movimiento lateral y se mantiene una experiencia de usuario fluida para la familia. La clave está en combinar un reverse proxy robusto, contenedores con privilegios mínimos y una segmentación de red que separe los servicios críticos de los de gestión.