Comment se connecter à PostgreSQL ou MySQL via un tunnel SSH

Exécutez ssh -N -L 5433:127.0.0.1:5432 user@server, puis connectez votre client à 127.0.0.1 sur le port 5433 : psql -h 127.0.0.1 -p 5433 -U app appdb. Pour MySQL, utilisez -L 3307:127.0.0.1:3306 et mysql -h 127.0.0.1 -P 3307 -u app -p appdb. La base peut rester fermée à Internet, à l'écoute uniquement sur sa propre adresse de bouclage.

Mis à jour le . Exécuté avec OpenSSH 10.3 et OpenSSH 10.0 comme clients, via OpenSSH 10.0 sous Debian 13, vers PostgreSQL 17 et MySQL 8.0 à l'écoute sur 127.0.0.1 uniquement, y compris verify-full avec host et hostaddr, et l'utilisateur réservé au tunnel.

PostgreSQL

Dans un terminal, laissez le tunnel tourner :

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

Dans un autre :

psql -h 127.0.0.1 -p 5433 -U app appdb

Le port 5433 est un port libre quelconque sur votre machine. Utiliser autre chose que 5432 évite un conflit avec un PostgreSQL local. Une chaîne de connexion fonctionne de la même façon, dans n'importe quel outil ou bibliothèque :

postgresql://app@127.0.0.1:5433/appdb

Dans un outil graphique de base de données, indiquez l'hôte 127.0.0.1 et le port 5433 avec l'identifiant de la base, et laissez désactivée l'option SSH de l'outil, puisque le tunnel s'en charge déjà.

Côté serveur, la connexion semble venir du serveur lui-même. Dans notre test, inet_client_addr() a renvoyé 127.0.0.1. Il faut donc une ligne pour cela dans pg_hba.conf, que la plupart des fichiers par défaut ont déjà :

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

Écrivez 127.0.0.1 et non localhost. Le client MySQL interprète localhost comme une demande du socket Unix local, ignore le port et échoue :

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

--protocol=TCP règle aussi le problème, si vous tenez vraiment à saisir localhost.

Le compte doit être autorisé depuis l'adresse que voit le serveur. Un utilisateur créé comme 'app'@'localhost' peut échouer en TCP. Sur l'image MySQL officielle, qui a skip_name_resolve activé, cela a échoué avec Access denied for user 'app'@'127.0.0.1'. Créez le compte pour cette adresse :

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

Quelle adresse va où

-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
  • Gardez la première adresse à 127.0.0.1, pour que personne d'autre sur votre réseau ne puisse utiliser votre tunnel.
  • La base peut écouter uniquement sur le bouclage : listen_addresses = 'localhost' dans PostgreSQL, bind-address = 127.0.0.1 dans MySQL. Les deux ont fonctionné dans notre test, et le tunnel n'a pas besoin qu'on les ouvre davantage.
  • Préférez aussi 127.0.0.1 à localhost comme destination. Le serveur SSH résout le nom, et il peut essayer l'IPv6 d'abord.

Les redirections distantes et dynamiques suivent le même schéma, voir tunnels SSH locaux, distants et dynamiques.

Si la base est sur une autre machine, comme un service managé, nommez-la comme destination. La base voit alors l'adresse privée du serveur SSH, c'est donc celle dont sa liste d'autorisation a besoin :

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

TLS reste important

SSH chiffre depuis votre machine jusqu'au serveur SSH. Si la base est sur une autre machine, le dernier tronçon est hors de ce chiffrement : utilisez donc aussi TLS sur la connexion à la base.

La vérification des certificats demande une attention particulière, car votre client croit se connecter à 127.0.0.1, qui ne figure pas sur le certificat :

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"

Dans PostgreSQL, mettez dans host le nom du certificat et dans hostaddr le tunnel. Cela a passé notre test avec vérification complète :

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

MySQL n'a pas de réglage équivalent. --ssl-mode=VERIFY_IDENTITY vérifie le nom que vous avez saisi : il vous faudrait donc un certificat qui liste 127.0.0.1, ou un nom dans votre fichier hosts qui y pointe. Le simple REQUIRED chiffre sans vérifier qui a répondu.

Un utilisateur qui ne peut ouvrir que ce tunnel

Une personne ou une application qui n'a besoin que de la base ne devrait pas obtenir de shell sur le serveur. Créez un utilisateur sans shell :

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

Installez sa clé publique dans /home/dbtunnel/.ssh/authorized_keys. Ajoutez ensuite un fichier drop-in, par exemple /etc/ssh/sshd_config.d/10-dbtunnel.conf. Match s'arrête à la fin de ce fichier : il n'affecte donc pas les réglages qui suivent :

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

Exécutez sudo sshd -t, rechargez, puis connectez-vous :

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

Ce que nous avons observé avec cette configuration :

  • Le tunnel ci-dessus fonctionne, et psql s'y connecte.
  • Le port 3306 ou toute autre destination est refusé.
  • -R échoue avec remote port forwarding failed for listen port.
  • Une connexion normale affiche This account is currently not available.
  • -L 5433:localhost:5432 est refusé aussi. PermitOpen compare du texte, sans résolution de noms : le client doit donc demander exactement 127.0.0.1.

Pour limiter une seule clé à la place, vous pouvez utiliser des options dans authorized_keys, sur une seule ligne :

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

Cela bloque aussi le shell et les autres destinations. Dans notre test, -R restait autorisé : ouvrir un port sur le serveur reste donc possible. Le bloc Match avec AllowTcpForwarding local le ferme, et c'est pour cette raison que nous le préférons.

Cet utilisateur ne peut atteindre que la connexion propre à la base. Le compte de la base doit quand même avoir son propre mot de passe ou certificat, et seulement les droits dont il a besoin.

Questions

Pourquoi utiliser le port 5433 de mon côté plutôt que 5432 ?

N'importe quel port libre convient. Un port différent évite un conflit avec un PostgreSQL qui tourne déjà sur votre machine.

Pourquoi mysql ignore-t-il le port quand j'écris localhost ?

Le client MySQL traite localhost comme le socket Unix local. Utilisez 127.0.0.1, ou ajoutez --protocol=TCP.

Le trafic de la base est-il chiffré dans un tunnel SSH ?

De votre machine jusqu'au serveur SSH, oui. Si la base tourne sur une autre machine, le dernier tronçon est hors du tunnel : utilisez aussi TLS.

Sources

Autres guides : Tunnels et hôtes de rebond

Tous les guides