Problema

Exponer servicios auto‑hospedados (media, archivos, gestión de fotos, etc.) directamente a Internet aumenta la superficie de ataque. Cada aplicación necesita su propio certificado TLS, reglas de firewall y actualizaciones de seguridad. Cuando varios servicios comparten el mismo host, un compromiso en uno puede escalar rápidamente a los demás o a la red interna. La necesidad recurrente es disponer de una capa única que:

  1. Termine TLS una sola vez.
  2. Redirija el tráfico al contenedor correspondiente.
  3. Aísle cada servicio a nivel de red.
  4. Bloquee patrones de ataque antes de que lleguen a la aplicación.

Causa

Los fallos habituales provienen de combinaciones de configuraciones débiles:

  • Exposición directa de puertos: Cada contenedor abre su puerto en la interfaz pública, lo que permite escaneos y ataques a cualquier servicio sin filtro.
  • Falta de aislamiento de red: Docker comparte el kernel del host; si un contenedor se vulnera, el atacante puede intentar escalar al host o a otros contenedores.
  • Gestión manual de certificados: Renovar y sincronizar certificados en varios contenedores genera errores y tiempo de inactividad.
  • Ausencia de detección de comportamiento: Sin un motor de bouncer, los intentos de fuerza bruta o escaneo continúan consumiendo recursos y generando logs ruidosos.
  • Configuración de router incompleta: Port‑forward sin filtrado, UPnP habilitado y NAT mal gestionado facilitan el acceso no autorizado.

Solución

Una arquitectura basada en Caddy como único punto de entrada, Docker Compose para orquestar contenedores y CrowdSec como bouncer de seguridad cubre los puntos críticos:

  1. Caddy como reverse proxy

    • Gestiona automáticamente certificados con Let’s Encrypt.
    • Define rutas por nombre de host (Host) y reescribe a la red interna del contenedor.
    • Centraliza la terminación TLS, evitando certificados duplicados.
  2. Redes Docker aisladas

    • Cada servicio se coloca en su propia red (bridge) sin acceso a otras redes.
    • La red proxy permite solo la comunicación entre Caddy y los contenedores de backend.
  3. CrowdSec integrado

    • Caddy exporta logs a CrowdSec mediante un bouncer compilado.
    • Cuando se detecta comportamiento sospechoso, CrowdSec inserta la IP en una lista de bloqueo que Caddy consulta en tiempo real.
  4. Firewall host‑level

    • Regla iptables que bloquea todo el tráfico entrante salvo el puerto 80/443 hacia Caddy.
    • Regla que impide que los contenedores alcancen la LAN interna del host.
  5. Docker Compose declarativo

    • Un único archivo docker-compose.yml describe los servicios, redes y volúmenes.
    • Permite reproducir el entorno en minutos y facilita actualizaciones.

