Mathieu Bernard Réserver 30 min

Prestataire injoignable : le guide technique pour reprendre la main

Quand le prestataire d’une application sur mesure ne répond plus, chercher tout de suite un repreneur est prématuré. Il faut d’abord sécuriser le domaine, le serveur, le code, les données et les accès, puis faire redémarrer l’application ailleurs.

Le scénario est toujours le même. Une application métier sur mesure (un ERP maison, un outil de devis, un extranet client) tourne depuis des années. Le prestataire qui l’a développée ne répond plus, ou répond en trois semaines. Et un jour, l’hébergeur annonce la fin d’une version de PHP, un certificat expire, ou un envoi d’e-mails tombe en panne sans que personne ne sache pourquoi.

Le premier réflexe est de chercher tout de suite un nouveau prestataire. C’est trop tôt. Sans le code, les données et les accès, aucun repreneur ne peut chiffrer sérieusement : il ne peut que deviner, et quand on devine, on facture une reconstruction complète.

Voici la méthode que je suis quand je reprends une application dans cet état. Elle est volontairement technique. L’ordre compte : d’abord sécuriser ce qui peut être perdu, ensuite comprendre, et seulement à la fin décider. Les commandes et les sorties ci-dessous sont des exemples génériques (exemple.fr, adresses IP de documentation), à adapter à votre cas.

1. Le nom de domaine : la clé de tout le reste

Si vous perdez le domaine, vous perdez le site, les e-mails, les liens de réinitialisation de mot de passe et la confiance des clients. C’est donc la première vérification, avant même de toucher au serveur.

$ whois exemple.fr
domain:      exemple.fr
status:      ACTIVE
holder-c:    ABC123-FRNIC
registrar:   UN-REGISTRAR SAS
Expiry Date: 2027-03-14T10:22:31Z
nserver:     ns1.hebergeur-exemple.net
nserver:     ns2.hebergeur-exemple.net

Trois informations m’intéressent :

  • Le titulaire (holder). Il doit s’agir de votre entreprise, pas du prestataire ni d’un de ses salariés. Pour beaucoup d’extensions, les coordonnées sont masquées par le RGPD : c’est alors dans l’espace client du registrar qu’on vérifie.
  • Le registrar : le bureau d’enregistrement chez qui le domaine est géré, et donc le compte auquel il faut accéder.
  • La date d’expiration. Un domaine qui expire dans un mois avec un prestataire injoignable, c’est l’urgence numéro un.

Si le domaine est dans le compte du prestataire, il faut obtenir le code de transfert (AuthInfo, ou code EPP) et le faire passer sur un compte à votre nom. Le transfert lui-même prend de quelques heures à quelques jours selon l’extension. Si le prestataire ne répond pas, le registrar a généralement une procédure de changement de titulaire sur justificatifs.

Avant tout transfert, je relève l’intégralité de la zone DNS. Un transfert mal préparé, c’est une zone qui disparaît et des enregistrements MX ou SPF oubliés :

$ for t in A AAAA MX TXT CNAME NS CAA; do dig +noall +answer exemple.fr $t; done
exemple.fr.      3600  IN  A      203.0.113.42
exemple.fr.      3600  IN  MX     10 mx1.fournisseur-mail.example.
exemple.fr.      3600  IN  TXT    "v=spf1 include:_spf.fournisseur-mail.example ~all"
exemple.fr.     86400  IN  NS     ns1.hebergeur-exemple.net.

$ dig +noall +answer www.exemple.fr app.exemple.fr _dmarc.exemple.fr TXT

Le dig sur le domaine nu ne montre pas les sous-domaines : il faut les deviner (www, app, api, mail, admin, les sélecteurs DKIM), les chercher dans le code et dans les journaux de certificats (crt.sh liste tous les certificats émis pour un domaine, donc souvent tous les sous-domaines publics). La colonne du milieu, le TTL, compte aussi : à 86 400 secondes, un changement mettra jusqu’à 24 heures à se propager. Avant une bascule, je baisse le TTL à 300 au moins un TTL complet à l’avance.

2. Identifier l’hébergement

On sait maintenant vers quelle IP pointe le domaine. Reste à savoir ce qu’il y a derrière.

$ dig +short app.exemple.fr
203.0.113.42

$ dig +short -x 203.0.113.42
vps-1234.hebergeur-exemple.net.

$ whois 203.0.113.42 | grep -iE 'netname|org-name|country'
netname:   HEBERGEUR-EXEMPLE-VPS
org-name:  Hebergeur Exemple SAS
country:   FR

Le reverse DNS et le whois de l’IP donnent l’hébergeur, et souvent le type d’offre (VPS, mutualisé, serveur dédié). Les en-têtes HTTP complètent le tableau :

