Aller au contenu

LDAP, MongoDB, certificats

Trois cas qui ne rentrent pas dans les autres pages. L'annuaire LDAP, parce que sa restauration réserve un piège que presque personne ne découvre avant le jour J. MongoDB, parce qu'il se restaure sans décompression. Les certificats, parce qu'on n'y vérifie pas un volume mais une identité.

Voir comment adapter le bloc fetch et comment trouver les seuils.

LDAP

La sauvegarde en entrée

Un export LDIF de l'annuaire, compressé : directory_2026-09-02T03-00.ldif.gz. Peu importe qu'il vienne d'un slapcat, d'un ldapsearch "*" "+" ou d'un db2ldif : ce qui compte, c'est ce qu'il contient — et, on va y venir, ce qu'il ne contient pas.

Le plan

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer l'export de l'annuaire",
      "config": {
        "source": "local",
        "path": "/backups/ldap/ldif",
        "file_pattern": "directory_*.ldif.gz",
        "selection_strategy": "latest_modified",
        "max_age": "3h",
        "output": "directory.ldif.gz"
      }
    },
    {
      "type": "unpack",
      "name": "Décompresser",
      "config": { "format": "gzip", "input": "directory.ldif.gz", "output": "directory.ldif" }
    },
    {
      "type": "start_sandbox",
      "name": "Démarrer OpenLDAP",
      "config": {
        "image": "bitnamilegacy/openldap:2.6.10-debian-12-r4",
        "name": "demo-ldap-restore",
        "env": {
          "LDAP_ROOT": "dc=demo,dc=kolapsis,dc=com",
          "LDAP_ADMIN_USERNAME": "admin",
          "LDAP_ADMIN_PASSWORD": "demo-ldap",
          "LDAP_SKIP_DEFAULT_TREE": "yes"
        },
        "healthcheck": {
          "cmd": ["ldapsearch", "-x", "-H", "ldap://localhost:1389", "-b", "", "-s", "base", "namingContexts"],
          "interval": "5s",
          "timeout": "5s",
          "retries": 12
        }
      }
    },
    {
      "type": "restore_ldap",
      "name": "Recharger l'annuaire",
      "config": {
        "host": "localhost",
        "port": 1389,
        "bind_dn": "cn=admin,dc=demo,dc=kolapsis,dc=com",
        "password": "demo-ldap",
        "base_dn": "dc=demo,dc=kolapsis,dc=com",
        "input": "directory.ldif",
        "format": "auto"
      }
    }
  ],
  "probes": [
    {
      "name": "comptes_et_authentification",
      "type": "ldap",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_LDAP_HOST": "demo-ldap-restore",
        "RESTOREPROOF_LDAP_PORT": "1389",
        "RESTOREPROOF_LDAP_BIND_DN": "cn=admin,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_PASSWORD": "demo-ldap",
        "RESTOREPROOF_LDAP_BASE_DN": "ou=people,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_FILTER": "(objectClass=inetOrgPerson)",
        "RESTOREPROOF_LDAP_SCOPE": "sub",
        "RESTOREPROOF_LDAP_ASSERT": "eq",
        "RESTOREPROOF_LDAP_EXPECTED_ENTRIES": "6",
        "RESTOREPROOF_LDAP_EXPECT_DN": "cn=camille.durand,ou=people,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_TEST_BIND_DN": "cn=camille.durand,ou=people,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_TEST_BIND_PASSWORD": "demo-user-2026"
      }
    },
    {
      "name": "groupes",
      "type": "ldap",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_LDAP_HOST": "demo-ldap-restore",
        "RESTOREPROOF_LDAP_PORT": "1389",
        "RESTOREPROOF_LDAP_BIND_DN": "cn=admin,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_PASSWORD": "demo-ldap",
        "RESTOREPROOF_LDAP_BASE_DN": "ou=groups,dc=demo,dc=kolapsis,dc=com",
        "RESTOREPROOF_LDAP_FILTER": "(objectClass=groupOfNames)",
        "RESTOREPROOF_LDAP_SCOPE": "sub",
        "RESTOREPROOF_LDAP_ASSERT": "eq",
        "RESTOREPROOF_LDAP_EXPECTED_ENTRIES": "2",
        "RESTOREPROOF_LDAP_EXPECT_DN": "cn=support,ou=groups,dc=demo,dc=kolapsis,dc=com"
      }
    }
  ]
}