Paso a paso resumido

  1. Pre‑requisitos

    • Host Linux con Docker y Docker Compose instalados.
    • Puerto 80/443 abierto en el router y apuntando al host.
    • Dominio apuntado a la IP pública (o DDNS).
  2. Estructura de directorios

    selfhosted/
    ├─ caddy/
    │   └─ Caddyfile
    ├─ crowdsec/
    │   └─ cs.yaml
    ├─ services/
    │   ├─ jellyfin/
    │   ├─ nextcloud/
    │   └─ immich/
    └─ docker-compose.yml
    
  3. Caddyfile básico

    # caddy/Caddyfile
    {
        email [email protected]
        acme_ca https://acme-v02.api.letsencrypt.org/directory
    }
    
    jellyfin.starman.app {
        reverse_proxy jellyfin:8096
    }
    
    photos.starman.app {
        reverse_proxy immich:3001
    }
    
    cloud.starman.app {
        reverse_proxy nextcloud:80
    }
    
  4. docker-compose.yml esencial

    version: "3.9"
    
    services:
      caddy:
        image: caddy:latest
        restart: unless-stopped
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./caddy/Caddyfile:/etc/caddy/Caddyfile
          - caddy_data:/data
          - caddy_config:/config
        networks:
          - proxy
        depends_on:
          - crowdsec
    
      crowdsec:
        image: crowdsecurity/crowdsec:latest
        restart: unless-stopped
        volumes:
          - crowdsec_data:/var/lib/crowdsec
          - ./crowdsec/cs.yaml:/etc/crowdsec/config.yaml
        networks:
          - proxy
    
      jellyfin:
        image: jellyfin/jellyfin:latest
        restart: unless-stopped
        networks:
          - jellyfin_net
          - proxy
        volumes:
          - jellyfin_data:/config
          - jellyfin_media:/media
    
      immich:
        image: immich/immich-server:latest
        restart: unless-stopped
        networks:
          - immich_net
          - proxy
        volumes:
          - immich_data:/usr/src/app/upload
    
      nextcloud:
        image: nextcloud:latest
        restart: unless-stopped
        networks:
          - nextcloud_net
          - proxy
        volumes:
          - nextcloud_data:/var/www/html
    
    networks:
      proxy:
        driver: bridge
      jellyfin_net:
        driver: bridge
      immich_net:
        driver: bridge
      nextcloud_net:
        driver: bridge
    
    volumes:
      caddy_data:
      caddy_config:
      crowdsec_data:
      jellyfin_data:
      jellyfin_media:
      immich_data:
      nextcloud_data:
    
  5. Regla iptables mínima

    # Bloquear todo excepto Caddy
    iptables -P INPUT DROP
    iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
    iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
    iptables -A INPUT -i lo -j ACCEPT
    # Permitir salida
    iptables -P OUTPUT ACCEPT
    # Evitar que contenedores accedan a la LAN
    iptables -A DOCKER-USER -s 172.18.0.0/16 -d 192.168.0.0/16 -j DROP
    

Cuándo aplicar esta solución

  • Necesitas exponer varios servicios (media, archivos, fotos) a usuarios externos sin que cada uno gestione TLS.
  • Quieres limitar la superficie de ataque a un único punto de entrada.
  • Tienes experiencia básica con Docker y puedes mantener actualizaciones regulares.
  • El host está detrás de un router con IP pública o usas DDNS; no sirve detrás de CGNAT sin túnel adicional.

No aplicar si:

  • Solo tú accedes a los servicios → una VPN (WireGuard/Tailscale) es más sencilla.
  • El hardware es extremadamente limitado y no soporta Caddy + CrowdSec simultáneamente.
  • No puedes abrir puertos 80/443 en el router (p.ej., ISP bloquea).

Código

# Clonar repositorio de referencia
git clone https://github.com/empty-town/selfhosted.git
cd selfhosted

# Iniciar stack
docker compose up -d

# Verificar que Caddy obtuvo certificados
docker logs caddy 2>&1 | grep "TLS handshake"

Verificación

  1. Acceso HTTPS
    • Navega a https://jellyfin.starman.app. El certificado debe ser válido y emitido por Let’s Encrypt.
  2. Bloqueo de IP
    • Simula un ataque con curl -I http://jellyfin.starman.app repetido 20 veces desde otra máquina. Revisa que la IP aparezca en la lista de bans de CrowdSec (docker exec crowdsec cscli bouncers list).
  3. Aislamiento de red
    • Desde el contenedor jellyfin, intenta ping 172.18.0.2 (IP de nextcloud). Debería fallar.
  4. Reglas de firewall
    • Ejecuta iptables -L -v -n y confirma que solo los puertos 80/443 están ACCEPT.

Notas adicionales

  • Renovación automática: Caddy renueva certificados cada 90 días. No es necesario programar cron.
  • Backups: Monta volúmenes críticos (caddy_data, crowdsec_data, y los datos de cada servicio) en una ubicación fuera del host o usa snapshots ZFS.
  • Actualizaciones: Cada vez que actualices una imagen, ejecuta docker compose pull && docker compose up -d.
  • Monitorización ligera: docker stats o Prometheus con el exporter de Caddy brinda visibilidad sin sobrecargar el host.
  • Extensión de CrowdSec: Puedes añadir parsers personalizados para detectar intentos de login en Nextcloud o Jellyfin.

Con esta arquitectura, el proceso de exponer servicios auto‑hospedados se reduce a mantener un único punto de control, mientras que cada aplicación sigue aislada y protegida. La combinación de Caddy, Docker y CrowdSec brinda un equilibrio práctico entre seguridad, simplicidad y coste para cualquier homelab serio.