Publicar lo que corre en tu portátil sin abrirle un puerto: Cloudflare → Nginx → Tailscale
Nuestro arnés de agentes vive en un portátil Windows y se usa desde el iPhone con HTTPS y login. El portátil no está en internet. Este es el camino de seis saltos que lo hace posible y por qué cada salto está ahí.
El problema: tienes un servicio web en una máquina que no debe estar en internet —tu portátil, una GX10— y quieres usarlo desde el móvil en cualquier parte, con una URL normal, HTTPS y sin instalar nada raro en el móvil.
La solución que usamos para todo (el arnés de agentes, la interfaz de Pi, el panel del laboratorio) es siempre la misma cadena:
Parece mucho. Cada salto hace una cosa y por eso se puede razonar sobre él.
Salto 1 · El DNS apunta al servidor, nunca a Tailscale
El registro A de agente.tudominio.es apunta a la IP pública del servidor (la que mantiene el
DNS dinámico de la guía 1). Nunca pongas una IP
100.x en un DNS público: no funciona desde fuera y además le cuenta a cualquiera cómo es tu
red interna.
Salto 2 · Nginx termina el HTTPS y reenvía por Tailscale
En el servidor, un vhost por servicio. Lo importante está en los comentarios:
server {
listen 443 ssl http2;
server_name agente.tudominio.es;
ssl_certificate /etc/letsencrypt/live/agente.tudominio.es/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/agente.tudominio.es/privkey.pem;
location / {
# La IP de Tailscale del portátil y el puerto del BRIDGE, no del servicio.
proxy_pass http://100.x.y.z:3084;
# WebSocket y streaming: sin esto, los agentes se cortan a los 60 s.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
# Safari puede no mandar Origin en un POST del mismo sitio (ver abajo).
proxy_set_header Origin https://agente.tudominio.es;
}
}
proxy_buffering off y el timeout largo no son opcionales: un agente que piensa dos minutos
antes de escribir el primer token parece, para un proxy con valores por defecto, una conexión
muerta.
Salto 3 · El bridge: login antes que nada
El servicio de verdad (el arnés, la interfaz de Pi) no se expone ni siquiera a Tailscale. Delante hay un pequeño servidor Node, el bridge, que:
- pide usuario y contraseña (los mismos para todos los servicios del laboratorio) y guarda una
cookie
SecureyHttpOnlyde hasta 30 días si marcas «mantener sesión»; - sin cookie, responde 401 a todo lo que no sea la pantalla de login;
- reenvía HTTP y WebSocket a
127.0.0.1del servicio real; - reescribe la cabecera
Hostal valor que espera el servicio.
Ese último punto nos costó una tarde: el arnés solo acepta ciertos endpoints si Host es
127.0.0.1:3080; al reenviar agente.tudominio.es, los menús de configuración devolvían
403 y parecía un fallo del arnés. No lo era.
Salto 4 · El servicio solo escucha en loopback
127.0.0.1:3080 el arnés
127.0.0.1:3082 su bridge (o en la IP de Tailscale, si Nginx llega directo)
Si alguien consiguiera saltarse el bridge, desde la red no hay nada a lo que conectarse.
La trampa de Safari y el Origin
Síntoma: la pantalla de login carga, metes la contraseña y el bridge responde
403 Invalid origin. Solo en iPhone.
Causa: Safari en iOS puede omitir Origin y Referer en un formulario del mismo sitio,
y un bridge con protección CSRF que exige una de las dos rechaza una petición legítima
(P-100). Como el bridge solo acepta conexiones del proxy conocido, el proxy
fija el Origin canónico (la última línea del vhost). No relajes la comprobación para
cualquiera: fíjala en el único sitio en el que confías.
Otra trampa: el nginx -t equivocado
Si tu servidor tiene un panel tipo aaPanel, hay dos Nginx instalados y nginx -t valida
el que no está sirviendo (P-043). Valida con el binario y la configuración
del panel antes de recargar.
Comprobación desde fuera
# Sin sesión: HTML → pantalla de login (200); cualquier otra cosa → 401
curl -sS -o /dev/null -w '%{http_code}\n' -H 'Accept: text/html' https://agente.tudominio.es/
curl -sS -o /dev/null -w '%{http_code}\n' https://agente.tudominio.es/api/algo
# Desde el servidor, ¿llega al bridge por Tailscale?
curl --max-time 8 -sS -o /dev/null -w '%{http_code}\n' http://100.x.y.z:3084/
Si Cloudflare da timeout pero la tercera orden funciona, el problema está entre el borde y tu servidor, no en el portátil.
Qué no hemos hecho, a propósito
- No hay puertos abiertos en el router salvo el 443 del servidor.
- No hay Tailscale Funnel: nada del portátil se publica directamente.
- Las notificaciones push con la app cerrada necesitarían un emisor de servidor y claves VAPID. No lo hemos montado sin una revisión de seguridad aparte: hoy el aviso de «un agente pide permiso» solo funciona con la web abierta.
Siguiente: cómo se despliega esta misma web
con un git push y nada más.