Ce que chaque sonde prouve

  • comptes_et_authentification — les six comptes sont revenus, celui de camille.durand existe, et son mot de passe fonctionne. Le test de connexion (TEST_BIND_DN / TEST_BIND_PASSWORD) est la seule vérification qui prouve qu'après restauration les gens pourront encore entrer. Un LDIF qui recharge tout sauf les empreintes de mots de passe donne un annuaire d'apparence parfaite où personne ne se connecte.
  • groupes — les deux groupes sont là, dont support. Les appartenances sont ce qui décide des droits : un annuaire restauré sans ses groupes est un annuaire où tout le monde a perdu ses accès.

RESTOREPROOF_LDAP_EXPECT_DN exige une entrée précise, par son DN complet. C'est plus fort qu'un décompte : six entrées, ce n'est pas forcément les six bonnes.

Pourquoi LDAP_SKIP_DEFAULT_TREE

Sans cette variable, l'image OpenLDAP crée au premier démarrage son propre arbre de démonstration sous LDAP_ROOT. Ces entrées entrent alors en collision avec celles que le LDIF s'apprête à charger sous le même suffixe (« entry already exists »), et une sauvegarde parfaitement restaurable échoue pour une raison qui n'a rien à voir avec elle.

Le piège du schéma personnalisé

Voici la découverte pour laquelle ce produit existe.

Un annuaire qui utilise un schéma personnalisé — n'importe quel attribut ajouté au fil des années, par une application métier ou par un greffon — ne se recharge pas sur un serveur neuf. ldapadd s'arrête sur la première entrée qui porte un attribut inconnu :

ldap_add: Undefined attribute type (17)

Et il s'arrête à chaque fois sur un seul attribut, sans rien dire des suivants. Un export qui en utilise trois demande donc trois exécutions complètes pour être diagnostiqué — récupération, décompression, démarrage du bac à sable à chaque fois — et vous n'apprenez le manque suivant qu'après avoir corrigé le précédent.

RestoreProof ne vous fait pas vivre ça. Avant d'écrire la première entrée, il demande au serveur du bac à sable ce qu'il sait accepter, le compare à ce que votre export utilise, et vous rend la liste complète en une fois : tous les types d'attributs manquants, toutes les classes d'objets manquantes.

Ce n'est pas un adoucissement. C'est un vrai constat d'échec : un export qui ne se recharge pas sur un serveur neuf n'est pas restaurable, et c'est précisément ce qu'il fallait découvrir aujourd'hui plutôt que pendant une panne.

La correction, en deux temps :

  1. Mettez le schéma dans la sauvegarde. Exportez-le à côté des données, dans un schema.ldif séparé. Un export de données ne le contient jamais : le schéma vit ailleurs, dans la configuration du serveur.
  2. Rejouez-le avant les données, avec une étape exec_in_sandbox insérée entre start_sandbox et restore_ldap. Le schéma se charge dans l'arbre de configuration, cn=config, pas dans l'arbre de données :
{
  "type": "exec_in_sandbox",
  "name": "Charger le schéma personnalisé",
  "config": {
    "cmd": "ldapadd -x -H ldap://localhost:1389 -D cn=admin,cn=config -w demo-ldap -f /workspace/schema.ldif",
    "name": "demo-ldap-restore"
  }
}

Sur l'image OpenLDAP, ce compte de configuration doit être activé dans le bac à sable — ajoutez à l'env de l'étape start_sandbox :

{
  "LDAP_CONFIG_ADMIN_ENABLED": "yes",
  "LDAP_CONFIG_ADMIN_PASSWORD": "demo-ldap"
}

L'ordre compte : le schéma d'abord, les données ensuite. C'est aussi l'ordre dans lequel il faudra les rejouer le jour où vous restaurerez pour de vrai.

Ce que ce plan ne prouve pas

