« 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.
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.
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
À é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.
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.
| Critère | Martin | OGC API Features | QGIS Server | PMTiles |
|---|---|---|---|---|
| Sortie | Tuiles MVT | GeoJSON OGC | WMS / WFS | Tuiles 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ée | Nulle |
| Cache / CDN | Via proxy | Via proxy | Via proxy | Natif CDN |
| Cas type | Carto web MapLibre | Interroger des objets | Écosystème QGIS | Donné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 donnée sort, la base reste privée
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.
-- 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.
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.
# 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.
# 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) :
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.
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.
Toutes ces erreurs ont la même racine : on a confondu montrer la donnée et donner accès à la base.
Coût
- Martin et un reverse proxy déjà en place : quelques heures de mise en service
- Le service tourne sur le serveur de la base — pas d'infrastructure supplémentaire
- Le schéma de diffusion se construit une fois, puis évolue à la vue près
Risque
- Couvert par les trois garde-fous : base privée, rôle lecture seule cadré, proxy en façade
- Le seul danger réel : un raccourci qui rouvre un des trois
- À condition de ne sauter aucune des couches
Bénéfice
- Le patrimoine consultable sur une carte web, sans jamais exposer la base
- Des couches choisies, en lecture, sous authentification maîtrisée
- Une charge tenue par le cache, même en fort trafic
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.