$ curl -sI https://app.exemple.fr
HTTP/2 200
server: nginx/1.18.0
x-powered-by: PHP/7.4.33
set-cookie: PHPSESSID=...; path=/; HttpOnly

Ici, on apprend déjà beaucoup : nginx, PHP 7.4 (hors support de sécurité depuis fin 2022), sessions PHP natives. Si l’IP appartient à un CDN ou à un proxy (Cloudflare par exemple), l’IP réelle du serveur est ailleurs : il faut la chercher dans la console du CDN, ou dans les en-têtes des e-mails envoyés par l’application.

Le point non technique, mais décisif : qui est titulaire du contrat d’hébergement ? Si c’est le prestataire, il peut résilier, ou simplement cesser de payer. Le serveur s’éteint alors sans préavis. Il faut obtenir un accès administrateur à votre nom, et idéalement migrer le contrat.

3. Récupérer le code, les données et la configuration

Le cas idéal est un dépôt git à jour, sur GitHub ou GitLab, avec un accès qu’on peut vous transférer. Mais je ne fais jamais confiance au dépôt seul : la production a souvent divergé (correctifs faits en direct sur le serveur, fichiers jamais commités). Ce qui tourne en production fait foi.

Avec un accès SSH, je fais une copie complète, sans rien modifier sur place :

# Copie de l'application, en conservant droits et dates
$ rsync -avz --numeric-ids user@203.0.113.42:/var/www/app/ ./backup/app/

# Si rsync n'est pas disponible côté serveur
$ ssh user@203.0.113.42 "tar czf - -C /var/www app" > backup/app.tar.gz

# Est-ce que la prod correspond au dépôt ?
$ cd backup/app && git status --short | head
 M src/Service/InvoiceService.php
?? public/patch_urgent.php

Ce genre de sortie est fréquent, et c’est précisément ce qu’on veut savoir avant de redéployer quoi que ce soit depuis le dépôt.

Ensuite, la base de données. Je dumpe avec les options qui garantissent un export cohérent et complet, procédures et triggers compris :

# MySQL / MariaDB
$ mysqldump --single-transaction --routines --triggers --events \
    -u app -p appdb | gzip > backup/appdb-$(date +%F).sql.gz

# PostgreSQL
$ pg_dump -Fc -U app -d appdb -f backup/appdb-$(date +%F).dump

Puis tout ce qui n’est ni dans le code ni dans la base, et qu’on oublie systématiquement :

  • Les fichiers uploadés : factures PDF, pièces jointes, photos. Souvent dans public/uploads, storage/ ou var/, parfois sur un stockage externe (S3 ou équivalent).
  • Les tâches planifiées : crontab -l pour l’utilisateur applicatif, mais aussi sudo crontab -l, /etc/cron.d/ et systemctl list-timers.
  • La configuration : .env, .env.local, config/parameters.yml, wp-config.php, la configuration du serveur web (/etc/nginx/sites-enabled/), celle de PHP-FPM, et les variables d’environnement du process.
  • Les certificats TLS : savoir qui les émet et quand ils expirent.
$ crontab -l
*/5 * * * * php /var/www/app/bin/console app:sync-orders
0 2 * * *   php /var/www/app/bin/console app:send-reminders
30 3 * * *  /home/user/scripts/export_compta.sh

$ sudo cat /proc/$(pgrep -f php-fpm | head -1)/environ | tr '\0' '\n' | grep -v PATH

$ echo | openssl s_client -connect app.exemple.fr:443 -servername app.exemple.fr 2>/dev/null \
    | openssl x509 -noout -issuer -enddate
issuer=C = US, O = Let's Encrypt, CN = R11
notAfter=Jan 12 08:14:03 2027 GMT

Un script comme export_compta.sh, hors du dépôt, dans le répertoire personnel d’un utilisateur, c’est exactement le traitement de nuit que personne ne connaît et dont l’absence se remarquera à la clôture du mois. Je le copie avec le reste.

Tout cela part dans un stockage qui vous appartient, hors du serveur d’origine, et chiffré : ces fichiers contiennent des mots de passe et des données personnelles.

4. Reconstruire un environnement local qui démarre

Une sauvegarde qu’on n’a jamais restaurée n’est qu’une hypothèse. L’étape suivante consiste à faire démarrer l’application ailleurs que sur son serveur d’origine. C’est là qu’on découvre ce qui manque vraiment.

Je relève d’abord les versions exactes en production :

$ php -v
PHP 7.4.33 (cli) (built: Nov  8 2022 ...)
$ php -m > backup/php-modules.txt
$ mysql --version
mysql  Ver 15.1 Distrib 10.3.39-MariaDB
$ node -v
v14.21.3

Ensuite je reproduis ces versions à l’identique dans Docker. L’objectif n’est pas encore de moderniser, mais de reproduire :

