Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos

Necesitaba un entorno de desarrollo con despliegues frecuentes, URLs públicas con HTTPS y acceso cómodo a las bases de datos para depurar. Lo que no quería era un servidor en Internet con los puertos 80 y 443 abiertos, e

Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos

Exponer entornos de desarrollo con Cloudflare Tunnel y WARP sin abrir puertos

Necesitaba un entorno de desarrollo con despliegues frecuentes, URLs públicas con HTTPS y acceso cómodo a las bases de datos para depurar. Lo que no quería era un servidor en Internet con los puertos 80 y 443 abiertos, esperando a que alguien lo escanee.

Esos requisitos tiran en direcciones opuestas: mientras más fácil el acceso, mayor la exposición. Esta es la historia de la combinación que me funcionó:

  • Cloudflare Tunnel para publicar las apps sin abrir los puertos 80 y 443.
  • Traefik como enrutador interno, con labels por aplicación.
  • Portainer para administrar los contenedores.
  • Cloudflare One WARP para llegar a la red privada de Docker solo desde mi máquina.

Voy a explicar el diseño, el detalle que rompe Laravel detrás del proxy y, sobre todo, los riesgos.

La idea: que el servidor llame hacia afuera

La forma habitual de publicar una app es abrir un puerto y dejar que el mundo se conecte. El túnel invierte eso:

Internet → Cloudflare (TLS) → túnel (conexión saliente) → Traefik → contenedores

La clave: el conector cloudflared abre la conexión hacia Cloudflare, y no al revés. El servidor no necesita puertos de entrada abiertos. Cloudflare termina el TLS, y dentro del servidor el tráfico va por HTTP hacia Traefik.

Eso resuelve la exposición y los certificados de un solo golpe. Pero un túnel solo lleva el tráfico hasta la puerta; algo tiene que decidir qué contenedor responde.

Elegir el enrutador

Despliego con mucha frecuencia. Con Traefik, cada aplicación declara su enrutamiento con labels en su propio docker-compose, y no hay configuración central que tocar por cada proyecto nuevo.

Antes evalué Dokploy para desarrollo. Es cómodo, pero me pareció pesado en recursos para un servidor pequeño. El camino que elegí (Portainer + Traefik + cloudflared) es más liviano, al precio de escribir a mano los labels de Traefik. Es un intercambio entre comodidad y consumo.

Armar las piezas

La base es una red compartida, Traefik sin HTTPS (de eso se encarga Cloudflare) y cloudflared con el token del túnel:

services:
traefik:
image: traefik:v3
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [edge]

cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
networks: [edge]

networks:

edge:

name: edge

Fíjate en que ningún servicio publica puertos (ports:). Es intencional y es la mejor defensa: si no se publica ningún puerto, no hay nada que puedas exponer por accidente.

En el panel de Cloudflare configuras el public hostname (por ejemplo, un comodín bajo tu dominio) apuntando al servicio http://traefik:80.

A partir de ahí, cada app nueva solo se une a la misma red y declara su enrutamiento:

services:

myapp:

image: myapp:dev

networks: [edge]

labels:

- traefik.enable=true

- traefik.http.routers.myapp.rule=Host(myapp.dev.example.com)

- traefik.http.routers.myapp.entrypoints=web

- traefik.http.services.myapp.loadbalancer.server.port=80

networks:

edge:

external: true

Con eso, una app queda en vivo por HTTPS. Pero si es una app Laravel, hay un detalle.

El detalle que rompe Laravel

Como el TLS termina en Cloudflare y Traefik habla HTTP con la app, Laravel ve peticiones HTTP. Los síntomas típicos: URLs generadas con http://, redirecciones rotas y mixed content (contenido mixto).

Se arregla confiando en los headers X-Forwarded- del proxy y forzando HTTPS. En Laravel 11 o superior, en bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {

$middleware->trustProxies(at: '
');

})

Y en el service provider, forzar el esquema:

URL::forceScheme('https');

