[décision] Fermer l'accès distant à PostgreSQL et passer par un tunnel SSH #65

Closed
opened 2026-09-02 08:32:42 +00:00 by lenaic · 2 comments
Owner

Contexte

PostgreSQL écoute sur 10.105.200.41:5433 en plus de la boucle locale, pg_hba.conf accepte host all all 0.0.0.0/0, et ufw ouvre 5433 à tout le monde. Vérifié depuis un poste tiers le 1er et le 2 septembre : la connexion aboutit. Seul un mot de passe sépare les quatre autres groupes de la salle de enervision_prod.

L'ouverture est délibérée, le #26 portait le critère « remote access par role », et le besoin derrière est légitime : se connecter à la base depuis son poste avec un client graphique. Mais elle contredit l'ENF-14, qui dit que ce qui ne doit pas sortir écoute sur 127.0.0.1, et le document ajoute que tant que le cloisonnement repose sur l'adresse d'écoute, cette règle est la seule frontière de sécurité que nous ayons.

Options envisagées

  1. Tunnel SSH. La base repasse en écoute locale. Chacun configure une fois l'onglet SSH de son client, DBeaver, pgAdmin et DataGrip en ont un, et le tunnel s'ouvre et se ferme tout seul à la connexion.
  2. Restreindre par adresse. Le port reste ouvert mais ufw et pg_hba.conf n'autorisent que les postes connus de l'équipe.
  3. Laisser ouvert et réviser l'ENF-14 en écrivant pourquoi.

Sur quoi on tranche

Ce qui reste exposé aux quatre autres groupes, ce qu'il faut maintenir quand une adresse change, et le confort réel une fois la configuration faite.

L'option 2 réduit beaucoup l'exposition sans la supprimer : 172.26.44.0/24 est le réseau des postes de l'école, partagé au delà du groupe. Elle demande de tenir une liste d'adresses à jour à chaque bail DHCP.

Décision retenue

Option 1, le tunnel SSH. Elle ne dépend d'aucune liste à maintenir, elle tient l'ENF-14 telle qu'elle est écrite sans révision à justifier, et le confort perdu est nul une fois le client configuré.

Alternative écartée, et pourquoi

L'ouverture restreinte par adresse, écartée parce qu'elle laisse le port joignable depuis un réseau partagé avec d'autres groupes et qu'elle impose un entretien manuel pour un gain de confort qui n'existe qu'à la première connexion.

Critères d'acceptation

  • listen_addresses = 'localhost' dans postgresql.conf, service redémarré
  • Les lignes 0.0.0.0/0 et ::/0 retirées de pg_hba.conf
  • ufw ne laisse plus passer 5433
  • Depuis un poste tiers, 10.105.200.41:5433 ne répond plus
  • Depuis un tunnel, la connexion à enervision_prod fonctionne
  • La marche à suivre pour configurer un client est dans docs/runbooks/postgresql.md
  • Le message est passé à l'équipe avant l'application, personne n'est coupé sans prévenir

Comment on le vérifie

Commande   depuis un autre poste : nc -zv 10.105.200.41 5433
Attendu    connexion refusée
Puis       ssh -L 5433:127.0.0.1:5433 <prénom>@10.105.200.41, puis psql sur localhost:5433
Preuve     les deux sorties collées à la fermeture

Fiche ADR produite

docs/adr/0002-acces-a-la-base-par-tunnel.md