# docker-compose.yml
services:
  app:
    image: php:7.4-fpm
    volumes: ["./app:/var/www/app"]
  web:
    image: nginx:1.18
    ports: ["8080:80"]
  db:
    image: mariadb:10.3
    environment:
      MARIADB_DATABASE: appdb
      MARIADB_ROOT_PASSWORD: local-only

Pour les dépendances, ce sont les fichiers de verrouillage qui font foi : composer.lock et package-lock.json (ou yarn.lock). J’installe exactement ce qu’ils décrivent, sans mise à jour :

$ composer install --no-dev   # respecte composer.lock, contrairement à composer update
$ npm ci                      # respecte package-lock.json

Si le fichier de verrouillage est absent, c’est un signal fort : personne ne peut garantir que les versions installées aujourd’hui seront celles de la production. Dans ce cas, je pars du répertoire vendor/ copié depuis le serveur, qui est la seule référence fiable.

Les pièges classiques à ce stade : une extension PHP manquante (gd, intl, imagick), un sql_mode MySQL différent qui casse des requêtes, un fuseau horaire implicite, des chemins absolus codés en dur, un dépôt Composer privé qui n’existe plus.

5. Vérifier que la restauration fonctionne vraiment

L’application démarre : ce n’est pas suffisant. Je vérifie que les données sont complètes et que les parcours principaux fonctionnent.

$ gunzip -c backup/appdb-2026-10-20.sql.gz | docker compose exec -T db mysql -uroot -plocal-only appdb

# Comparer le nombre de lignes par table entre prod et local
$ mysql -N -e "SELECT table_name, table_rows FROM information_schema.tables
               WHERE table_schema='appdb' ORDER BY table_name" appdb

Attention, table_rows est une estimation sous InnoDB. Pour les tables critiques (commandes, factures, clients), je fais un vrai SELECT COUNT(*) des deux côtés. Ensuite je me connecte, j’ouvre une fiche, je génère un document, et je vérifie que les fichiers uploadés s’affichent.

Un point de sécurité indispensable : un environnement local branché sur les vrais identifiants SMTP ou de paiement peut envoyer de vrais e-mails à de vrais clients. Avant de lancer quoi que ce soit, je remplace les identifiants de production dans le .env local, je redirige les e-mails vers un capteur local (Mailpit, par exemple), et je ne lance pas les crons.

6. Inventorier les dépendances et leur fin de support

Maintenant que le code tourne, on peut mesurer l’écart avec un socle maintenu.

$ composer show --direct
symfony/framework-bundle  v4.4.49
doctrine/orm              2.7.5
twig/twig                 v2.15.3

$ composer outdated --direct
$ composer audit
Found 4 security vulnerability advisories affecting 3 packages

$ npm outdated
$ npm audit --omit=dev

Je croise ensuite chaque brique avec endoflife.date, qui donne les dates de fin de support des langages, frameworks et bases de données. Le site propose aussi une API :

$ curl -s https://endoflife.date/api/php/7.4.json
{"releaseDate":"2019-11-28","support":"2021-11-28","eol":"2022-11-28", ...}

J’en tire un tableau simple, qui sert de base à la discussion :

BriqueVersion en prodFin de supportVersion cible
PHP7.4Dépassée8.3 ou plus
Symfony4.4DépasséeLTS actuelle
MariaDB10.3DépasséeLTS actuelle
Node (build front)14DépasséeLTS actuelle

Les versions de ce tableau sont un exemple, mais la configuration est typique d’une application laissée sans maintenance pendant quelques années.

7. Les comptes tiers : ce qui casse sans prévenir

Une application métier dépend presque toujours de services extérieurs, et chacun est un compte avec un titulaire, un moyen de paiement et des clés d’API. Je les trouve en cherchant dans le code et la configuration :

$ grep -rEn "MAILER_DSN|SMTP|STRIPE|PAYPAL|GOOGLE_MAPS|MAPBOX|AWS_|S3_|SENTRY" \
    app/.env* app/config/ | cut -c1-120

Pour chaque service, je note :

  • L’envoi d’e-mails (SMTP, service transactionnel) : qui détient le compte, et si le domaine expéditeur est bien authentifié (SPF, DKIM, DMARC relevés à l’étape 1).
  • Le paiement : le compte marchand doit être au nom de l’entreprise. Les webhooks pointent vers une URL précise de l’application : il faudra la mettre à jour lors d’une migration.
  • La cartographie et les API à la consommation : une clé payée par la carte bancaire du prestataire cessera de fonctionner le jour où il arrêtera de payer.
  • Le stockage externe : un bucket dans le compte cloud du prestataire, c’est une partie de vos données qui ne vous appartient pas encore.

