SIG & Carto Guide

Diffuser PostGIS sur le web en sécurité : Martin, OGC API et authentification

Exposer vos couches sur une carte web ou une API métier sans jamais ouvrir la base au navigateur

Mattieu Pottier 10 min de lecture
Schéma d'une chaîne de diffusion PostGIS : navigateur, reverse proxy authentifié, Martin et base PostGIS en réseau privé

« Pour afficher ma donnée PostGIS sur le web, il suffit d'ouvrir un accès à la base. » Cette phrase défait en une ligne tout le travail de sécurisation mené pour mettre la base en production.

Diffuser PostGIS sur le web ne veut pas dire exposer la base. Cela veut dire poser un service de diffusion devant elle : il sert des couches choisies, en lecture, pendant que la base reste injoignable depuis le navigateur.

Cet article montre comment exposer vos couches sur une carte web ou une API métier — avec Martin, OGC API Features ou QGIS Server — sans rouvrir ce que vous venez de fermer.

Côté métier — ce que ça change

Diffuser, c'est rendre votre patrimoine géographique consultable sur une carte web — par les équipes terrain, les services métier, les décideurs — sans donner à personne les clés de la base.

  • Vous choisissez quelles couches sont visibles : le réseau, les parcelles, les interventions — et rien d'autre.
  • L'accès est en lecture seule : consulter ne permet jamais de modifier la donnée de référence.
  • L'accès reste maîtrisé : une carte interne derrière authentification, une API ouverte à une appli partenaire — vous décidez, par un seul point de contrôle.
  • Le risque est déplacé, pas supprimé : une diffusion mal cadrée rouvre la base. Les trois garde-fous ci-dessous existent pour fermer ce risque, sans licence propriétaire.

Diffuser n'est pas exposer

L'erreur de raccourci consiste à brancher le navigateur directement sur PostgreSQL : on rouvre le port, on donne un identifiant de connexion, et « la carte s'affiche ». Le problème, c'est que tout ce qui touche le navigateur est public — l'identifiant finit lisible dans le code de la page, et c'est toute la base qui devient atteignable.

Le bon modèle inverse la logique : la base reste privée, et un service de diffusion se place devant elle. Ce service lit la donnée côté serveur et n'expose au web que des tuiles ou des objets — jamais une connexion à la base.

service de diffusion (tile server) Logiciel intermédiaire qui lit une base géospatiale côté serveur et expose son contenu au web sous une forme consommable par un navigateur — tuiles vectorielles, GeoJSON, WMS. Le client web parle au service de diffusion ; il ne parle jamais à la base.

La différence n'est pas cosmétique : elle décide de votre surface d'attaque.

Recommandé

Un service de diffusion devant la base

  • La base n'est jamais joignable depuis le navigateur
  • Seules les couches déclarées sont servies, en lecture
  • Un seul point exposé : le service, derrière un proxy
  • L'identifiant de base ne quitte jamais le serveur
VS

À éviter

Le navigateur branché sur la base

  • L'identifiant de connexion finit en clair côté client
  • Toute la base devient atteignable, pas une couche
  • Le port de la base rouvert au monde
  • La sécurisation de la mise en production annulée

Les voies de diffusion : tuiles, API, services OGC

Trois familles d'outils servent PostGIS sur le web, plus un cas particulier pour la donnée figée. Le choix se fait sur deux axes : le format attendu côté client, et la charge que vous acceptez de faire porter à la base.

Martin génère des tuiles vectorielles à la volée depuis PostGIS. C'est la voie naturelle quand le client est une carte MapLibre : la donnée arrive découpée, légère, prête à styler côté navigateur.

MVT (Mapbox Vector Tile) Format de tuile vectorielle : la donnée géo est découpée en carrés par niveau de zoom et encodée en binaire compact. Le navigateur ne télécharge que les tuiles visibles, et applique le style côté client. C'est ce qui rend une carte web fluide même sur de gros volumes.

