Cómo conectarse a PostgreSQL o MySQL a través de un túnel SSH

Ejecuta ssh -N -L 5433:127.0.0.1:5432 user@server y conecta tu cliente a 127.0.0.1 puerto 5433: psql -h 127.0.0.1 -p 5433 -U app appdb. Para MySQL usa -L 3307:127.0.0.1:3306 y mysql -h 127.0.0.1 -P 3307 -u app -p appdb. La base de datos puede seguir cerrada a internet, escuchando solo en su propia dirección de loopback.

Actualizada el . Ejecutado con OpenSSH 10.3 y OpenSSH 10.0 como clientes, a través de OpenSSH 10.0 en Debian 13, hacia PostgreSQL 17 y MySQL 8.0 escuchando solo en 127.0.0.1, incluido verify-full con host y hostaddr, y el usuario solo de túnel.

PostgreSQL

En una terminal, deja el túnel en marcha:

ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:5433:127.0.0.1:5432 user@server

En otra:

psql -h 127.0.0.1 -p 5433 -U app appdb

El puerto 5433 es cualquier puerto libre de tu equipo. Usar uno distinto de 5432 evita un choque con un PostgreSQL local. Una cadena de conexión funciona igual, en cualquier herramienta o biblioteca:

postgresql://app@127.0.0.1:5433/appdb

En una herramienta gráfica de base de datos, pon el host 127.0.0.1 y el puerto 5433 con el usuario de la base de datos, y deja desactivada la opción SSH de la propia herramienta, porque el túnel ya hace ese trabajo.

En el servidor, la conexión parece venir del propio servidor. En nuestra prueba, inet_client_addr() devolvió 127.0.0.1. Por eso pg_hba.conf necesita una línea para esa dirección, que la mayoría de los archivos por defecto ya tienen:

host  appdb  app  127.0.0.1/32  scram-sha-256

MySQL

ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:3307:127.0.0.1:3306 user@server
mysql -h 127.0.0.1 -P 3307 -u app -p appdb

Escribe 127.0.0.1 y no localhost. El cliente de MySQL interpreta localhost como una petición del socket Unix local, ignora el puerto y falla:

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

--protocol=TCP también lo arregla, si de verdad quieres escribir localhost.

La cuenta debe estar permitida desde la dirección que ve el servidor. Un usuario creado como 'app'@'localhost' puede fallar por TCP. En la imagen oficial de MySQL, que tiene skip_name_resolve activado, falló con Access denied for user 'app'@'127.0.0.1'. Crea la cuenta para esa dirección:

CREATE USER 'app'@'127.0.0.1' IDENTIFIED BY '...';
GRANT ALL ON appdb.* TO 'app'@'127.0.0.1';

Qué dirección va en cada sitio

-L 127.0.0.1:5433:127.0.0.1:5432
   |         |    |         |
   |         |    |         +- port of the database
   |         |    +- database address, as seen by the SSH server
   |         +- port on your machine
   +- address to listen on, on your machine
  • Deja la primera dirección como 127.0.0.1, para que nadie más de tu red pueda usar tu túnel.
  • La base de datos puede escuchar solo en loopback: listen_addresses = 'localhost' en PostgreSQL, bind-address = 127.0.0.1 en MySQL. Ambos funcionaron en nuestra prueba, y el túnel no necesita que se abran.
  • Prefiere también 127.0.0.1 a localhost como destino. El servidor SSH resuelve el nombre, y podría probar IPv6 primero.

Los reenvíos remotos y dinámicos siguen el mismo esquema, mira túneles SSH locales, remotos y dinámicos.

Si la base de datos está en otra máquina, como un servicio gestionado, ponla como destino. Entonces la base de datos ve la dirección privada del servidor SSH, que es lo que necesita su lista de permitidos:

ssh -N -L 127.0.0.1:5433:db.internal.example.com:5432 user@server

TLS sigue importando

SSH cifra desde tu equipo hasta el servidor SSH. Si la base de datos está en otra máquina, el último tramo queda fuera de ese cifrado, así que usa también TLS en la conexión a la base de datos.

La comprobación del certificado requiere cuidado extra, porque tu cliente cree que se conecta a 127.0.0.1, y eso no está en el certificado:

psql "host=127.0.0.1 port=5433 user=app dbname=appdb sslmode=verify-full sslrootcert=ca.crt"
... server certificate for "db.example.com" does not match host name "127.0.0.1"

En PostgreSQL, pon en host el nombre del certificado y en hostaddr el túnel. Esto pasó nuestra prueba con verificación completa:

psql "host=db.example.com hostaddr=127.0.0.1 port=5433 user=app dbname=appdb sslmode=verify-full sslrootcert=ca.crt"

MySQL no tiene un ajuste equivalente. --ssl-mode=VERIFY_IDENTITY comprueba el nombre que escribiste, así que necesitarías un certificado que incluya 127.0.0.1, o un nombre en tu archivo hosts que apunte a esa dirección. REQUIRED a secas cifra sin comprobar quién respondió.

Un usuario que solo puede abrir este túnel

Una persona o una aplicación que solo necesita la base de datos no debería tener una shell en el servidor. Crea un usuario sin shell:

sudo useradd -m -s /usr/sbin/nologin dbtunnel

Instala su clave pública en /home/dbtunnel/.ssh/authorized_keys. Después añade un archivo drop-in, por ejemplo /etc/ssh/sshd_config.d/10-dbtunnel.conf. Match termina al final de ese archivo, así que no afecta a los ajustes posteriores:

Match User dbtunnel
  AllowTcpForwarding local
  PermitOpen 127.0.0.1:5432
  X11Forwarding no
  AllowAgentForwarding no
  PermitTTY no
  ForceCommand /usr/sbin/nologin

Ejecuta sudo sshd -t, recarga y conecta:

ssh -N -L 127.0.0.1:5433:127.0.0.1:5432 dbtunnel@server

Lo que vimos con esta configuración:

  • El túnel de arriba funciona, y psql conecta a través de él.
  • El puerto 3306 o cualquier otro destino se rechaza.
  • -R falla con remote port forwarding failed for listen port.
  • Un acceso normal imprime This account is currently not available.
  • -L 5433:localhost:5432 también se rechaza. PermitOpen compara texto, sin resolver nombres, así que el cliente debe pedir exactamente 127.0.0.1.

Para limitar una sola clave, puedes usar opciones en authorized_keys, en una línea:

restrict,port-forwarding,permitopen="127.0.0.1:5432" ssh-ed25519 AAAA... dbtunnel

Esto también bloquea la shell y otros destinos. En nuestra prueba seguía permitiendo -R, así que abrir un puerto en el servidor sigue siendo posible. El bloque Match con AllowTcpForwarding local lo cierra, y por eso lo preferimos.

Este usuario solo puede llegar al acceso de la propia base de datos. La cuenta de la base de datos debe tener aun así su propia contraseña o certificado, y solo los permisos que necesita.

Preguntas

¿Por qué usar el puerto 5433 en mi lado en vez de 5432?

Sirve cualquier puerto libre. Uno distinto evita el choque con un PostgreSQL que ya funcione en tu máquina.

¿Por qué mysql ignora el puerto si escribo localhost?

El cliente de MySQL trata localhost como el socket Unix local. Usa 127.0.0.1, o añade --protocol=TCP.

¿El tráfico de la base de datos va cifrado dentro de un túnel SSH?

Desde tu máquina hasta el servidor SSH, sí. Si la base de datos está en otra máquina, el último tramo queda fuera del túnel, así que usa también TLS.

Fuentes

Más en Túneles y jump hosts

Todas las guías