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.1en MySQL. Ambos funcionaron en nuestra prueba, y el túnel no necesita que se abran. - Prefiere también
127.0.0.1alocalhostcomo 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
psqlconecta a través de él. - El puerto 3306 o cualquier otro destino se rechaza.
-Rfalla conremote port forwarding failed for listen port.- Un acceso normal imprime
This account is currently not available. -L 5433:localhost:5432también se rechaza.PermitOpencompara texto, sin resolver nombres, así que el cliente debe pedir exactamente127.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
- ssh(1), manual de OpenBSD
- sshd_config(5), manual de OpenBSD
- sshd(8), formato del archivo authorized_keys, manual de OpenBSD
- PostgreSQL, conexiones TCP/IP seguras con túneles SSH
- PostgreSQL, parámetros de conexión de libpq
- MySQL 8.0, conexión al servidor