OGC API Features (servi par exemple par pg_featureserv) expose des objets en GeoJSON selon un standard ouvert : pertinent quand un client a besoin d'interroger des entités, pas d'afficher un fond cartographique. QGIS Server publie du WMS/WFS, le bon choix quand vous êtes déjà dans un écosystème QGIS. Enfin, PMTiles sert un jeu de tuiles figé dans un fichier unique — zéro serveur, zéro charge base, mais une donnée non dynamique.

Quatre voies pour diffuser PostGIS sur le web
CritèreMartinOGC API FeaturesQGIS ServerPMTiles
SortieTuiles MVTGeoJSON OGCWMS / WFSTuiles figées
Donnée dynamique✅ À la volée✅ À la volée✅ À la volée❌ Jeu figé
Charge sur la baseÉlevée — 1 requête/tuileÉlevéeÉlevéeNulle
Cache / CDNVia proxyVia proxyVia proxyNatif CDN
Cas typeCarto web MapLibreInterroger des objetsÉcosystème QGISDonnée stable, fort trafic

La règle de tri est simple : carte web dynamique sur MapLibre → Martin ; besoin d'interroger des objets selon un standard → OGC API Features ; donnée qui ne bouge presque pas mais très consultée → PMTiles sur CDN. Le tour complet des protocoles d'exposition — WMS, OGC API, tuiles, CDN — fait l'objet d'un article dédié. La suite prend ici Martin comme fil conducteur, parce que c'est le cas le plus courant et celui qui pose le plus clairement la question de la sécurité.

Sécuriser la diffusion : la base ne bouge pas, le service est cadré, le proxy filtre

Un service de diffusion ouvre une nouvelle porte sur la donnée. Trois garde-fous, indépendants, suffisent à la tenir — et ils se lisent sur une seule chaîne.

La base reste fermée : le service tourne à côté d'elle

Le point qui réconcilie diffusion et sécurité tient en une phrase : Martin tourne sur le même serveur que la base et s'y connecte par le réseau privé (localhost ou réseau Docker interne). Le port PostgreSQL n'est jamais publié sur Internet — l'acquis de la mise en production reste intact.

Concrètement, ni la base ni Martin n'exposent de port public. Le seul service joignable depuis l'extérieur est le reverse proxy. Tout le reste vit derrière lui.

Un rôle de service en lecture seule, sur un périmètre choisi

Le service de diffusion se connecte avec un compte dédié, en lecture seule, distinct des comptes humains posés à l'étape précédente. Jamais un compte éditeur, jamais un compte nominatif.

Mais attention au faux ami : lecture seule ne veut pas dire périmètre restreint. Un rôle avec un SELECT global peut lire toutes les tables de la base. Pour ne publier que le réseau et rien d'autre, il faut un mécanisme explicite : un schéma de diffusion dédié, qui ne contient que des vues des couches à exposer.

SQL
-- Compte de service dédié à la diffusion : lecture seule, non nominatif
CREATE ROLE tiles_ro LOGIN PASSWORD 'remplacez_par_un_vrai_secret';

-- Schéma de diffusion : on n'y place QUE les couches destinées au web
CREATE SCHEMA diffusion;

-- Une vue expose la couche réseau en ne gardant que les colonnes utiles
CREATE VIEW diffusion.reseau_canalisations AS
SELECT id, type, diametre, geom
FROM reseau.canalisations;

-- Le service ne voit QUE le schéma de diffusion, et seulement en lecture
GRANT USAGE  ON SCHEMA diffusion TO tiles_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA diffusion TO tiles_ro;

-- tiles_ro n'a aucun droit sur le schéma métier (reseau, public) :
-- la vue suffit, car elle s'exécute avec les droits de son propriétaire.

Ce dernier point est le cœur du cloisonnement. Par défaut, une vue PostgreSQL s'exécute avec les droits de son créateur, pas de celui qui l'interroge.

Résultat : tiles_ro lit diffusion.reseau_canalisations sans avoir le moindre accès direct à reseau.canalisations. Même si la configuration du service déclarait une source de trop, le rôle ne verrait que ce que le schéma de diffusion contient.

L'erreur qui rouvre tout

