El plan The plan

Con el servidor ya funcionando (Jellyfin, qBittorrent, Tailscale...) me quedaba la parte que más pereza me daba: el cortafuegos. Da un poco de miedo tocarlo, la verdad, porque como te equivoques te puedes quedar fuera de tu propio servidor. Así que antes de nada me planteé unas reglas claras para no liarla:

  1. Política de entrada bloqueada por defecto (DROP): nadie entra si no hay una regla explícita que lo permita.
  2. Permitir tráfico de salida: que el servidor pueda actualizarse o descargar portadas en Jellyfin.
  3. Mantener conexiones establecidas (Stateful): si el servidor pide algo hacia afuera, que la respuesta pueda entrar sola.
  4. Permitir tráfico local interno (lo / localhost): para que las aplicaciones se hablen entre sí.
  5. Permitir la interfaz de Tailscale (tailscale0): para tener acceso total a todos mis servicios (Jellyfin, qBittorrent, SSH) desde mis propios dispositivos, sin importar el puerto.
  6. Permitir tráfico local de casa (opcional, pero lo dejé puesto): para que los dispositivos del WiFi de casa sigan viendo Jellyfin directamente sin pasar por Tailscale.

With the server already running (Jellyfin, qBittorrent, Tailscale...) the part I was dreading the most was left: the firewall. It's a bit scary to touch, honestly, because if you mess it up you can lock yourself out of your own server. So before doing anything I laid out some clear rules for myself so I wouldn't screw it up:

  1. Default-deny inbound policy (DROP): nobody gets in unless there's an explicit rule allowing it.
  2. Allow outbound traffic: so the server can update itself or fetch cover art in Jellyfin.
  3. Keep established connections (Stateful): if the server requests something outbound, let the response back in on its own.
  4. Allow internal loopback traffic (lo / localhost): so applications can talk to each other.
  5. Allow the Tailscale interface (tailscale0): to have full access to all my services (Jellyfin, qBittorrent, SSH) from my own devices, regardless of the port.
  6. Allow local home traffic (optional, but I left it on): so devices on the home WiFi keep seeing Jellyfin directly without going through Tailscale.

Cómo lo configuré, paso a paso How I set it up, step by step

Me conecté por SSH a mi Debian y fui haciendo esto:

I connected to my Debian machine over SSH and did this:

Paso 1: instalar y habilitar nftables

Lo primero, asegurarme de que la herramienta estaba instalada y arrancaba con el sistema:

sudo apt update && sudo apt install nftables -y
sudo systemctl enable nftables

First thing, making sure the tool was installed and would start with the system:

Paso 2: crear el archivo de reglas

Aquí sobrescribí la configuración por defecto de nftables con un archivo limpio siguiendo mi propia estrategia. Abrí el archivo de configuración con el editor:

sudo nano /etc/nftables.conf

Borré todo lo que había dentro y pegué este bloque:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # 1. Permitir tráfico de loopback (comunicación interna del servidor)
        iif "lo" accept

        # 2. Permitir conexiones ya establecidas o relacionadas (Stateful Firewall)
        ct state established,related accept

        # 3. Rechazar paquetes inválidos por seguridad
        ct state invalid drop

        # 4. Permitir todo el tráfico a través de la interfaz de Tailscale
        iifname "tailscale0" accept

        # 5. Permitir tráfico de tu red local de casa (IPs 192.168.1.X)
        # Ajusta "192.168.1.0/24" si tu red local usa otro rango (ej. 192.168.0.0/24)
        ip saddr 192.168.1.0/24 accept

        # 6. Permitir PING (ICMP) para diagnóstico local
        ip protocol icmp accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

Nota para no perder tiempo buscándolo luego: para guardar en nano es Ctrl + O, Enter, y para salir Ctrl + X.

Here I overwrote nftables' default config with a clean file following my own strategy. I opened the config file with the editor, deleted everything inside, and pasted the block above. Note so I don't waste time looking it up again later: to save in nano it's Ctrl + O, Enter, and to exit Ctrl + X.

Paso 3: probar antes de aplicar

Antes de dejarlo activo de forma permanente, comprobé que la sintaxis fuera correcta:

sudo nft -c -f /etc/nftables.conf

Si no devuelve ningún error, va bien. Entonces reinicié el servicio para que el cortafuegos se aplicase ya mismo:

sudo systemctl restart nftables

Before leaving it on permanently, I checked the syntax was correct. If it doesn't return any error, you're good. Then I restarted the service so the firewall would apply right away.

Comprobar que funciona Checking that it works

Para ver el estado real del cortafuegos y las reglas que se están aplicando en el momento, uso este comando:

sudo nft list ruleset

Y ahí sale la tabla montada tal cual la escribí. Desde ese momento:

  • Cualquier intento de acceso desde internet fuera de mi red local o de Tailscale se rechaza automáticamente.
  • Mi acceso por SSH desde la red local o desde Tailscale sigue funcionando al 100%.
  • Jellyfin, qBittorrent y las actualizaciones automáticas no han tenido ninguna interrupción.

To see the firewall's real status and the rules being applied at that moment, I use the command above. And there's the table exactly as I wrote it. From that point on:

  • Any access attempt from the internet outside my local network or Tailscale gets rejected automatically.
  • My SSH access from the local network or from Tailscale keeps working 100%.
  • Jellyfin, qBittorrent, and the automatic updates haven't had any interruption.