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:
- Termine TLS una sola vez.
- Redirija el tráfico al contenedor correspondiente.
- Aísle cada servicio a nivel de red.
- 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:
-
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.
-
Redes Docker aisladas
- Cada servicio se coloca en su propia red (
bridge) sin acceso a otras redes. - La red
proxypermite solo la comunicación entre Caddy y los contenedores de backend.
- Cada servicio se coloca en su propia red (
-
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.
-
Firewall host‑level
- Regla
iptablesque 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.
- Regla
-
Docker Compose declarativo
- Un único archivo
docker-compose.ymldescribe los servicios, redes y volúmenes. - Permite reproducir el entorno en minutos y facilita actualizaciones.
- Un único archivo
Paso a paso resumido
-
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).
-
Estructura de directorios
selfhosted/ ├─ caddy/ │ └─ Caddyfile ├─ crowdsec/ │ └─ cs.yaml ├─ services/ │ ├─ jellyfin/ │ ├─ nextcloud/ │ └─ immich/ └─ docker-compose.yml -
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 } -
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: -
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
- Acceso HTTPS
- Navega a
https://jellyfin.starman.app. El certificado debe ser válido y emitido por Let’s Encrypt.
- Navega a
- Bloqueo de IP
- Simula un ataque con
curl -I http://jellyfin.starman.apprepetido 20 veces desde otra máquina. Revisa que la IP aparezca en la lista de bans de CrowdSec (docker exec crowdsec cscli bouncers list).
- Simula un ataque con
- Aislamiento de red
- Desde el contenedor
jellyfin, intentaping 172.18.0.2(IP denextcloud). Debería fallar.
- Desde el contenedor
- Reglas de firewall
- Ejecuta
iptables -L -v -ny confirma que solo los puertos 80/443 están ACCEPT.
- Ejecuta
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 statso 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.