Donner au service de diffusion un compte avec des droits d'écriture — ou pire, le compte administrateur. Le service est exposé au web par nature : une faille sur le proxy ou sur le service devient alors une écriture directe dans votre base de référence. Le compte de diffusion ne doit posséder que SELECT, et uniquement sur le schéma de diffusion.

Le reverse proxy, seule porte exposée

Devant le service, un reverse proxy (Traefik, Nginx) concentre tout ce qui touche l'extérieur : le HTTPS (certificat Let's Encrypt), l'authentification, le CORS restreint à vos origines, et le rate limiting. C'est lui, et lui seul, qui est publié sur Internet.

Cas pratique : exposer une couche réseau en tuiles vectorielles

On reprend la couche réseau de la mise en production. Objectif : la servir en tuiles, derrière un proxy authentifié, et l'afficher dans une carte MapLibre — sans que le navigateur touche jamais la base.

D'abord, la configuration de Martin. On déclare explicitement la seule source à publier ; rien d'autre ne sera exposé. Le mot de passe n'apparaît pas dans le fichier — il est passé par la variable d'environnement PGPASSWORD.

YAML
# config.yaml — Martin ne publie QUE ce qui est déclaré ici
postgres:
  connection_string: postgresql://tiles_ro@db:5432/gis_db
  tables:
    reseau_canalisations:
      schema: diffusion
      table: reseau_canalisations
      srid: 2154
      geometry_column: geom
      id_column: id
      minzoom: 0
      maxzoom: 22
      geometry_type: GEOMETRY
      properties:
        type: text
        diametre: int4

Ensuite, le service et son proxy. Martin n'expose aucun port publié : il vit sur le réseau interne backend, et n'est joignable que par Traefik. Le router applique trois middlewares — rate limit, CORS restreint, authentification — et le certificat TLS est géré automatiquement.

YAML
# docker-compose.yml (extrait) — Martin derrière Traefik, jamais exposé en direct
services:
  martin:
    image: ghcr.io/maplibre/martin:latest
    command: --config /config/config.yaml
    environment:
      - PGPASSWORD=${TILES_RO_PASSWORD}
    volumes:
      - ./martin/config.yaml:/config/config.yaml:ro
    networks:
      - backend   # interne : Martin n'a aucun port publié
      - web       # pour être joignable par Traefik uniquement
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.tuiles.rule=Host(`tuiles.exemple.fr`)"
      - "traefik.http.routers.tuiles.entrypoints=websecure"
      - "traefik.http.routers.tuiles.tls.certresolver=letsencrypt"
      - "traefik.http.routers.tuiles.middlewares=tuiles-rl,tuiles-cors,tuiles-auth"
      - "traefik.http.services.tuiles.loadbalancer.server.port=3000"
      # 1. Rate limiting : 50 req/s par IP, pics à 100
      - "traefik.http.middlewares.tuiles-rl.ratelimit.average=50"
      - "traefik.http.middlewares.tuiles-rl.ratelimit.burst=100"
      # 2. CORS : seule votre carte est autorisée à consommer les tuiles
      - "traefik.http.middlewares.tuiles-cors.headers.accesscontrolalloworiginlist=https://carte.exemple.fr"
      - "traefik.http.middlewares.tuiles-cors.headers.accesscontrolallowmethods=GET,OPTIONS"
      - "traefik.http.middlewares.tuiles-cors.headers.accesscontrolallowheaders=Authorization"
      # 3. Authentification (Basic ici — voir ci-dessous pour un token)
      - "traefik.http.middlewares.tuiles-auth.basicauth.users=carto:$$apr1$$XXXXXXXX$$REMPLACEZ_PAR_LE_HASH"

Le hash Basic se génère avec htpasswd (les $ sont doublés pour Docker Compose) :

Bash
htpasswd -nb carto 'votre_mot_de_passe' | sed -e 's/\$/\$\$/g'