### Contexte PostgreSQL écoute sur `10.105.200.41:5433` en plus de la boucle locale, `pg_hba.conf` accepte `host all all 0.0.0.0/0`, et `ufw` ouvre 5433 à tout le monde. Vérifié depuis un poste tiers le 1er et le 2 septembre : la connexion aboutit. Seul un mot de passe sépare les quatre autres groupes de la salle de `enervision_prod`. L'ouverture est délibérée, le #26 portait le critère « remote access par role », et le besoin derrière est légitime : se connecter à la base depuis son poste avec un client graphique. Mais elle contredit l'ENF-14, qui dit que ce qui ne doit pas sortir écoute sur `127.0.0.1`, et le document ajoute que tant que le cloisonnement repose sur l'adresse d'écoute, cette règle est la seule frontière de sécurité que nous ayons. ### Options envisagées 1. **Tunnel SSH.** La base repasse en écoute locale. Chacun configure une fois l'onglet SSH de son client, DBeaver, pgAdmin et DataGrip en ont un, et le tunnel s'ouvre et se ferme tout seul à la connexion. 2. **Restreindre par adresse.** Le port reste ouvert mais `ufw` et `pg_hba.conf` n'autorisent que les postes connus de l'équipe. 3. **Laisser ouvert** et réviser l'ENF-14 en écrivant pourquoi. ### Sur quoi on tranche Ce qui reste exposé aux quatre autres groupes, ce qu'il faut maintenir quand une adresse change, et le confort réel une fois la configuration faite. L'option 2 réduit beaucoup l'exposition sans la supprimer : `172.26.44.0/24` est le réseau des postes de l'école, partagé au delà du groupe. Elle demande de tenir une liste d'adresses à jour à chaque bail DHCP. ### Décision retenue **Option 1, le tunnel SSH.** Elle ne dépend d'aucune liste à maintenir, elle tient l'ENF-14 telle qu'elle est écrite sans révision à justifier, et le confort perdu est nul une fois le client configuré. ### Alternative écartée, et pourquoi L'ouverture restreinte par adresse, écartée parce qu'elle laisse le port joignable depuis un réseau partagé avec d'autres groupes et qu'elle impose un entretien manuel pour un gain de confort qui n'existe qu'à la première connexion. ### Critères d'acceptation - [x] `listen_addresses = 'localhost'` dans `postgresql.conf`, service redémarré - [x] Les lignes `0.0.0.0/0` et `::/0` retirées de `pg_hba.conf` - [x] `ufw` ne laisse plus passer 5433 - [x] Depuis un poste tiers, `10.105.200.41:5433` ne répond plus - [x] Depuis un tunnel, la connexion à `enervision_prod` fonctionne - [x] La marche à suivre pour configurer un client est dans `docs/runbooks/postgresql.md` - [x] Le message est passé à l'équipe avant l'application, personne n'est coupé sans prévenir ### Comment on le vérifie ``` Commande depuis un autre poste : nc -zv 10.105.200.41 5433 Attendu connexion refusée Puis ssh -L 5433:127.0.0.1:5433 <prénom>@10.105.200.41, puis psql sur localhost:5433 Preuve les deux sorties collées à la fermeture ``` ### Fiche ADR produite `docs/adr/0002-acces-a-la-base-par-tunnel.md`
lenaic self-assigned this 2026-09-02 08:32:42 +00:00
Member