Pour chaque compte, l’objectif est le même : un accès propriétaire à votre nom, puis une rotation des clés, puisque l’ancien prestataire les connaît.

8. Poser un filet avant de toucher au code

Avant la moindre modification, je mets en place trois protections. Elles prennent peu de temps et changent tout le reste du projet.

Une sauvegarde automatique, hors du serveur. Base de données et fichiers uploadés, chaque nuit, avec une rétention et une restauration testée :

# crontab de l'utilisateur de sauvegarde
15 1 * * * mysqldump --single-transaction appdb | gzip > /backup/appdb-$(date +\%F).sql.gz \
           && restic backup /backup /var/www/app/public/uploads --tag nightly
30 1 * * 0 restic forget --keep-daily 14 --keep-weekly 8 --prune

Une supervision minimale. Un contrôle externe toutes les minutes sur une page qui touche la base, une alerte sur l’expiration du certificat et du domaine. N’importe quel service de monitoring d’uptime fait l’affaire. Les crons importants signalent leur exécution (un ping en fin de script) pour qu’on sache quand ils ne tournent plus.

Des tests de fumée sur les parcours critiques. Pas une suite de tests complète, juste de quoi savoir en deux minutes si l’essentiel fonctionne après un déploiement :

#!/usr/bin/env bash
set -e
BASE=https://app.exemple.fr
check() { code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE$1"); \
          [ "$code" = "$2" ] && echo "OK  $1" || { echo "KO  $1 ($code)"; exit 1; }; }
check /login 200
check /api/health 200
check /admin 302        # doit rediriger vers le login

Ensuite, j’ajoute progressivement des scénarios de navigateur (connexion, création d’un devis, génération d’une facture) avec un outil comme Playwright. Ce filet est ce qui permet ensuite de mettre à jour PHP ou une dépendance sans travailler à l’aveugle.

9. Remise à niveau ou reconstruction : des critères explicites

C’est seulement maintenant, avec le code, les données, un environnement qui démarre et un inventaire, qu’on peut trancher. Et la réponse n’est pas automatiquement « on refait tout ». Une application qui sert le métier depuis des années contient des années de règles métier, souvent non documentées, qu’une reconstruction devra redécouvrir une par une.

Les critères que j’utilise :

CritèrePlutôt remise à niveauPlutôt reconstruction
FrameworkFramework reconnu, montées de version documentéesFramework maison ou abandonné
Structure du codeSéparation lisible entre logique métier, accès aux données et affichageRequêtes SQL dans les templates, copier-coller massif
Écart de versionsUne ou deux versions majeures de retardPlusieurs générations, dépendances introuvables
Modèle de donnéesCohérent, contraintes en placeIncohérent, données dupliquées partout
Besoin métierStable, l’outil fait le travailLe métier a changé, l’outil est contourné
Connaissance des règlesRègles enfouies dans le code, peu documentéesRègles connues et documentées ailleurs

La dernière ligne est contre-intuitive mais décisive : moins les règles métier sont documentées, plus une reconstruction est risquée, parce que le code existant est alors la seule spécification.

Entre les deux, il existe une voie souvent sous-estimée : la remise à niveau progressive, où l’on monte les versions par étapes, sous la protection des tests de fumée, en réécrivant au passage les modules les plus fragiles. Des outils comme Rector automatisent une bonne partie des montées de version PHP et Symfony. Elle a l’avantage de garder l’application en service pendant toute la durée des travaux.

Sans le code et les accès, on ne reprend pas une application : on la reconstruit, et on paie pour redécouvrir ce qu’elle savait déjà faire.

Les leçons

  • Le domaine d’abord. Titulaire, registrar, expiration, zone DNS relevée en entier avant tout transfert.
  • La production fait foi, pas le dépôt. Comparez toujours les deux avant de redéployer.
  • Le code ne suffit pas. Base, fichiers uploadés, crons, configuration, variables d’environnement, certificats : tout doit être copié.
  • Une sauvegarde non restaurée n’existe pas. Faites démarrer l’application ailleurs, aux versions exactes.
  • Chaque service tiers est un compte à récupérer. Et chaque clé connue de l’ancien prestataire doit être renouvelée.
  • Le filet avant les travaux. Sauvegarde, supervision, tests de fumée : d’abord.
  • Décidez sur critères, pas sur impression. Un code ancien n’est pas forcément un code à jeter.
En résumé : quand le prestataire ne répond plus, l’objectif des premières semaines n’est pas de tout réparer, mais de ne plus dépendre de personne. Domaine, hébergement, code, données et comptes tiers à votre nom, un environnement qui redémarre ailleurs et un filet de sécurité en place : alors seulement, un repreneur peut chiffrer sérieusement, et vous pouvez choisir entre remise à niveau et reconstruction en connaissance de cause.