Túnel SSH paso a paso: entrá a un servicio que solo escucha en 127.0.0.1
Cómo usar un túnel SSH (-L) para entrar a un panel, una base de datos o una copia de prueba que escucha solo en 127.0.0.1, sin abrir puertos en el firewall. Con Docker, ~/.ssh/config, PuTTY, los errores típicos y los cuidados.
En la entrada de Roundcube y PHP2Shell hubo un paso que pasó rápido pero que vale oro: probar una copia del webmail en el servidor de producción, con datos reales, sin publicarla en ningún lado. La copia escuchaba solo en 127.0.0.1 y se entraba por un túnel SSH.
Es una de esas herramientas que, una vez que la usás, aparece en todos lados: entrar a un panel de administración que no querés exponer, conectarte a una base de datos desde tu editor o probar algo nuevo sin abrir un solo puerto en el firewall. Acá va paso a paso, con todo probado en un laboratorio.
La idea en una línea
Un túnel SSH (local port forwarding) hace que un puerto de tu máquina se comporte como si fuera un puerto del servidor. Lo que mandás a localhost:9102 en tu PC viaja cifrado por la conexión SSH y sale del otro lado hacia 127.0.0.1:9002 del servidor.
Tu PC Servidor
navegador ─► localhost:9102 ══ SSH (22) ══► 127.0.0.1:9002 (la app)
Para el servidor, el pedido viene de él mismo. Por eso llega a un servicio que no acepta conexiones de afuera.
Lo que necesitás
- Acceso SSH al servidor (usuario y clave, o mejor, llave).
- Un servicio escuchando en el servidor. Lo ideal: solo en
127.0.0.1. - En tu PC, el cliente
ssh. Linux y macOS lo traen. Windows 10 y 11 también (OpenSSH viene incluido; se usa igual desde PowerShell).
Paso 1: que el servicio escuche solo en 127.0.0.1
Este es el punto que hace que todo sea seguro: si el servicio escucha solo en la interfaz local, desde Internet no existe.
Con Docker, eso se define en ports, poniendo la IP delante:
services:
webmail-prueba:
image: roundcube/roundcubemail:1.6.20-apache
ports:
- "127.0.0.1:9002:80" # solo local
# - "9002:80" # NO: queda abierto en todas las interfaces
Ojo con esto: en Linux, Docker publica los puertos saltándose UFW (escribe sus propias reglas de iptables). Un "9002:80" queda abierto a Internet aunque UFW diga que está cerrado. Poner 127.0.0.1: adelante es la forma segura.
Si no es Docker, cada programa tiene su opción: bind-address = 127.0.0.1 en MySQL, listen_addresses = 'localhost' en PostgreSQL, Listen 127.0.0.1:9002 en Apache, --bind 127.0.0.1 en muchos servidores de desarrollo.
Comprobalo en el servidor:
ss -ltn | grep 9002
LISTEN 0 128 127.0.0.1:9002 0.0.0.0:*
Tiene que decir 127.0.0.1:9002. Si dice 0.0.0.0:9002 o *:9002, está abierto a todas las interfaces.
Y desde afuera no tiene que responder:
curl -m 3 http://IP-DEL-SERVIDOR:9002/
curl: (7) Failed to connect to ... port 9002 ... Could not connect to server
Paso 2: abrir el túnel
Desde tu PC:
ssh -L 9102:127.0.0.1:9002 usuario@servidor
Cómo se lee -L 9102:127.0.0.1:9002:
| Parte | Qué es |
|---|---|
9102 |
El puerto que se abre en tu PC |
127.0.0.1 |
A quién se conecta el servidor, visto desde el servidor |
9002 |
El puerto de destino en el servidor |
El 127.0.0.1 del medio es lo que más confunde: no es tu PC, es el servidor hablando consigo mismo. Ahí también podés poner otra máquina de la red interna del servidor (lo vemos más abajo).
Usé 9102 en vez de 9002 a propósito, para no confundir cuál es cuál. Puede ser cualquier puerto libre de tu PC por encima de 1024.
Paso 3: usarlo
Mientras esa sesión SSH esté abierta, en el navegador de tu PC:
http://localhost:9102
Y aparece la aplicación del servidor. En el laboratorio:
curl http://localhost:9102/
<h1>Webmail de prueba (solo 127.0.0.1)</h1>
En tu PC, el que escucha en ese puerto es el propio ssh, y solo en la interfaz local:
ss -ltnp | grep 9102
LISTEN 0 128 127.0.0.1:9102 0.0.0.0:* users:(("ssh",pid=45787,fd=5))
LISTEN 0 128 [::1]:9102 [::]:* users:(("ssh",pid=45787,fd=4))
Cerrás la sesión SSH y el túnel desaparece. No queda nada abierto.
Paso 4: túnel sin terminal (opcional)
Si no necesitás la consola del servidor y solo querés el túnel:
ssh -f -N -L 9102:127.0.0.1:9002 usuario@servidor
-N: no ejecuta nada en el servidor, solo el túnel.-f: se va a segundo plano después de conectar (y de pedirte la clave, si hace falta).
Para cerrarlo:
pgrep -af "ssh .*-L 9102" # ver cuál es
pkill -f "ssh .*-L 9102" # cerrarlo
Varios túneles en una sola conexión
Se pueden poner varios -L. Por ejemplo, el webmail de prueba y su base de datos, para mirarla con DBeaver o psql desde tu PC:
ssh -N -L 9102:127.0.0.1:9002 -L 15432:127.0.0.1:5432 usuario@servidor
psql -h localhost -p 15432 -U roundcube roundcube
Llegar a una máquina que está detrás del servidor
El destino no tiene que ser el servidor mismo. Si el servidor ve una red interna, el túnel puede terminar en otra máquina de esa red:
ssh -L 8006:10.0.0.20:8006 usuario@servidor
Ahora https://localhost:8006 en tu PC es el panel de 10.0.0.20, una máquina que desde tu casa no se ve. El tramo entre el servidor y 10.0.0.20 no va por SSH: va como iría normalmente dentro de esa red.
Y si para llegar al servidor primero tenés que pasar por un bastión (jump host):
ssh -J usuario@bastion -L 9102:127.0.0.1:9002 usuario@servidor
Dejarlo guardado en ~/.ssh/config
Si lo vas a usar seguido, no hace falta recordar el comando. En ~/.ssh/config de tu PC:
Host webmail-prueba
HostName servidor.ejemplo.com
User usuario
IdentityFile ~/.ssh/id_ed25519
LocalForward 9102 127.0.0.1:9002
LocalForward 15432 127.0.0.1:5432
ExitOnForwardFailure yes
Y después:
ssh -N webmail-prueba
ExitOnForwardFailure yes hace que, si el túnel no se puede abrir, ssh termine con error en vez de conectarse igual sin túnel (y dejarte pensando que anda).
Desde Windows con PuTTY
Si usás PuTTY en vez de la consola:
- Cargá la sesión del servidor como siempre.
- Andá a Connection → SSH → Tunnels.
- En Source port poné
9102y en Destination127.0.0.1:9002. Dejá marcado Local. - Tocá Add. Tiene que aparecer
L9102 127.0.0.1:9002en la lista. - Volvé a Session, guardá y conectá.
Con la sesión abierta, http://localhost:9102 en el navegador.
Los errores que vas a ver (y qué significan)
Todos estos los provoqué a propósito en el laboratorio.
El puerto de tu PC ya está ocupado:
bind [127.0.0.1]:9102: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 9102
Could not request local forwarding.
Otro programa (o un túnel anterior que quedó en segundo plano) ya usa el 9102. Cerralo o elegí otro puerto. Sin ExitOnForwardFailure, ssh se conecta igual y solo muestra este aviso.
El servicio del otro lado no está escuchando:
channel 2: open failed: connect failed: Connection refused
El túnel anda, pero en el servidor no hay nada en 127.0.0.1:9002. Revisá con ss -ltn en el servidor que la app esté levantada y en ese puerto.
El servidor no permite túneles:
channel 2: open failed: administratively prohibited: open failed
El sshd del servidor tiene AllowTcpForwarding no. Es una decisión del administrador: si el servidor es tuyo y lo necesitás, se habilita en /etc/ssh/sshd_config; si no es tuyo, hay que pedirlo.
La página carga, pero redirige a HTTPS o a otro dominio:
La aplicación está configurada para un nombre y protocolo de producción. En la copia de prueba, desactivá la redirección o ajustá la URL base. En el caso del webmail se usó un archivo de configuración extra que existía solo en la prueba.
Cuidados
- El túnel abre el puerto solo en tu PC (
127.0.0.1:9102). No lo cambies por-L 0.0.0.0:9102:...: eso lo comparte con toda tu red, y con cualquiera en el Wi-Fi del bar. - Usá llave, no contraseña, para entrar por SSH. Y si el servidor es tuyo, desactivá el login con contraseña.
- Cerralo al terminar. Un túnel olvidado en segundo plano es una puerta abierta hacia el servidor desde tu PC.
- En el servidor, se pueden permitir túneles solo a ciertos destinos con
PermitOpenensshd_config(por ejemplo,PermitOpen 127.0.0.1:9002). Útil si le das acceso a otra persona. - Al terminar la prueba, borrá lo que levantaste: en el caso del webmail,
docker-compose -p prueba down -v, que elimina contenedores y volúmenes de la copia.
Los otros dos túneles, en una línea
-R(remote forwarding): al revés. Un puerto del servidor lleva a tu PC. Sirve, por ejemplo, para mostrarle a alguien algo que corre en tu máquina.-D(dynamic): arma un proxy SOCKS. Configurás el navegador para usarlo y navegás como si estuvieras en el servidor, hacia cualquier destino.
Pero el -L es el del día a día. Con eso, el panel que no querías exponer, la base de datos y la copia de prueba quedan a un localhost de distancia, sin abrir nada al mundo.