L'authentification Basic convient à un accès humain à une carte interne : le navigateur demande l'identifiant une fois. Pour une application front qui consomme les tuiles, on ne veut pas de fenêtre de connexion : on protège par token. Le middleware basicauth est alors remplacé par un forwardAuth qui valide un jeton (via oauth2-proxy ou un petit service de validation maison) — et c'est là que MapLibre entre en jeu.

Côté carte, MapLibre joint le token à chaque requête de tuile grâce à transformRequest. La base n'est jamais sollicitée par le navigateur : seul le proxy l'est, et seulement s'il accepte le jeton.

JavaScript
const map = new maplibregl.Map({
  container: 'map',
  style: 'https://votre-fond-de-carte/style.json',
  center: [-52.33, 4.92],
  zoom: 11,
  transformRequest: (url, resourceType) => {
    // Le token n'accompagne QUE les requêtes vers notre service de tuiles
    if (resourceType === 'Tile' && url.startsWith('https://tuiles.exemple.fr')) {
      return {
        url,
        headers: { Authorization: 'Bearer ' + monToken }
      };
    }
  }
});

map.on('load', () => {
  map.addSource('reseau', {
    type: 'vector',
    url: 'https://tuiles.exemple.fr/reseau_canalisations' // TileJSON servi par Martin
  });
  map.addLayer({
    id: 'canalisations',
    type: 'line',
    source: 'reseau',
    'source-layer': 'reseau_canalisations', // = nom de la source Martin
    paint: { 'line-color': '#0d5c63', 'line-width': 1.5 }
  });
});

Ce qui se passe côté front au-delà de cette source — fond de carte, styles, interactions — relève du moteur de rendu et sort du périmètre ici ; il est traité dans le comparatif des moteurs cartographiques et le choix du fond de carte.

Les erreurs qui rouvrent ce qu'on vient de fermer

La plupart des incidents ne viennent pas du service de diffusion lui-même, mais d'un raccourci qui annule un des trois garde-fous.

🔓

Le 5432 « juste pour tester »

Rouvrir le port de la base au monde annule toute la sécurisation de la mise en production. Le service de diffusion suffit à montrer la donnée.

✍️

Un compte d'écriture pour le service

Une faille sur le proxy devient une écriture en base. Le compte de diffusion ne doit avoir que SELECT, sur le seul schéma de diffusion.

🐌

Des tuiles dynamiques sans cache

Chaque tuile déclenche une requête PostGIS. Sans cache devant, c'est la base qui encaisse tout le trafic — jusqu'à la facture ou la saturation.

🌐

Un CORS en wildcard

Une liste d'origines à « * » laisse n'importe quel site consommer vos tuiles. Listez vos origines, jamais l'étoile.

Toutes ces erreurs ont la même racine : on a confondu montrer la donnée et donner accès à la base.

Ce qu'il faut retenir

Diffuser PostGIS sur le web ne consiste jamais à ouvrir la base : on place un service de diffusion devant elle, et on ne sert que des couches choisies, en lecture. Le triptyque tient en trois garde-fous indépendants — le service sur le réseau privé (la base reste fermée), un rôle de service en lecture seule sur un périmètre cadré (un schéma de diffusion, pas un SELECT global), et un reverse proxy en façade (HTTPS, authentification, CORS restreint, rate limiting et cache).

Posées dans cet ordre, ces trois couches permettent d'exposer une carte web ou une API métier sans rien rouvrir de ce que la mise en production avait fermé.

Mise en diffusion sécurisée

  • Service de diffusion sur le réseau privé — aucun port publié, seul le proxy l'est
  • Rôle de service dédié, en lecture seule, distinct des comptes humains
  • Périmètre limité : schéma ou vues de diffusion, jamais un SELECT global
  • Reverse proxy en façade : HTTPS, authentification, CORS restreint, rate limit
  • Cache devant les tuiles dynamiques (proxy ou CDN)
  • Le port de la base toujours fermé au monde — vérifié après mise en service

Le dernier article de cette série quitte la diffusion pour entrer dans la base elle-même : tout ce que PostGIS sait faire en pur SQL — distances, zones tampon, intersections, agrégats spatiaux — sans jamais sortir de la requête.