Funciona. Pero mira bien ese '', porque trae un riesgo.

La advertencia importante

trustProxies(at: '') significa "confío en los headers de cualquiera". Solo es seguro si el origen es inalcanzable salvo a través del túnel. Si alguien puede llegar directo al contenedor, puede falsificar X-Forwarded-For o X-Forwarded-Proto.

Por eso conviene combinar tres cosas:

  1. No publicar los puertos de las apps.
  2. Cerrar 80/443 en el firewall.
  3. Limitar en Traefik qué IPs son de confianza para los headers reenviados (forwardedHeaders.trustedIPs en el entrypoint), usando la red interna del túnel.

Lo que lleva a la pregunta de la que depende todo el diseño: ¿está realmente cerrado el origen?

Cerrar los puertos de verdad

Hay capas, de la más a la menos confiable:

  1. No publiques puertos en Docker. Sin ports:, no hay nada que filtrar.
  2. Firewall del proveedor de nube (reglas de seguridad de red): permite solo lo estrictamente necesario, por ejemplo SSH desde tu IP.
  3. Reglas en el host como defensa adicional. Ojo: Docker manipula iptables por su cuenta y puede saltarse reglas pensadas para firewalls de nivel superior; para filtrar el tráfico hacia contenedores publicados, la cadena DOCKER-USER es el lugar previsto.

Verifica siempre desde afuera con un escaneo de puertos, no solo leyendo la configuración.

La última pieza: llegar a las bases de datos

Las apps eran públicas y los puertos estaban cerrados. Pero para depurar yo seguía queriendo conectarme a las bases de datos del entorno, sin exponerlas a nadie.

Ahí entra Cloudflare One WARP. Con una ruta de red privada a través del mismo túnel, mi máquina llega a la red de Docker como si estuviera dentro. Esto permite:

  • Darles a las bases de datos una IP fija en la red interna y conectarme con mi cliente de siempre.
  • Que solo los dispositivos autorizados en la política de Zero Trust puedan acceder.
  • No abrir ningún puerto de base de datos hacia afuera.

Producción es otra decisión

¿Se traslada la misma configuración a producción? No del todo. En producción uso Portainer sin Traefik: el túnel apunta directamente al contenedor de cada aplicación. Menos piezas, menos superficie y menos cosas que pueden fallar. Traefik me resuelve un problema de velocidad de despliegue en desarrollo; en producción, donde los cambios son menos frecuentes, ese problema no existe.

Lo que cedes

Nada de esto es gratis:

  • Dependencia de Cloudflare. Si el servicio tiene una caída, tus entornos quedan inalcanzables. Para desarrollo es tolerable; para producción hay que evaluarlo.
  • El token del túnel es un secreto. Quien lo tenga puede ejecutar un conector. Guárdalo como secreto, no en el repositorio.
  • El socket de Docker. Traefik lo monta en solo lectura, pero sigue siendo un acceso sensible. Hay alternativas con un proxy de socket que limita las operaciones permitidas.
  • Labels escritos a mano. Es más trabajo por proyecto que un panel integrado.
  • Mantenimiento de WARP. Hay que gestionar el enrolamiento de dispositivos y las políticas de acceso.
  • Versiones fijadas. Evita latest para cloudflared y Traefik si quieres despliegues reproducibles.

Lo que me llevo

El servidor que quería mantener oculto está oculto: nada escucha en Internet público, y aun así despliego con frecuencia con HTTPS y llego a las bases de datos desde mi propia máquina.

Si te quedas con pocas cosas:

  • Un túnel saliente elimina la necesidad de abrir puertos y simplifica el TLS.
  • No publicar puertos en Docker es la defensa más fuerte y más barata.
  • trustProxies(at: '*') solo es seguro si el origen es inalcanzable por cualquier otra ruta.
  • Traefik acelera el desarrollo; en producción, menos piezas suelen ser mejor.
  • Verifica la exposición desde afuera, no solo en la configuración.
📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.