Le rechargement se fait en ligne, avec ldapadd. Les attributs opérationnels — entryUUID, entryCSN, dates de création — sont retirés avant l'envoi, et refabriqués par le serveur du bac à sable : ils appartiennent au serveur, pas à vos données.

Le plan prouve donc que les entrées, leurs attributs et leurs mots de passe reviennent utilisables. Il ne prouve pas que la base interne du serveur est identique octet pour octet, et c'est très bien : ce qui vous intéresse, c'est que vos utilisateurs se reconnectent.

Les autres annuaires

L'interface propose trois modèles, selon votre serveur :

  • OpenLDAP — celui ci-dessus ;
  • 389 Directory Server / FreeIPA — même principe, mais l'image ne crée pas d'emplacement pour votre suffixe au démarrage : le modèle insère une étape exec_in_sandbox qui lance dsconf ... backend create, sans option de création d'entrées, pour que le suffixe existe et reste vide ;
  • lldap — cas à part : lldap garde tout son annuaire dans un unique fichier SQLite (users.db), pas dans un export LDIF. Le plan restaure ce fichier directement et démarre lldap dessus, sans étape de rechargement.

MongoDB

La sauvegarde en entrée

Une archive mongodump --archive --gzip : demo_2026-09-02T03-00.archive.gz.

Pas d'étape unpack : mongorestore lit l'archive compressée telle quelle. Le format: "auto" de l'étape reconnaît aussi bien une archive qu'un répertoire de fichiers BSON.

Le plan

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer l'archive MongoDB",
      "config": {
        "source": "local",
        "path": "/backups/mongo/mongo",
        "file_pattern": "demo_*.archive.gz",
        "selection_strategy": "latest_modified",
        "max_age": "3h",
        "output": "demo.archive.gz"
      }
    },
    {
      "type": "start_sandbox",
      "name": "Démarrer MongoDB",
      "config": {
        "image": "mongo:7",
        "name": "demo-mongo-restore",
        "healthcheck": {
          "cmd": ["mongosh", "--quiet", "--eval", "db.adminCommand('ping')"],
          "interval": "5s",
          "timeout": "5s",
          "retries": 12
        }
      }
    },
    {
      "type": "restore_mongo",
      "name": "Restaurer la base",
      "config": { "database": "demo", "input": "demo.archive.gz", "format": "auto" }
    }
  ],
  "probes": [
    {
      "name": "produits",
      "type": "mongodb",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_MONGO_HOST": "demo-mongo-restore",
        "RESTOREPROOF_MONGO_DATABASE": "demo",
        "RESTOREPROOF_MONGO_COLLECTION": "products",
        "RESTOREPROOF_MONGO_ASSERT": "eq",
        "RESTOREPROOF_MONGO_EXPECTED_ROWS": "50"
      }
    },
    {
      "name": "commandes_payees",
      "type": "mongodb",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_MONGO_HOST": "demo-mongo-restore",
        "RESTOREPROOF_MONGO_DATABASE": "demo",
        "RESTOREPROOF_MONGO_COLLECTION": "orders",
        "RESTOREPROOF_MONGO_FILTER": "{\"status\": \"payée\"}",
        "RESTOREPROOF_MONGO_ASSERT": "eq",
        "RESTOREPROOF_MONGO_EXPECTED_ROWS": "450"
      }
    },
    {
      "name": "amorcage_complet",
      "type": "mongodb",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_MONGO_HOST": "demo-mongo-restore",
        "RESTOREPROOF_MONGO_DATABASE": "demo",
        "RESTOREPROOF_MONGO_COLLECTION": "seed_complete",
        "RESTOREPROOF_MONGO_ASSERT": "eq",
        "RESTOREPROOF_MONGO_EXPECTED_ROWS": "1"
      }
    }
  ]
}