La décision retenue ici est appliquée. Elle l'a été à l'occasion du passage de PostgreSQL sur Docker (#80, demande de fusion #81) : le service a d'abord été ouvert à 10.105.200.0/24 — l'option 2 de ce ticket — puis repassé en écoute locale une fois ce ticket relu. Je n'avais pas vu qu'il était déjà tranché, et c'est la leçon que j'en retiens : lire les tickets de décision avant de toucher au réseau, pas après.

Ce qui est en place

Critère de ce ticket État
listen_addresses = 'localhost', service redémarré fait — listen_addresses = '127.0.0.1' dans conf/postgresql.conf, conteneur recréé
Les lignes 0.0.0.0/0 de pg_hba.conf retirées fait — le fichier est réécrit : peer sur le socket, reject en TCP pour postgres, et une ligne par paire (base, rôle) sur 127.0.0.1/32 et ::1/128. Rien d'autre
La règle ufw sur 5433 retirée fait — plus aucune règle 5433, il n'y a plus rien à ouvrir

L'écoute réelle, relevée sur le serveur :

$ sudo ss -ltnp | grep 5433
LISTEN 0 200 127.0.0.1:5433 0.0.0.0:*  users:(("postgres",pid=930552,fd=6))

La fermeture est éprouvée, pas seulement écrite

Depuis un poste hors de la machine (172.26.44.15), en interrogeant le décideur pg_hba par le protocole PostgreSQL lui-même — aucun mot de passe envoyé, on ne teste que la décision d'accès :

1. En direct, sans tunnel
  enervision_prod     -> enervision_prod     INJOIGNABLE

2. Par le tunnel  ssh -N -L 15433:127.0.0.1:5433 olivier@10.105.200.41
  enervision_prod     -> enervision_prod     AUTORISE, demande SASL/SCRAM
  enervision_preprod  -> enervision_preprod  AUTORISE, demande SASL/SCRAM
  mlflow              -> mlflow              AUTORISE, demande SASL/SCRAM
  grafana             -> enervision_prod     AUTORISE, demande SASL/SCRAM
  postgres            -> postgres            REFUSE : pg_hba.conf rejette la connexion
  postgres            -> enervision_prod     REFUSE : pg_hba.conf rejette la connexion
  grafana             -> mlflow              REFUSE : aucune entree dans pg_hba.conf
  mlflow              -> enervision_prod     REFUSE : aucune entree dans pg_hba.conf
  enervision_prod     -> enervision_preprod  REFUSE : aucune entree dans pg_hba.conf

3. Tunnel referme
  enervision_prod     -> enervision_prod     INJOIGNABLE

Le premier et le dernier essai sont ceux qui prouvent quelque chose : la fermeture tient, et elle ne dépend pas de ce qu'on lit dans ufw status.

Deux choses utiles pour la suite

Le tunnel ne demande aucun entretien côté serveur, et c'est mieux que prévu. Une connexion tunnelée débouche sur 127.0.0.1 : elle correspond aux lignes déjà présentes dans pg_hba.conf. Il n'a donc pas fallu élargir le fichier pour accueillir le tunnel, et un nouveau poste ne demandera aucune modification — ce qui confirme l'argument « aucune liste à maintenir » de ce ticket, y compris côté pg_hba.conf et pas seulement côté ufw.

La manœuvre est au §2 de docs/runbooks/postgresql.md, avec le geste en ligne de commande et le réglage de l'onglet SSH pour DBeaver, pgAdmin et DataGrip.

Un avertissement qui dépasse ce ticket. En essayant l'option 2, j'ai constaté qu'un port publié par Docker échappe entièrement à UFW sur cette machine : dans la chaîne FORWARD, DOCKER-USER est consultée en position 1 et les chaînes d'UFW seulement en position 3, et DOCKER-USER est vide. Une règle ufw allow from <réseau> sur un service en réseau bridge n'a donc aucun effet, tout en affichant le bon masque. Le port 5433 était bien joignable depuis un poste tiers alors qu'ufw status annonçait 10.105.200.0/24.

Autrement dit : si l'option 2 avait été retenue, elle aurait été plus permissive que ce qu'elle prétendait, et personne ne l'aurait vu en lisant ufw status. C'est un argument de plus pour l'option 1, et surtout un point à trancher avant MinIO, MLflow et la supervision — soit rester en réseau hôte comme la forge et la base, soit poser des règles dans DOCKER-USER. C'est suivi comme écart n°14 dans docs/POSTGRESQL.md.

Ce ticket peut être fermé à la fusion de #81. Ses trois critères sont servis, et la référence Closes est portée par #80 pour ne pas fermer celui-ci avant relecture.

La décision retenue ici est **appliquée**. Elle l'a été à l'occasion du passage de PostgreSQL sur Docker (#80, demande de fusion #81) : le service a d'abord été ouvert à `10.105.200.0/24` — l'option 2 de ce ticket — puis **repassé en écoute locale** une fois ce ticket relu. Je n'avais pas vu qu'il était déjà tranché, et c'est la leçon que j'en retiens : lire les tickets de décision avant de toucher au réseau, pas après. ### Ce qui est en place | Critère de ce ticket | État | |---|---| | `listen_addresses = 'localhost'`, service redémarré | fait — `listen_addresses = '127.0.0.1'` dans `conf/postgresql.conf`, conteneur recréé | | Les lignes `0.0.0.0/0` de `pg_hba.conf` retirées | fait — le fichier est réécrit : `peer` sur le socket, `reject` en TCP pour `postgres`, et une ligne par paire (base, rôle) sur `127.0.0.1/32` et `::1/128`. Rien d'autre | | La règle `ufw` sur 5433 retirée | fait — plus aucune règle 5433, il n'y a plus rien à ouvrir | L'écoute réelle, relevée sur le serveur : ``` $ sudo ss -ltnp | grep 5433 LISTEN 0 200 127.0.0.1:5433 0.0.0.0:* users:(("postgres",pid=930552,fd=6)) ``` ### La fermeture est éprouvée, pas seulement écrite Depuis un poste hors de la machine (`172.26.44.15`), en interrogeant le décideur `pg_hba` par le protocole PostgreSQL lui-même — aucun mot de passe envoyé, on ne teste que la décision d'accès : ``` 1. En direct, sans tunnel enervision_prod -> enervision_prod INJOIGNABLE 2. Par le tunnel ssh -N -L 15433:127.0.0.1:5433 olivier@10.105.200.41 enervision_prod -> enervision_prod AUTORISE, demande SASL/SCRAM enervision_preprod -> enervision_preprod AUTORISE, demande SASL/SCRAM mlflow -> mlflow AUTORISE, demande SASL/SCRAM grafana -> enervision_prod AUTORISE, demande SASL/SCRAM postgres -> postgres REFUSE : pg_hba.conf rejette la connexion postgres -> enervision_prod REFUSE : pg_hba.conf rejette la connexion grafana -> mlflow REFUSE : aucune entree dans pg_hba.conf mlflow -> enervision_prod REFUSE : aucune entree dans pg_hba.conf enervision_prod -> enervision_preprod REFUSE : aucune entree dans pg_hba.conf 3. Tunnel referme enervision_prod -> enervision_prod INJOIGNABLE ``` Le premier et le dernier essai sont ceux qui prouvent quelque chose : la fermeture tient, et elle ne dépend pas de ce qu'on lit dans `ufw status`. ### Deux choses utiles pour la suite **Le tunnel ne demande aucun entretien côté serveur, et c'est mieux que prévu.** Une connexion tunnelée débouche sur `127.0.0.1` : elle correspond aux lignes déjà présentes dans `pg_hba.conf`. Il n'a donc pas fallu élargir le fichier pour accueillir le tunnel, et **un nouveau poste ne demandera aucune modification** — ce qui confirme l'argument « aucune liste à maintenir » de ce ticket, y compris côté `pg_hba.conf` et pas seulement côté `ufw`. La manœuvre est au §2 de `docs/runbooks/postgresql.md`, avec le geste en ligne de commande et le réglage de l'onglet SSH pour DBeaver, pgAdmin et DataGrip. **Un avertissement qui dépasse ce ticket.** En essayant l'option 2, j'ai constaté qu'**un port publié par Docker échappe entièrement à UFW** sur cette machine : dans la chaîne `FORWARD`, `DOCKER-USER` est consultée en position 1 et les chaînes d'UFW seulement en position 3, et `DOCKER-USER` est vide. Une règle `ufw allow from <réseau>` sur un service en réseau bridge n'a donc **aucun effet**, tout en affichant le bon masque. Le port 5433 était bien joignable depuis un poste tiers alors qu'`ufw status` annonçait `10.105.200.0/24`. Autrement dit : si l'option 2 avait été retenue, elle aurait été **plus permissive que ce qu'elle prétendait**, et personne ne l'aurait vu en lisant `ufw status`. C'est un argument de plus pour l'option 1, et surtout un point à trancher avant MinIO, MLflow et la supervision — soit rester en réseau hôte comme la forge et la base, soit poser des règles dans `DOCKER-USER`. C'est suivi comme écart n°14 dans `docs/POSTGRESQL.md`. Ce ticket peut être fermé à la fusion de #81. Ses trois critères sont servis, et la référence `Closes` est portée par #80 pour ne pas fermer celui-ci avant relecture.
olivier added spent time 2026-09-02 11:27:38 +00:00
1 hour
florian added this to the EnerVision project 2026-09-02 14:52:02 +00:00
Author
Owner

Vérifié ce soir depuis l'extérieur (VPN + SSH) : l'accès distant est fermé, la migration de PostgreSQL en conteneur (travail d'Olivier) l'a réglé structurellement.

Preuves :

  • conf/postgresql.conf : listen_addresses = '127.0.0.1', port = 5433 — la base n'écoute que la boucle locale ;
  • sur le serveur : ss -lntLISTEN 127.0.0.1:5433 uniquement ;
  • depuis l'extérieur : 10.105.200.41:5433 → fermé (5432 : rien du tout) ;
  • pg_hba.conf : superutilisateur rejeté sur le réseau, rôles applicatifs en scram-sha-256 sur loopback seulement.

Accès depuis un poste : ssh -L 5433:127.0.0.1:5433 <compte>@10.105.200.41 puis psql -h localhost -p 5433 -U <rôle> <base>.

La décision et sa justification sont tracées dans l'ADR 0003 (MR #97). Je ferme.

Vérifié ce soir depuis l'extérieur (VPN + SSH) : l'accès distant est fermé, la migration de PostgreSQL en conteneur (travail d'Olivier) l'a réglé structurellement. Preuves : - `conf/postgresql.conf` : `listen_addresses = '127.0.0.1'`, `port = 5433` — la base n'écoute que la boucle locale ; - sur le serveur : `ss -lnt` → `LISTEN 127.0.0.1:5433` uniquement ; - depuis l'extérieur : `10.105.200.41:5433` → fermé (5432 : rien du tout) ; - `pg_hba.conf` : superutilisateur rejeté sur le réseau, rôles applicatifs en scram-sha-256 sur loopback seulement. Accès depuis un poste : `ssh -L 5433:127.0.0.1:5433 <compte>@10.105.200.41` puis `psql -h localhost -p 5433 -U <rôle> <base>`. La décision et sa justification sont tracées dans l'ADR 0003 (MR #97). Je ferme.
gabriel added the due date 2026-09-04 2026-09-03 09:35:50 +00:00
gabriel removed the due date 2026-09-04 2026-09-03 09:40:36 +00:00
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Total time spent: 1 hour
olivier
1 hour
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision#65
No description provided.