Supervision multi-tenant pour MSP : séparer quarante clients sans quarante installations
Un Zabbix par client ne passe pas à l'échelle ; un Zabbix pour tout le monde fuit. Voici l'architecture qui fonctionne avec des briques open source — Prometheus par client, organisations Grafana, routage Alertmanager sur un label tenant, une seule convention de nommage et une seule procédure d'offboarding — et la facture honnête de ce que cela coûte en maintenance à dix, quarante et cent clients.
Sommaire
- Étape 0 : les cinq exigences qui rendent un MSP différent
- Étape 1 : nommage et étiquetage — la fondation que tout le monde saute
- Étape 2 : collecte — un Prometheus par client, un Thanos ou Mimir central
- Étape 3 : tableaux de bord — organisations Grafana, pas des dossiers
- Étape 4 : alertes — un seul jeu de règles, routage sur tenant
- Étape 5 : rapports clients et preuves
- Étape 6 : offboarding — un script, démontrable
- La facture
- Pièges
- Ce qu'il vous manque encore
- Comment monsys fait
- FAQ
Un MSP avec quarante clients a deux mauvaises options et une difficile. Mauvaise : quarante installations de supervision distinctes, chacune avec sa version, ses règles d'alerte et son mot de passe connu d'un seul collègue. Mauvaise aussi : une grosse installation dans laquelle le client A peut en principe voir les hôtes du client B si quelqu'un oublie un filtre. Difficile : une plateforme avec une vraie séparation — par client ses propres données, ses accès, ses alertes, ses rapports — et pourtant une seule façon de travailler. Cet article construit cette troisième option avec des briques open source, et est honnête sur les endroits où ça fait mal.
Étape 0 : les cinq exigences qui rendent un MSP différent
Avant d'installer quoi que ce soit, la liste contre laquelle chaque choix est vérifié :
| Exigence | Pourquoi | Ce que ça signifie techniquement |
|---|---|---|
| Isolation | Le client A ne doit jamais rien voir du client B, même par accident | Séparation au niveau des données, pas seulement dans l'interface |
| Accès par client | Le responsable IT du client A veut regarder lui-même | Comptes en lecture seule par client, sans que vous gériez une installation par client |
| Une seule façon de travailler | Votre équipe doit pouvoir reprendre n'importe quel client | Les mêmes règles d'alerte, le même nommage, les mêmes runbooks — partout |
| Preuve par client | Les clients NIS2 demandent des rapports ; vous êtes leur fournisseur (art. 21 §2 d) | Exportable, par client, daté |
| Offboarding | Les contrats se terminent ; le RGPD exige qu'alors les données disparaissent | Une procédure qui supprime tout du client X, de façon démontrable |
Étape 1 : nommage et étiquetage — la fondation que tout le monde saute
Tout ce qui suit tient à un label : le client. Choisissez le format maintenant et ne vous en écartez jamais.
# Chaque hôte, chaque métrique, chaque alerte reçoit ces labels :
tenant: acme # code client court et stable ; jamais le nom de l'entreprise (ça change)
env: prod # prod | staging | dev
role: web # web | db | app | edge | backup
site: brussels-dc1 # ou région cloud
Sur l'hôte lui-même, le code client est à un seul endroit, et chaque outil le lit là :
sudo tee /etc/monitoring-identity >/dev/null <<'EOF'
TENANT=acme
ENV=prod
ROLE=web
SITE=brussels-dc1
EOF
# Le nom d'hôte suit la convention : <tenant>-<env>-<role>-<nn>
hostnamectl set-hostname acme-prod-web-01
Ça a l'air bureaucratique. Mais c'est la différence entre {tenant="acme"} dans chaque requête et un tableur « quelle IP appartient à qui ».
Étape 2 : collecte — un Prometheus par client, un Thanos ou Mimir central
La séparation dure la plus simple : chaque client a son propre Prometheus (sur une petite VM chez vous, ou sur site chez le client), et ces Prometheus écrivent vers un stockage central unique avec le label tenant comme clé.
# /etc/prometheus/prometheus.yml sur le Prometheus du client acme
global:
external_labels:
tenant: acme # accroché à CHAQUE métrique avant qu'elle ne quitte le bâtiment
scrape_configs:
- job_name: node
file_sd_configs:
- files: ['/etc/prometheus/targets/*.yml'] # hôtes d'acme, gérés via Ansible
remote_write:
- url: https://metrics.msp.example.be/api/v1/push
basic_auth:
username: acme
password_file: /etc/prometheus/remote-write.pass
headers:
X-Scope-OrgID: acme # en-tête tenant Mimir/Cortex ; Thanos utilise l'external_label
Au centre tourne Grafana Mimir (ou Thanos Receive) avec le multi-tenant activé : le X-Scope-OrgID détermine dans quel ensemble de blocs physiquement séparé les données atterrissent. Une requête sans en-tête tenant n'obtient rien. C'est de l'isolation au niveau des données, pas de l'interface.
Le prix : une VM Prometheus par client (1 vCPU, 2 Go suffisent jusqu'à ~50 hôtes), plus un cluster Mimir central que vous devez comprendre. À dix clients, c'est un après-midi par mois ; à quarante, un demi-ETP.
Étape 3 : tableaux de bord — organisations Grafana, pas des dossiers
Grafana connaît les équipes et les dossiers (séparation dans l'interface) et les organisations (séparation des sources de données, utilisateurs et tableaux de bord). Pour l'accès client, vous utilisez les organisations. Un utilisateur de l'org « acme » ne voit aucune source de données de l'org « globex », point.
# Par client une org, avec une source de données qui transmet l'en-tête tenant
curl -s -u admin:$GRAFANA_ADMIN_PW -X POST https://grafana.msp.example.be/api/orgs \
-H 'Content-Type: application/json' -d '{"name":"acme"}'
ORG_ID=$(curl -s -u admin:$GRAFANA_ADMIN_PW https://grafana.msp.example.be/api/orgs/name/acme | jq .id)
curl -s -u admin:$GRAFANA_ADMIN_PW -X POST https://grafana.msp.example.be/api/datasources \
-H "X-Grafana-Org-Id: $ORG_ID" -H 'Content-Type: application/json' -d '{
"name":"metrics","type":"prometheus","url":"https://metrics.msp.example.be/prometheus",
"access":"proxy","jsonData":{"httpHeaderName1":"X-Scope-OrgID"},"secureJsonData":{"httpHeaderValue1":"acme"}}'
Les tableaux de bord eux-mêmes, vous les gérez comme du code (JSON dans git, provisioning par org) pour que chaque client reçoive le même tableau « Fleet overview » et qu'un correctif atterrisse partout d'un coup. Votre propre équipe a une org « MSP » séparée avec une source de données sans filtre tenant (Mimir : X-Scope-OrgID: acme|globex|... pour les requêtes inter-tenants) — c'est le seul endroit où tout est visible ensemble, et cette org a la MFA obligatoire.
Étape 4 : alertes — un seul jeu de règles, routage sur tenant
Les règles d'alerte s'écrivent une fois et sont provisionnées sur chaque Prometheus client. Alertmanager, au centre, route sur le label tenant vers le bon canal, et vers la bonne astreinte.
# alertmanager.yml (central)
route:
receiver: msp-default
group_by: ['tenant', 'alertname']
routes:
- matchers: ['tenant="acme"']
receiver: acme
continue: true # vers le client ET vers nous
- matchers: ['tenant="globex"']
receiver: globex
continue: true
- matchers: ['severity="critical"']
receiver: msp-oncall
receivers:
- name: msp-default
webhook_configs: [{ url: 'https://ntfy.msp.example.be/all' }]
- name: msp-oncall
webhook_configs: [{ url: 'https://ntfy.msp.example.be/oncall' }]
- name: acme
email_configs: [{ to: 'it@acme.example', from: 'monitoring@msp.example.be', smarthost: 'mail.msp.example.be:587' }]
- name: globex
webhook_configs: [{ url: 'https://ntfy.msp.example.be/globex' }]
Le piège : un receiver qui pointe vers un canal partagé où se trouvent plusieurs clients. Un canal Teams avec « toutes les alertes clients » est une fuite de données en devenir. Par client son propre canal ou adresse e-mail, toujours.
Étape 5 : rapports clients et preuves
Un client NIS2 vous demande de démontrer que ses serveurs sont patchés, supervisés et sauvegardés — vous êtes son fournisseur au titre de l'art. 21 §2 (d). Ce rapport doit être par client, daté, et sans rien d'un autre client dedans.
# Mensuel par tenant : disponibilité, alertes ouvertes, hôtes, état des correctifs depuis Prometheus
TENANT=acme; MONTH=$(date -d 'last month' +%Y-%m)
Q='https://metrics.msp.example.be/prometheus/api/v1/query'
H="X-Scope-OrgID: $TENANT"
{
echo "# Rapport $TENANT — $MONTH — $(date -Is)"
echo "hosts: $(curl -s -H "$H" "$Q" --data-urlencode 'query=count(up{job="node"})' | jq -r '.data.result[0].value[1]')"
echo "uptime%: $(curl -s -H "$H" "$Q" --data-urlencode 'query=avg_over_time(up{job="node"}[30d])*100' | jq -r '.data.result[0].value[1]')"
echo "reboot-required: $(curl -s -H "$H" "$Q" --data-urlencode 'query=count(node_reboot_required == 1)' | jq -r '.data.result[0].value[1] // 0')"
echo "critical alerts (30d): $(curl -s -H "$H" "$Q" --data-urlencode 'query=count_over_time(ALERTS{severity="critical",alertstate="firing"}[30d])' | jq -r '[.data.result[].value[1]|tonumber]|add // 0')"
} > "/srv/reports/$TENANT/$MONTH.txt"
sha256sum "/srv/reports/$TENANT/$MONTH.txt" >> "/srv/reports/$TENANT/MANIFEST"
Signez le fichier manifeste comme dans le guide des preuves ; le client (ou son auditeur) peut alors vérifier que le rapport n'a pas été modifié.
Étape 6 : offboarding — un script, démontrable
Quand le contrat se termine, tout ce qui concerne ce client doit disparaître : métriques, tableaux de bord, utilisateurs, routes d'alerte, et les hôtes de votre inventaire. Écrivez le script avant d'intégrer le premier client.
#!/usr/bin/env bash
# offboard.sh <tenant> — supprime tout d'un client, journalise chaque étape
set -euo pipefail
T=$1; LOG=/srv/offboarding/$T-$(date +%F).log
log(){ echo "$(date -Is) $*" | tee -a "$LOG"; }
log "start offboarding $T"
# 1. Org Grafana (emporte tableaux de bord, sources de données et utilisateurs de l'org)
ORG=$(curl -sf -u admin:$GRAFANA_ADMIN_PW https://grafana.msp.example.be/api/orgs/name/$T | jq .id) && \
curl -sf -u admin:$GRAFANA_ADMIN_PW -X DELETE https://grafana.msp.example.be/api/orgs/$ORG >/dev/null && log "grafana org $ORG deleted"
# 2. Métriques : suppression de tenant Mimir (asynchrone ; marque tous les blocs pour suppression)
curl -sf -X POST -H "X-Scope-OrgID: $T" https://metrics.msp.example.be/compactor/delete_tenant && log "mimir tenant deletion requested"
# 3. Route Alertmanager + VM Prometheus
sed -i "/tenant=\"$T\"/,+2d" /etc/alertmanager/alertmanager.yml && systemctl reload alertmanager && log "alert route removed"
log "TODO manuel : supprimer la VM Prometheus $T-prom ; révoquer les identifiants remote-write ; hôtes hors de l'inventaire Ansible"
# 4. Rapports : conserver selon le contrat (généralement 1-3 ans), puis supprimer — planifié séparément
log "reports retained until $(date -d '+3 years' +%F) per contract"
log "done"
Le fichier de journal est la preuve que vous remettez au client : « à la date X, tout a été supprimé, sauf les rapports que nous conservons contractuellement jusqu'à Y ».
La facture
| 10 clients | 40 clients | 100 clients | |
|---|---|---|---|
| VM Prometheus | 10 | 40 | 100 |
| Maintenance (mises à jour, espace disque, déploiement des règles) | ~4 h/mois | ~20 h/mois | ~1 ETP |
| Cluster Mimir/Thanos | 1 nœud suffit | 3 nœuds, quelqu'un qui le comprend | dédié |
| Orgs Grafana + provisioning | script | script + tests | script + tests + un propriétaire |
| Risque de fuite entre tenants par erreur humaine | faible | moyen (chaque nouveau collègue) | réel |
Le modèle fonctionne. À quarante clients, il coûte une demi-personne pour continuer à fonctionner, et cette personne doit vraiment connaître Prometheus, Mimir, le provisioning Grafana et le routage Alertmanager.
Pièges
- Fuite de label via
external_labels. Oublieztenantsur un Prometheus et ces métriques atterrissent sans tenant et apparaissent — selon votre config Mimir — chez le tenant par défaut ou nulle part. Testez chaque nouveau Prometheus client avec une requête avant de le mettre en production. - Jetons d'agent partagés. Un seul basic auth
node_exporterpour tous les clients signifie qu'un hôte compromis du client A peut pousser (ou lire) des métriques du client B. Par tenant ses propres identifiants, et rotation à l'offboarding. - Un admin Grafana qui entre partout. Pratique, jusqu'au départ d'un collègue. L'org MSP avec accès inter-tenants a la MFA, son propre journal d'audit et une revue trimestrielle de qui en fait partie.
- Le nom du client dans le nom d'hôte.
acmeest un code. « Acme Industries NV » change lors d'une fusion et se retrouve alors dans 400 tableaux de bord. Les codes ne changent pas. - Oublier l'accord de sous-traitance. Vous supervisez leurs systèmes : c'est un traitement de données personnelles (IP, noms d'utilisateur dans les logs). Chaque client signe un DPA, et la procédure d'offboarding y fait référence.
Ce qu'il vous manque encore
- Tout ce qui n'est pas une métrique. Prometheus, c'est pour les chiffres. CVE par client, inventaire, qui-a-sudo, fraîcheur des sauvegardes, certificats : ce sont des pipelines séparés avec une séparation tenant séparée — et chacun est à reconstruire avec les mêmes cinq exigences.
- Intervenir. « Redémarre ce service chez le client A » est une session ssh, avec la question de qui a le droit, qui l'a fait, et si le client A peut le voir. Une trace signée de cela n'existe pas.
- La preuve que la séparation fonctionne. Un client qui demande « comment savoir que mon concurrent ne voit pas mes données ? » reçoit un schéma d'architecture. Un auditeur veut davantage.
Comment monsys fait
monsys est multi-tenant depuis la première migration : chaque table a un tenant_id, chaque requête l'épingle explicitement et la row-level security de PostgreSQL est le filet de sécurité. Un hub, quarante tenants, zéro VM supplémentaire. Par tenant : ses propres agents avec leurs propres jetons et certificats mTLS, ses propres utilisateurs avec des rôles par périmètre (viewer sur le client A, editor sur le client B), sa propre identité visuelle et son propre topic ntfy. Pour vous en tant que MSP : une vue inter-clients sur tous les tenants où vous êtes staff, un rapport de transfert par client (signé Ed25519, vérifiable hors ligne) et des rotations d'astreinte par groupe. Intervenir passe par des Emergency Action Tokens journalisés par tenant, de sorte que le client A voit dans son propre journal d'audit ce que vous avez fait sur ses hôtes — et rien du client B.
FAQ
Ne puis-je pas simplement utiliser un seul Prometheus avec un label tenant ?
Techniquement oui, mais la séparation est alors un filtre dans l'interface et dans chaque requête. Un {tenant="acme"} oublié dans une variable de tableau de bord et le client A voit le client B. Pour un MSP sous NIS2, c'est un risque inacceptable ; la séparation doit être au niveau du stockage.
Combien de clients puis-je gérer avant d'avoir besoin de cela ?
Jusqu'à cinq clients, « une petite installation par client » fonctionne bien. À partir de dix, la maintenance d'installations séparées devient plus chère qu'une configuration centrale. À partir de quarante, la configuration centrale elle-même est un demi-poste.
Que demande concrètement un client sous NIS2 à son MSP ?
De la démontrabilité : que ses systèmes sont supervisés, patchés et sauvegardés, avec des dates et sans données d'autres clients. Plus un contrat (DPA et une annexe sécurité) et une procédure d'incident : si vous voyez une intrusion chez le client A, le client A doit pouvoir notifier le CCB sous 24 heures, donc vous devez le lui signaler encore plus vite.
Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.
Fait à la main ? Laissez monsys s'en charger en continu.
Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.