Ce que chaque sonde prouve

  • produits — le catalogue est revenu au complet.
  • commandes_payeesRESTOREPROOF_MONGO_FILTER prend un filtre Mongo en JSON. Ce n'est plus « il y a des commandes », c'est « il y a les 450 commandes payées ». Une restauration qui ramène les documents mais perd un champ ne passe pas ce contrôle.
  • amorcage_complet — un marqueur écrit par le script d'amorçage à la toute fin. Il prouve que la sauvegarde a été prise sur une base complète, pas pendant son remplissage. Le même motif marche chez vous : un document témoin écrit à la fin de votre traitement de nuit.

Si votre serveur demande une authentification, ajoutez RESTOREPROOF_MONGO_USER, RESTOREPROOF_MONGO_PASSWORD et RESTOREPROOF_MONGO_AUTH_DATABASE (admin par défaut). Le port se règle avec RESTOREPROOF_MONGO_PORT.

Certificats TLS

Une sauvegarde qui ramène un certificat expiré est une sauvegarde qui ne servira pas de trafic. Ici, on ne compte rien : on vérifie une identité et une date.

La sauvegarde en entrée

Une archive des fichiers PEM publics — chaîne, certificat serveur, certificat d'autorité. Pas les clés privées : le plan n'en a pas besoin, et ce qui n'entre pas dans le bac à sable ne peut pas en sortir.

Le plan

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer l'archive des certificats",
      "config": {
        "source": "local",
        "path": "/backups/pki/files",
        "file_pattern": "certs_*.tar.gz",
        "selection_strategy": "latest_modified",
        "max_age": "3h",
        "output": "certs.tar.gz"
      }
    },
    {
      "type": "unpack",
      "name": "Extraire l'archive",
      "config": {
        "format": "tar.gz",
        "input": "certs.tar.gz",
        "output": "."
      }
    }
  ],
  "probes": [
    {
      "name": "arborescence_non_vide",
      "type": "filesystem-canary",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_CANARY_MIN_FILES": "3",
        "RESTOREPROOF_CANARY_MIN_TOTAL_SIZE": "3000"
      }
    },
    {
      "name": "certificat_serveur",
      "type": "certificate",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_CERT_PATH": "public/fullchain.pem",
        "RESTOREPROOF_CERT_MIN_DAYS": "10",
        "RESTOREPROOF_CERT_EXPECT_CN": "interne.demo.kolapsis.com",
        "RESTOREPROOF_CERT_EXPECT_SAN": "interne.demo.kolapsis.com,pki.demo.kolapsis.com",
        "RESTOREPROOF_CERT_EXPECT_ISSUER": "Demo RestoreProof CA"
      }
    }
  ]
}

Ce que chaque sonde prouve

  • arborescence_non_vide — les trois PEM sont là, et pèsent quelque chose.
  • certificat_serveur — le certificat restauré est le bon et il est encore valide :
    • RESTOREPROOF_CERT_MIN_DAYS — il lui reste au moins dix jours. Un certificat qui expire dans trois jours se restaure parfaitement et ne servira rien la semaine prochaine ;
    • RESTOREPROOF_CERT_EXPECT_CN — il porte le bon nom ;
    • RESTOREPROOF_CERT_EXPECT_SAN — il couvre bien tous les noms attendus, séparés par des virgules ;
    • RESTOREPROOF_CERT_EXPECT_ISSUER — il vient bien de votre autorité, et pas d'une autre. C'est ce qui distingue le vrai certificat d'un auto-signé fabriqué par un script de secours.

Deux variables complètent la sonde : RESTOREPROOF_CERT_FINGERPRINT, qui exige une empreinte SHA-256 exacte, et RESTOREPROOF_CERT_REQUIRE_KEY, qui exige qu'un bloc de clé privée accompagne le certificat.

Sur ce dernier point, RestoreProof reste prudent : la clé privée qui se trouve éventuellement à côté du certificat n'est jamais analysée, jamais hachée, jamais nommée au-delà de « un bloc de clé privée est présent ».

Ici, pas de calibration

Il n'y a rien à mesurer sur un certificat. Un nom, une autorité, une empreinte ne sont pas des seuils : ce sont des valeurs que vous connaissez déjà, et RestoreProof ne vous proposera jamais de les déduire d'un essai. Quant aux jours restants, ils diminuent par nature — les figer comme plancher n'aurait aucun sens.