Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Système de sauvegarde · client multi-plateforme · Rust

Nom de code

Cairn

Sauvegarde adressée par le contenu, chiffrée côté client. Le serveur ne voit jamais que des empreintes.

Chaque bloc de données est réduit à une empreinte. Le client découpe, hache, chiffre ; le serveur ne stocke que des octets opaques indexés par leur adresse.

b3:7f3a9c…e1c40a55

Système — stocké, structurel Secret — clés, convergence Risque — le clair exposé

Ce document est un document de conception vivant. Il est organisé en seize sections (A–P) qui couvrent toutes les problématiques du système. Chaque section liste ses sujets avec leur état : décidé, en cours, à documenter.

Commencer par la Vue d’ensemble.

rust · fastcdc · blake3 · xchacha20 · argon2id · lsm
« Cairn » est un nom de code provisoire : un empilement de pierres qui balise un chemin et tient dans le temps.

Vue d’ensemble 7f3a

Un client fait tout le travail sensible ; un serveur fait office de stockage de blobs intelligent doublé d'un index.

Le client — multi-plateforme (Linux, macOS, Windows, Synology, QNAP…) — assure le découpage, le hachage, la déduplication et le chiffrement. Le serveur ne reçoit que des objets déjà chiffrés et les range dans un magasin adressé par le contenu plus un index. Trois propriétés structurantes orientent toute la suite :

  • Client stateless — aucune base locale persistante. Le client redémarre de zéro et interroge le serveur pour savoir ce qui existe déjà.
  • Déduplication inter-clients — un même contenu stocké une seule fois, à travers toute la flotte de machines.
  • Chiffrement côté client — rien n'atteint le disque du serveur en clair, jamais.

Restic, borg et kopia occupent ce voisinage. La combinaison précise stateless + dédup inter-clients + chiffrement est ce qui force les décisions intéressantes — chacun de ces trois projets les a tranchées différemment.

Ce document est organisé en seize sections (A–P) qui couvrent toutes les problématiques du système. Chaque section liste ses sujets avec leur état : décidé, en cours, à documenter.

Client / agent A

Le composant critique — tout le travail sensible s'y fait. Le serveur ne reçoit que des octets opaques.

  • Découpage FastCDC512 Kio / 2 Mio / 8 Mio, épinglés par tenantcdc
  • Hachage BLAKE3b291
  • Compression (zstd)zstd niveau 3, par chunk, avant l'AEAD ; dictionnaires écartészstd
  • Chiffrement / AEADaead
  • Construction des manifestes / Merkle DAGConvergence différenciée, hashsplit, CBOR canonique, flux nommésdag
  • Scan & détection de changementsMarche fusionnée sur l'arbre parent, heuristique taille+mtime+ctime+inode, journaux OS en accélérateursscan
  • Client statelessCache de l'arbre parent cadré : optionnel, jetable, revalidé par adressescan
  • RestaurationQuatre modes / un moteur, reprise = scan inversé, plan par segment, arbre traité en entrée hostilerest
  • Uploads résumables / reprise après coupure / throttling bande passanteReprise = re-run idempotent + dédup ; token-bucket + plages horairesup
  • Fichiers spéciauxInventaire exhaustif : 5 familles d'ACL (dont SynoACL, richacl), xattrs, flags d'inode, règle du rapportspec
  • Fichiers ouverts / verrouillésSnapshot-first (VSS par défaut, APFS, btrfs/ZFS/LVM), hooks pre/post, niveau de cohérence gravé par snapshotvss
  • Multi-plateformeBinaires statiques musl, 4 écosystèmes de signature, Synology non-root par dépôt tiers (nominal)plat
  • Config clientCôté serveur, tirée à chaque run, éditable dashboard + CLI ; hooks local-only ; politique à 3 étagescfg
  • FormeDaemon + CLI/GUI (Tauri) en clients IPC, self-update signé à rollout progressif, heartbeatform
  • Mode synchro à la demandeBackup continu / au fil de l'eau plutôt que planifié — watchers, debouncing, interaction avec le scan

Modèle de données b291

FastCDC découpe, BLAKE3 nomme, un Merkle DAG relie. La déduplication structurelle en tombe gratuitement.

FastCDC découpe chaque fichier en chunks de taille variable, déterminés par le contenu — robuste aux insertions, là où des blocs de taille fixe décaleraient tout après le moindre octet ajouté. BLAKE3 donne à chaque chunk son adresse de contenu.

À partir de là, tout se compose :

  • un fichier devient un manifeste — la liste des hashes de ses chunks ;
  • un gros fichier devient un arbre de manifestes, pour éviter des listes à un million d'entrées ;
  • un répertoire est une liste nom → hash ;
  • un snapshot est une racine qui pointe vers le répertoire de tête, plus des métadonnées.

L’ensemble forme un Merkle DAG. La dédup structurelle est automatique pour tout ce qui est pur contenu : deux snapshots qui partagent des fichiers partagent leurs chunks et leurs manifestes — comme Git. Les répertoires et racines, qui portent noms et dates, sont partagés par référence plutôt que par convergence — le pourquoi et le comment sont détaillés à la page Construction des manifestes & Merkle DAG.

Construction des manifestes & Merkle DAG dag Décidé

Comment une arborescence de fichiers devient un graphe de nœuds adressés par le contenu. Quatre décisions tranchées : convergence différenciée, découpage des manifestes par le contenu, CBOR canonique, fichiers à flux nommés.

En clair — recettes, annuaires, racines Un fichier est découpé en morceaux (chunks). Le manifeste, c'est la recette : la liste des morceaux qui recomposent le fichier — sans nom, sans date, que du contenu. Le nœud répertoire, c'est l'annuaire : « presentation.pptx → recette X », avec les dates, les droits, le propriétaire. La racine de snapshot, c'est la photo d'ensemble datée : « le 2 juillet à 3h00, la machine ressemblait à ça ». Toute cette page décrit comment on fabrique ces objets — et pourquoi chaque détail compte pour la déduplication.

Chaque nœud est nommé par le hash de son contenu chiffré (adresse = BLAKE3(chiffré)) → immuable, auto-vérifiable, déduplicable.

Le modèle objet — cinq types de nœuds

Comme Git (blob / tree / commit), adapté au chiffrement.

  • Chunk — un morceau de données brut, la feuille. Adresse = BLAKE3(chiffré).
  • Manifeste de fichier — la recette d'un flux de contenu : la liste ordonnée des références de ses chunks.
  • Arbre de manifestes — pour un gros fichier, le manifeste est lui-même découpé en sous-manifestes.
  • Nœud répertoire — une liste nom → référence plus les métadonnées de chaque entrée.
  • Racine de snapshot — pointe vers le répertoire de tête plus les métadonnées du snapshot.

Pourquoi un DAG, pas un arbre

Dans un arbre pur, chaque nœud a un seul parent. Ici, grâce à la dédup, un même nœud est référencé par plusieurs parents — deux fichiers qui partagent un bloc, deux snapshots qui partagent un fichier. Ces nœuds partagés font un graphe orienté acyclique (DAG), exactement comme les objets Git. Ce partage est la déduplication.


Décision 1 — Qui est convergent, qui est aléatoire

En clair — c'est quoi, « convergent » ? Chiffré convergent = la même chose chiffrée deux fois donne exactement le même résultat (la clé est calculée à partir du contenu + le secret du tenant). Si le PC d'Alice et le PC de Bob chiffrent le même logo.png, le serveur reçoit deux fois le même blob et n'en garde qu'un : c'est la déduplication. Chiffré aléatoire = chaque chiffrement tire un aléa, le résultat diffère à chaque fois : aucun partage possible, mais aussi aucune devinette possible. Car le convergent a un prix : qui possède déjà un fichier candidat peut vérifier s'il existe chez toi (l'attaque par confirmation). Tout le choix est là : dédup + devinettes, ou ni l'un ni l'autre.

La décision crypto traitait « les métadonnées » d’un bloc : tout en nonce aléatoire. En y regardant de près, les métadonnées n’ont pas toutes le même profil — on tranche donc par type de nœud, selon deux questions : que gagne-t-on à déduplicer ? et que risque-t-on à laisser deviner ?

La règle retenue Tout nœud dérivé purement du contenu → convergent (chunks, manifestes de fichier).
Tout nœud portant des noms ou de l'horloge → nonce aléatoire (répertoires, racines), avec partage par référence explicite.

Pourquoi les manifestes peuvent être convergents — l’oracle est déjà payé. Une recette, c’est la liste des adresses et clés de tous les morceaux du fichier. Pour fabriquer une recette candidate à deviner, il faut donc… posséder le fichier entier. Mais qui possède le fichier entier peut déjà le confirmer morceau par morceau, via l’oracle des chunks qu’on a accepté. Rendre les manifestes convergents ne donne rien de plus à l’attaquant — et offre une vraie dédup : la recette ne contenant ni nom ni date, deux machines qui ont le même fichier partagent la même recette même si leurs répertoires diffèrent par ailleurs. C’est le cas commercial type — la flotte de machines semblables — où l’onboarding de la N-ième machine ne coûte presque rien.

Pourquoi les répertoires restent aléatoires — tout risque, zéro gain. L’annuaire porte les noms (« existe-t-il un dossier licenciements-2026/ ? » — très devinable, très sensible) et les dates. Et il ne dédupliquerait presque jamais : les mtimes diffèrent toujours un peu d’une machine à l’autre. Convergence inutile et dangereuse → nonce aléatoire. Les racines de snapshot sont uniques par construction (date, hôte) : zéro valeur de dédup → aléatoire aussi. L’incrémental n’en souffre pas : le client stateless récupère de toute façon l’arbre du snapshot parent pour détecter les changements, et réutilise par référence les sous-arbres inchangés.

Deux propriétés qui verrouillent le raisonnement :

  • La convergence se compose vers le haut. Un nœud ne peut être convergent que si les références de ses enfants sont déterministes. Manifestes convergents → référençables de façon stable. Répertoires aléatoires → référencés par leurs parents (aléatoires aussi). Aucun conflit : le convergent ne référence que du convergent.
  • Le point de comparaison. borg, restic et kopia dédupliquent toutes leurs métadonnées, noms compris (adressage par hash/HMAC du clair). Notre choix est plus conservateur que les trois — on ne rend convergent que l'indevinable — tout en gardant l'essentiel du gain.
Impact sur la doc crypto Ce choix précise la règle « métadonnées = nonce aléatoire » de la solution retenue : elle reste vraie pour les répertoires et racines, mais les manifestes de fichier — purs contenus — passent au régime convergent. La page crypto est mise à jour en conséquence.

Le manifeste de fichier

À cause du chiffrement convergent des chunks, la recette ne liste pas que des hashes : il faut aussi la clé de chaque chunk, impossible à re-dériver à la restauration sans le clair. C’est le surcoût métadonnées 2×.

FileManifest {                                   // un flux de contenu, rien d'autre
  entries: [
    { addr: 32 o, clé: 32 o, len_clair: varint } // un chunk
    | { hole: varint }                           // un trou (fichier sparse)
    …                                            // ordre = ordre du fichier
  ]
}
  • addr pour aller chercher le blob, clé pour le déchiffrer, len_clair pour les frontières.
  • L’offset de chaque chunk se déduit par somme préfixe des len — inutile de le stocker, et ça permet le seek (restaurer une plage d’octets sans tout lire).
  • Une entrée hole dit « ici, N octets de vide » : un disque de VM de 100 Go rempli à 20 % ne stocke aucun chunk de zéros, et se restaure à l’identique.
  • Le flag de compression (brut|zstd) est dans le payload chiffré du chunk, pas dans le manifeste.
  • Rien d’autre : ni nom, ni date, ni droits — voir la règle d’or de la décision 4.

Décision 2 — Gros fichiers : l’arbre de manifestes, découpé par le contenu

Un fichier de 1 Tio à 2 Mio de chunk ≈ 500 000 entrées → une recette plate de ~32 Mo, à télécharger et déchiffrer en entier pour restaurer le moindre octet. Trop lourd : on découpe la recette en pages (sous-manifestes), avec un sommaire qui pointe vers les pages — récursivement si besoin. Reste à décider où couper les pages.

En clair — le piège des pages de taille fixe Option naïve : une page = 1 000 lignes, point. Mais insère trois lignes au début de la recette (des données ajoutées en tête du fichier) : tout se décale, page 1, page 2, … toutes les pages changent, tout est re-stocké alors que 99,9 % n'a pas bougé. C'est exactement le défaut des blocs de taille fixe qu'on a refusé pour les données en choisissant FastCDC. Utiliser la solution intelligente pour les octets et la naïve pour les recettes serait incohérent.
Décision — hashsplit récursif On coupe la page quand une entrée a une « forme » particulière — son adresse satisfait un masque de bits (cible ~1 024 entrées/nœud, min/max bornés), technique éprouvée par bup. Les frontières « collent » au contenu : une insertion ne change qu'une ou deux pages au lieu de toutes. Déterministe, car les adresses des enfants le sont (décision 1 — la composition, encore). Les paramètres (masque, cible, min/max) rejoignent le profil épinglé du tenant, même discipline que le chunker.

Coût réel : nul pour presque tout le monde. À 2 Mio de chunk moyen, un fichier < ~2 Gio tient en une seule page — l’arbre ne s’active que pour les fichiers réellement énormes.

Décision 3 — Sérialisation : CBOR canonique

En clair — le formulaire, pas la lettre libre Avant de chiffrer un nœud, il faut l'écrire en octets. Or l'adresse d'un nœud = l'empreinte de ses octets. Si le client d'Alice écrit {nom: "a", taille: 5} et celui de Bob {taille: 5, nom: "a"} — même information, octets différents — les adresses diffèrent et la dédup casse. Il faut donc des règles d'écriture strictes, sans aucune liberté : le formulaire administratif où chacun remplit les cases dans le même ordre, pas la lettre libre. C'est ce qu'on appelle un encodage canonique.
Décision — CBOR, profil déterministe CBOR canonique (RFC 8949 §4.2) : clés triées, longueurs définies, encodages minimaux — le déterminisme est déjà spécifié par la norme, pas à réinventer. Clés entières pour la compacité, un octet de version de format dans chaque nœud, et une validation stricte de canonicité au parsing dans une bibliothèque partagée par tous les clients — c'est elle qui garantit « même contenu → mêmes octets », pas la bonne volonté. En Rust : minicbor.

Écartés : protobuf (explicitement non-déterministe entre implémentations — disqualifiant pour de l’adressage par hash), MessagePack (pas de profil canonique normalisé : à réinventer), format maison (gagne ~10–20 % d’octets — sans intérêt face à des chunks de 2 Mio, au prix d’une spec et d’un outillage à maintenir).

Décision 4 — Nœud répertoire & métadonnées : flux nommés, et la règle d’or

DirEntry {
  nom, type,
  flux: [ { nom_flux, ref: {addr, clé}, taille } ],   // "data" + ADS Windows / forks macOS
  meta: { mode, uid, gid, user, group,                 // numéros ET noms
          mtime_ns, ctime_ns,
          cible_symlink?, dev?, groupe_hardlink?,
          xattrs?, acl?, … }                           // champs optionnels versionnés
}
DirNode { entries: [DirEntry] triées par nom }          // tri = sérialisation déterministe

Trois choix structurent ce modèle :

  • Un fichier = un tiroir à compartiments. Windows autorise des flux cachés en plus du contenu principal (les Alternate Data Streams) ; macOS a ses resource forks. Plutôt que de les boulonner plus tard, chaque fichier est d'emblée un ensemble de flux nommés, chacun avec sa propre recette. Un fichier Linux ordinaire a un seul flux — coût zéro ; le jour du support Windows sérieux, la case existe déjà.
  • Propriétaire en double. uid/gid numériques et noms (user/group) — sur une autre machine, « l'utilisateur 1000 » n'est pas forcément le même ; à la restauration on choisit le mapping. Liens durs regroupés (même dev+inode → contenu stocké une fois, liens recréés). Champs optionnels versionnés pour étendre (ACL, xattrs, SID/DACL Windows, flags Finder) sans casser le format.
  • La règle d'or : les métadonnées ne vont jamais dans la recette. Alice et Bob ont le même fichier, modifié à des dates différentes. Si la date était dans le manifeste, leurs recettes différeraient d'un octet → adresses différentes → plus aucun partage — toute la décision 1 s'évapore. Donc : recette = pur contenu ; date, droits, propriétaire = dans l'annuaire (chiffré aléatoire). C'est le verrou de l'édifice.

Petites métadonnées inline dans l’entrée ; grosses valeurs (ACL/xattrs volumineux) débordées en blobs référencés, à nonce aléatoire. À raffiner avec le sujet Fichiers spéciaux (sparse déjà couvert par les entrées hole).

Racine de snapshot

Snapshot {
  tree:    { addr, clé },            // le répertoire de tête
  date, hôte, chemins_source[],
  parent?: { addr, clé },            // snapshot précédent (historique + réutilisation)
  profils: { chunker_id, compression_id, manifest_split_id },
  version_outil, tags[]
}

profils fige quels paramètres de découpage, de compression et de hashsplit ont produit ce snapshot. L’ensemble des racines d’un tenant = les racines du GC : le mark-and-sweep des points ouverts part de là.

Construction & restauration

# construction : post-order (enfants avant parents)
fichier → chunker → (clé, chiffré, adresse) par chunk → manifeste (convergent)
répertoire → nœud à partir des références de ses enfants (nonce aléatoire)
tête → racine de snapshot (nonce aléatoire)
# à chaque nœud : vérifier l'existence avant d'uploader (dédup)

On construit enfants avant parents : un parent référence ses enfants par adresse, qu’on ne connaît qu’après les avoir hachés. La mémoire reste bornée par la profondeur du chemin courant — bon pour les NAS. À la restauration, on descend le DAG : racine → répertoire → manifeste → chunks → déchiffrer → décompresser → réassembler ; restauration partielle en ne descendant que la branche voulue, seek dans les gros fichiers via l’arbre de manifestes. Et comme chaque adresse est le hash de son contenu, télécharger et rehacher vérifie l’intégrité de haut en bas — inviolable.


Les quatre décisions en une ligne

  • 1 · Convergence différenciée — recettes partagées (indevinables sans posséder le fichier), annuaires et racines aléatoires (les noms se devinent). Plus prudent que borg/restic/kopia, qui dédupliquent tout.
  • 2 · Hashsplit récursif — les recettes des gros fichiers sont découpées par le contenu, même astuce que FastCDC un étage plus haut. Paramètres épinglés.
  • 3 · CBOR canonique — mêmes octets partout, règles déjà normalisées (RFC 8949), validation stricte partagée.
  • 4 · Flux nommés + règle d'or — fichier = compartiments (ADS/forks prêts), trous explicites, et jamais de métadonnées dans les recettes.

Cohérence d’ensemble : chaque nœud reçoit le régime crypto que son entropie justifie, le principe « content-defined » s’applique aux deux étages, tout paramètre influençant les adresses vit dans le profil épinglé du tenant, et un seul GC traite tous les nœuds pareil.

Taille des chunks cdc Décidé

Une cible : min 512 Kio · moyenne 2 Mio · max 8 Mio, épinglés par tenant. On penche vers le gros — plus que restic — parce que le chiffrement convergent double le coût des métadonnées par chunk.

FastCDC découpe sur le clair, BLAKE3 nomme chaque chunk. Reste à fixer les trois bornes du découpage. C’est un choix quasi-irréversible : changer les paramètres change toutes les frontières, donc toutes les adresses — c’est un re-découpage complet du dépôt. On tranche une fois, prudemment.

L’arbitrage

Le curseur oppose deux coûts.

  • Petits chunks (~8–64 Kio) — meilleure granularité de dédup (modifs internes, fragments partagés entre fichiers), mais explosion des métadonnées, flot de vérifications d'existence pour un client stateless, et compression médiocre par chunk.
  • Gros chunks (~1–4 Mio) — index et manifestes légers, gros débit, peu d'aller-retours, compression par chunk déjà pleine ; en échange une granularité de dédup plus grossière (une modif ré-écrit ~un chunk entier, mais FastCDC borne déjà le rayon de souffle à un seul chunk).

Le gain réel des petits chunks est étroit : les fichiers identiques dédupliquent déjà entièrement (même fichier = même manifeste = même jeu de chunks), quelle que soit la taille. Ce qu’on gagnerait, c’est surtout la dédup de fragments partagés entre fichiers différents, et la dédup des gros fichiers mutables (images VM, bases, boîtes mail) édités en place — un cas précis, à valider par la mesure avant de payer pour lui.

Ce que font les voisins

Outilminmoyennemax
restic512 Kio~1 Mio8 Mio
rustic512 Kio~1 Mio8 Mio
borg512 Kio2 Mio8 Mio
kopia2 Mio4 Mio8 Mio
Cairn512 Kio2 Mio8 Mio

Deux constantes : min ≥ 512 Kio et max = 8 Mio partout. restic tire vers ~1 Mio de moyenne, borg vers 2 Mio, kopia jusqu’à 4 Mio. rustic hérite par défaut du chunker de restic (compatibilité des dépôts) mais reste le seul à exposer min/moyenne/max en configuration — précédent utile pour nous.

Le facteur décisif : nos métadonnées pèsent le double

Pourquoi Cairn doit pencher plus gros que restic Chez restic et borg, une clé de dépôt unique chiffre tout : le manifeste ne stocke qu'un identifiant de contenu par chunk (~32 o, le hash). Cairn fait du chiffrement convergentclé_chunk = BLAKE3_keyed(secret, chunk) — et cette clé ne peut pas être re-dérivée à la restauration (il faudrait le clair, précisément ce qu'on restaure). Il faut donc la stocker dans le manifeste, à côté de l'adresse : ~64 o par chunk, soit ≈ 2× restic. À taille de chunk égale, notre index et nos manifestes sont deux fois plus lourds.

Conséquence directe : le coût métadonnées est le seul qu’on ne peut pas récupérer après coup (contrairement à l’espace des blobs, que le GC finit par rendre). Donc dans le doute on vise un peu trop gros, et jamais plus petit que restic/borg.

Le piège : des paramètres non épinglés cassent la dédup

À figer par scope — sinon dédup silencieusement cassée Les paramètres de découpage — la table/seed gear de FastCDC et min/moyenne/max — doivent être épinglés par scope de dédup et rangés dans la config du tenant, exactement comme le profil de compression. Deux versions du client qui découpent différemment produisent des frontières différentes → des chunks différents → des adresses différentes → aucune dédup entre elles, sans le moindre message d'erreur. C'est le même mécanisme que restic, qui stocke son polynôme (degré 53, tiré au hasard) dans la config du dépôt pour que tous les clients découpent à l'identique.

La décision

min 512 Kio · moyenne 2 Mio · max 8 Mio, FastCDC en chunking normalisé (NC niveau 2) pour resserrer la distribution autour de la cible. En Rust : la crate fastcdc.

  • On ne descend à 1 Mio de moyenne que si une mesure sur une charge réelle riche en gros fichiers mutables montre un gain de dédup net.
  • On ne descend jamais sous 512 Kio de minimum : en dessous, le coût métadonnées, l'overhead par chunk (tag AEAD, framing) et le flot de vérifications d'existence dominent.
  • kopia va plus gros encore (2/4/8) : si l'index devient le goulot, c'est la direction — pas l'inverse.
Pas de corpus de référence — pour l'instant On ne sait pas encore ce que les clients sauvegarderont, donc on ne peut pas mesurer le meilleur réglage. On assume ce défaut raisonné (aligné borg) et on le remesurera dès qu'un corpus réel existera. Comme les paramètres vivent dans la config du tenant, on pourra ajuster le défaut des futurs dépôts sans toucher aux existants — un dépôt déjà en place garde ses paramètres (les changer = re-découpage complet).

Corollaire, traité au point Compression : à 2 Mio de chunk, chaque chunk se compresse pleinement seul — les dictionnaires zstd, utiles seulement sur de petits chunks, sont écartés.

Compression zstd Décidé

Compression par chunk, en zstd, à un seul emplacement possible : entre le découpage et l'AEAD. À 2 Mio de chunk, chaque chunk se compresse pleinement seul — donc pas de dictionnaire.

Où — et quand — la compression a lieu

Dans le pipeline décidé, il n’y a qu’un seul créneau viable : par chunk, après FastCDC, après la dérivation de la clé depuis le clair, et juste avant l’AEAD.

# par chunk, sur le clair — tout est déterministe
clé_chunk = BLAKE3_keyed(secret, chunk) # clé dérivée du CLAIR
payload = compresse(chunk) # ← la compression a lieu ICI
chiffré = XChaCha20-Poly1305(clé_chunk, payload)
adresse = BLAKE3(chiffré)

La clé reste dérivée du clair, pas du compressé : la compression ne change que le payload chiffré, jamais la logique de clé ni de manifeste. Les trois autres emplacements sont exclus :

  • Compresser puis découper (CDC sur le flux compressé) → tue la dédup : la compression rend le flux positionnel, un octet modifié en tête décale tout l'aval et tous les chunks changent d'adresse. Le découpage se fait sur le clair.
  • Compresser après chiffrement → impossible : le chiffré est à entropie maximale, incompressible.
  • Par fichier plutôt que par chunk → le chunk est l'unité de dédup et de chiffrement ; on compresse chaque chunk indépendamment.

La contrainte qui domine : déterminisme figé

Le compressé doit être identique sur toute la flotte Puisque adresse = BLAKE3(chiffré) et que la dédup se fait sur l'adresse, deux machines d'un même tenant doivent produire un chiffré bit-à-bit identique pour le même chunk. La clé et le nonce le sont déjà ; reste le payload. Les octets compressés doivent donc être identiques partout, pour toujours — sinon le même chunk logique se stocke deux fois et la dédup se dégrade en silence.

En pratique, on épingle un profil de compression versionné, rangé dans la config du tenant (même discipline que les paramètres de découpage) : codec + version de zstd + niveau + framing + la règle de décision. zstd est déterministe pour un (version, niveau, params) donné, mais pas garanti stable entre versions majeures — d’où l’épinglage.

Compresser seulement si ça aide

Beaucoup de chunks sont déjà compressés (jpeg, mp4, zip, docx…). Les recompresser gâche du CPU et, par le principe des tiroirs, gonfle légèrement le résultat : zstd ajoute son framing (en-tête de frame + en-têtes de bloc), et sur du contenu incompressible il range un « raw block » ≈ taille + une dizaine d'octets.

Règle : tenter zstd, garder le compressé seulement s’il est plus petit, sinon stocker brut — avec 1 octet de flag brut|zstd à l’intérieur du payload chiffré (couvert par le tag AEAD, ne fuit pas). Cette décision fait elle aussi partie du profil épinglé : deux clients doivent trancher pareil, ou le chiffré diffère et la dédup casse.

Le codec

zstd, niveau ~3. Meilleur ratio/vitesse, déterministe, niveaux réglables. La cible inclut des NAS bas de gamme (déjà l’argument du choix d’AEAD) : on vise un niveau modéré, pas 19. lz4 serait plus rapide mais moins bon, inutile car zstd niveau bas est déjà très rapide ; brotli/xz trop lents pour du débit backup.

Dictionnaires zstd — écartés

Utiles seulement sur de petits chunks Un dictionnaire donne au compresseur un « faux historique » de contenu commun, ce qui aide énormément sur les petits chunks (peu de redondance interne, temps de chauffe). À 2 Mio de chunk, chaque chunk a déjà tout le contexte qu'il lui faut : le dictionnaire n'apporte quasi rien. Or son coût est réel — artefact à entraîner côté client, à chiffrer par tenant, à distribuer, et surtout à épingler dans le profil : en changer forke l'espace d'adresses et impose une migration.

On les écarte donc tant qu’on reste sur de gros chunks. Piste conservée si un jour un cas d’usage impose de petits chunks : rendre chaque chunk auto-descriptif (un index de profil dans le payload chiffré, un registre de profils et un objet dictionnaire chiffré par tenant), pour qu’une flotte mixte restaure toujours correctement. Hors périmètre actuel.

Le choix de l’AEAD aead Décidé

Le danger « réutilisation de nonce » qu'on agite d'habitude ne s'applique presque pas ici. Une fois ce faux problème écarté, le vrai critère est le matériel — et il pointe vers XChaCha20-Poly1305.

Le faux problème. La catastrophe d’AES-GCM, c’est un même (clé, nonce) sur deux clairs différents. Or sur le chemin des chunks, la clé est dérivée du contenu : clé_chunk = BLAKE3_keyed(secret, chunk). Deux chunks différents → deux clés différentes ; deux fois le même chunk → même clé, même chiffré (ce qu’on veut pour la dédup). Le cas dangereux — même clé, même nonce, clair différent — ne peut pas se produire par construction, car la clé est unique par clair. On peut même prendre un nonce constant. La seule « fuite » : deux chiffrés identiques trahissent deux clairs identiques — ce que la dédup révèle déjà.

Où le nonce compte vraiment — les métadonnées Manifestes et racines ne sont pas convergents : une seule clé de tenant, longue durée de vie, des millions de clairs différents, nonce aléatoire. Là, la collision de nonce redevient réelle — un nonce de 96 bits (AES-GCM) tombe sous la borne anniversaire. Deux réponses : un nonce assez grand (XChaCha = 192 bits), ou la misuse-resistance (GCM-SIV).

Le vrai critère : le matériel. AES n’est rapide qu’avec accélération (AES-NI sur x86, extensions ARMv8). Sans elle, l’AES logiciel est lent et vulnérable au cache-timing (implémentations à table-T qui fuient la clé). ChaCha20 est constant-time par conception et rapide en logiciel pur.

Débit AEAD mesuré — 16 Ko/bloc, 1 cœur

AVEC AES-NI · MACHINE NORMALE AES-256-GCM 3,68 AES-256-GCM-SIV* ~2,4 XChaCha20-Poly1305 2,08 SANS AES-NI · NAS BAS DE GAMME* AES-256-GCM ~0,2 ⚠ fuite cache-timing XChaCha20-Poly1305 ~0,7 0 1 2 3 4 GB/s

Mesuré sur x86 AES-NI, 1 cœur, blocs 16 Ko (openssl speed). *GCM-SIV et le cas sans AES-NI sont estimés. La ligne corail = débit Gigabit Ethernet (~0,125 GB/s) : même la branche la plus lente sature le réseau.

Sur cette machine, AES-GCM va ~1,8× plus vite que ChaCha. Mais trois choses font fondre cet écart :

  • Ce n'est pas le bon candidat. Ton candidat était AES-GCM-SIV, qui fait deux passes (~2,4 GB/s). Face aux ~2,08 de XChaCha, l'écart réel n'est plus 1,8× mais ~1,1-1,25×.
  • Le chiffrement n'est pas le goulot. À 2 GB/s, tu es bien au-dessus du disque et du réseau.
  • Sur NAS sans AES-NI, le rapport s'inverse. L'AES logiciel chute à ~0,1-0,3 GB/s et fuit la clé par cache-timing ; ChaCha tient ~0,5-1 GB/s et reste constant-time.
Décision — XChaCha20-Poly1305 partout Chunks : nonce déterministe. Métadonnées : nonce aléatoire 192 bits, zéro souci de collision. Un seul code-path, constant-time sur toute la flotte hétérogène. En Rust : crate chacha20poly1305 (RustCrypto), XChaCha20Poly1305.

Honnêteté. L’extension XChaCha (nonce 192 bits) est un draft CFRG, pas un RFC final — mais déployée depuis des années via libsodium et considérée solide. AES-GCM-SIV reste défendable si ta flotte réelle est massivement x86/ARMv8-crypto. Vu les NAS bas de gamme dans la cible, on tranche ChaCha.

Scan & détection de changements scan Décidé

Savoir quels fichiers ont changé sans base locale et sans relire des téraoctets chaque nuit : l'arbre du snapshot parent sert d'état de référence, une marche fusionnée compare les métadonnées, et seuls les fichiers modifiés sont relus. Vite, mais jamais faux.

En clair — l'inventaire comparé Chaque nuit, l'inventoriste refait le tour de l'entrepôt. Méthode naïve : rouvrir chaque carton et tout recompter — des heures. Méthode intelligente : prendre l'inventaire de la veille et longer les étagères en le comparant — même étiquette, même poids, même date sur le carton ? On ne l'ouvre pas, on recopie la ligne d'hier. Seuls les cartons dont l'étiquette a bougé sont rouverts. Toute la page décrit cette comparaison — et les pièges qui feraient recopier la ligne d'un carton qui a en réalité changé.

L’état de référence : l’arbre du snapshot parent

Le client est stateless — aucune base locale requise. Son état de référence, c’est l’arbre du snapshot précédent, téléchargé au début du backup : la racine, puis les nœuds répertoire (petits, servis depuis les segments métadonnées SSD — le stockage a été conçu pour exactement ce chemin). L’état vit chez le serveur, chiffré ; le client ne fait que l’emprunter le temps du backup.

La marche fusionnée

On parcourt en parallèle le système de fichiers local et l’arbre parent, répertoire par répertoire, dans l’ordre des noms — et la décision « entrées triées par nom » paie ici : comparer deux listes triées se fait en un seul passage, mémoire bornée à la profondeur du chemin, aucun arbre entier en RAM.

# pour chaque répertoire, deux listes triées côte à côte
présent des deux côtés → comparer les métadonnées # inchangé ? → copier la référence
présent seulement en local → nouveau fichier # chunker, chiffrer, uploader
présent seulement dans l'arbre parent → supprimé # absent du nouveau snapshot

Fichier inchangé → zéro lecture. On copie la référence (addr, clé) de son manifeste depuis l’entrée parente, sans même ouvrir le manifeste. Un backup incrémental d’un NAS calme = une promenade de métadonnées plus quelques fichiers relus.

Décision 1 — L’heuristique « inchangé »

En clair — contrôler la carte d'identité, pas refaire l'interrogatoire Relire un fichier de 10 Go pour vérifier qu'il n'a pas changé, c'est refaire l'interrogatoire complet. Comparer sa taille, sa date de modification et son numéro d'inode, c'est contrôler la carte d'identité : quasi instantané, et fiable — sauf faux papiers. Le reste de la page traite précisément des faux papiers.

Le verdict « inchangé » s’appuie sur taille + mtime + ctime + inode quand ils sont disponibles et fiables — le même trio que restic, borg et kopia, renforcé du ctime. Chaque champ a son rôle et ses pièges :

  • taille + mtime — le socle. Pièges : mtime est falsifiable (touch -t), certains outils le préservent en changeant le contenu, et sa granularité peut être grossière (FAT : 2 s).
  • ctime — le garde-fou : non falsifiable sous Linux (aucune API pour le forcer), il trahit les modifications à mtime préservé et les changements de métadonnées.
  • inode — attrape le fichier remplacé par un autre de même taille et même date. Piège : instable sur certains FS réseau/FUSE → ignoré là où il ment.
  • Dégradation par système de fichiers — le client sait quels champs sont fiables sur quel FS et ajuste : pas d'inode sur tel montage réseau, granularité 2 s sur FAT, etc.

Décision 2 — La fenêtre d’instabilité

Le piège classique — le fichier modifié pendant qu'on le lit Un fichier modifié pendant sa lecture peut être capturé incohérent — moitié ancienne version, moitié nouvelle. Pire : si sa date de modification tombe dans la même seconde que le scan, le backup suivant le jugera « inchangé » et recopiera pour toujours la version incohérente. borg s'y est brûlé. Parade en deux temps : stat avant lecture, re-stat après — s'il a bougé entre les deux, on le relit ou on le marque instable ; et tout fichier dont le mtime est trop proche de l'instant du scan est relu d'office au backup suivant, par principe.

Décision 3 — Les journaux OS : accélérateurs, jamais autoritaires

Certains systèmes tiennent un journal des changements : USN Journal sous Windows (fiable, éprouvé — il permet de sauter des sous-arbres entiers), FSEvents sous macOS (indices par répertoire), et rien de fiable sous Linux qui survive au reboot (inotify ne tient pas l’échelle).

Même principe que le bloom filter du stockage Un journal a le droit de dire « rien n'a bougé ici, saute » pour accélérer — jamais de servir de source de vérité. La marche de métadonnées reste le chemin canonique, et un scan complet périodique sert de filet de sécurité. Optimisation par plateforme, pas fondation : le jour où le journal ment (rotation, reboot, bug), le backup reste correct, juste moins rapide.

Décision 4 — La politique, en trois crans

  • fast (défaut) — l'heuristique métadonnées : confiance au trio taille + mtime + ctime/inode.
  • paranoid — en plus : relire un échantillon aléatoire des fichiers « inchangés » à chaque run (attrape les modifications à mtime préservé), et une relecture complète planifiée (p. ex. mensuelle). Se marie avec la vérification de restauration de la section M.
  • force — tout relire. Pour les audits et les doutes.

Décision 5 — Le cache éphémère local, enfin cadré

Télécharger l’arbre parent à chaque backup coûte un peu (quelques Mo à quelques centaines de Mo pour des millions de fichiers). Un cache local de l’arbre parent l’évite — à trois conditions qui préservent le contrat stateless :

  • Optionnel — tout fonctionne cache vide, rien n'en dépend.
  • Jetable — le supprimer ne casse rien, il se reconstitue au backup suivant.
  • Revalidé par adresse — le serveur annonce l'adresse de la racine du snapshot parent ; si le cache ne correspond pas, il est ignoré et re-téléchargé. Le serveur reste l'unique source de vérité.

Ça referme la note « cache éphémère local à cadrer » du sujet Client stateless.

Ce que ça donne, chiffré

NAS d’un million de fichiers, 0,5 % modifiés par nuit : la marche stat prend quelques minutes, ~5 000 fichiers sont relus et re-chunkés, le reste n’est que copies de références. Sans ce mécanisme : relecture de 4 To chaque nuit. Avec : quelques Go lus. C’est le multiplicateur de vitesse des backups quotidiens — et la raison d’être du choix stateless-mais-pas-lent.

Restauration rest Décidé

Quatre modes servis par un seul moteur, une reprise qui n'est que le scan à l'envers, des écritures atomiques, un plan de téléchargement groupé par segment — et un principe : le snapshot est une entrée non fiable.

En clair — l'album photo, pas la pile de calques Les backups traditionnels empilent un « full » puis des incréments : pour retrouver le 12 mars, on repart du full et on rejoue 40 diffs — long, fragile, et si un calque de la pile est abîmé, tout ce qui suit est perdu. Cairn range des pages d'album : chaque snapshot est une photo complète de la machine (la racine pointe vers un arbre entier — les pages partagent leurs photos en coulisse via la dédup, donc l'album ne grossit presque pas). Restaurer le 12 mars = ouvrir l'album à la page du 12 mars. Le « point-in-time » n'est pas une fonctionnalité, c'est la construction même.

Ce que l’architecture donne gratuitement

  • Chaque snapshot est un « full » logique — aucune chaîne à rejouer, on choisit une racine et on descend le DAG.
  • Restaurer, c'est vérifier. Chaque adresse est le hash du contenu chiffré, chaque déchiffrement valide le tag AEAD : un chunk corrompu, tronqué ou substitué est détecté mécaniquement, pas par bonne volonté. Un restore --dry-run qui n'écrit rien = le test de vérification de la section M, gratuit.

Décision 1 — Quatre modes, un seul moteur

Restauration complète, partielle (un sous-arbre, une sélection), un fichier, ou une plage d’octets d’un fichier — les quatre sont le même algorithme : descendre le DAG en n’ouvrant que les branches demandées. Pour la plage, la somme préfixe des len du manifeste donne directement quels chunks couvrent les octets voulus (seek) : restaurer 100 Mo au milieu d’une image disque de 2 To ne télécharge que ~50 chunks, pas le fichier.

Décision 2 — La reprise, c’est le scan à l’envers

En clair — le déménageur qui coche sa liste Restauration interrompue au carton 80 000 sur 100 000 : on ne redéménage pas tout. Le déménageur repasse avec sa liste et coche ce qui est déjà en place — bon carton, bonne pièce, bon état — et ne transporte que ce qui manque. Or cette « liste à cocher », on l'a déjà construite : c'est la marche fusionnée du scan, utilisée à l'envers.

Le moteur de comparaison du backup (arbre du snapshot ↔ disque local) sert dans les deux sens : au backup, ce qui diffère monte ; à la restauration, ce qui diffère descend. Conséquences :

  • Reprise après crash = relancer la même commande — le re-run est idempotent, les fichiers déjà restaurés (présents et conformes) sont sautés.
  • Mode « synchroniser » gratuit — ramener un dossier à l'état exact d'un snapshot, en ne touchant que ce qui a dévié.
  • Un seul moteur à écrire, tester et durcir, pour les deux directions.

Décision 3 — Écriture atomique, métadonnées en dernier

Jamais de demi-fichier sous son nom final. Chaque fichier est écrit dans un temporaire puis basculé d’un rename() atomique — un crash en pleine restauration laisse des brouillons identifiables, jamais un fichier final corrompu. Et l’ordre des finitions compte :

  • contenu → xattrs/ACL → permissions → timestamps en dernier (sinon la pose des attributs les modifierait) ;
  • mtimes des répertoires en post-order — écrire les enfants modifie le mtime du parent, donc on date le parent après ses enfants ;
  • liens durs : première occurrence = contenu, suivantes = lien ;
  • propriétaires : au choix par uid/gid numériques ou par noms (on stocke les deux) — indispensable quand on restaure sur une autre machine où « l'utilisateur 1000 » n'est pas le même.

Décision 4 — Le plan de téléchargement

En clair — les courses par rayon, pas dans l'ordre de la recette La recette dit « farine, puis beurre, puis encore de la farine » : personne ne traverse le magasin trois fois. On lit toute la recette, on regroupe la liste par rayon, et on fait un seul passage. Restaurer fichier par fichier, c'est suivre la recette : des allers-retours partout dans l'entrepôt (le point douloureux de restic). Cairn lit d'abord tous les manifestes, regroupe les chunks par segment, et télécharge dans l'ordre des caisses.

Concrètement, une restauration de 1 To ≈ 500 000 chunks éparpillés. La phase de plan collecte toutes les adresses, les trie par (segment, offset) → le serveur lit ses segments quasi séquentiellement (les HDD adorent), le client réassemble. Et la dédup joue à l’envers : un chunk partagé par 50 fichiers est téléchargé une fois, écrit 50 fois — cache éphémère de session, même contrat que celui du scan (optionnel, jetable).

Décision 5 — Le snapshot est une entrée non fiable

L'étiquette piégée — à ne jamais suivre aveuglément Un arbre corrompu — ou forgé — peut contenir un nom de fichier ../../etc/passwd, ou un lien symbolique vers l'extérieur suivi d'écritures « à travers » le lien. Suivre ces étiquettes aveuglément = écrire hors du répertoire cible — la famille de CVE « path traversal » qui a mordu tar et restic. Règles gravées : noms validés (pas de séparateur, pas de .., noms réservés Windows rejetés), symlinks jamais suivis pendant la création (O_NOFOLLOW / RESOLVE_BENEATH), et toute violation arrête la restauration avec un diagnostic — pas un warning qu'on scrolle.

Décision 6 — Politiques de collision

Restaurer vers l’emplacement d’origine ou ailleurs ; si un fichier existe déjà : skip, overwrite ou newer-only — l’overwrite étant sûr par construction (temp + rename). Défaut non destructif : restauration dans un sous-dossier daté (restore-2026-07-02/) ; écraser en place est un choix explicite, jamais une surprise.


Plus tard (noté, pas conçu) : monter un snapshot en lecture seule comme un disque (FUSE / WinFsp), avec téléchargement paresseux — parcourir « le NAS d’il y a trois semaines » dans l’explorateur de fichiers avant de choisir quoi restaurer. Servira l’UI de restauration de la section H.

Uploads résumables & throttling up Décidé

La reprise après coupure est un sous-produit de la dédup — pas de sessions d'upload, pas d'état de reprise : un re-run idempotent suffit. Reste à cadrer le pipeline, le throttling et la discipline de retry.

En clair — le déménagement par camions Le premier backup, c'est un déménagement : des milliers de cartons (les chunks), un camion à la fois. Si la route ferme à mi-parcours, les cartons déjà livrés restent livrés. Le lendemain, on repointe la liste devant l'entrepôt — « ceux-là, je les ai déjà » — et seuls les manquants repartent. Personne ne « recommence le déménagement ». Chez Cairn, ce pointage existe déjà : c'est le check d'existence de la déduplication.

Le problème

Le premier backup est long — bien plus long que l’intuition ne le dit.

En clair — bits et octets, le piège du facteur 8 Les opérateurs vendent des bits par seconde, vos fichiers pèsent des octets — et 1 octet = 8 bits. « 100 méga » (100 Mbit/s) = 12,5 Mo/s seulement. Donc 500 Go ÷ 12,5 Mo/s = 40 000 s ≈ 11 heures théoriques, ~12 h avec l'overhead réel (TCP, TLS). Et pour un backup c'est le débit montant qui compte, souvent bien pire : à 20 Mbit/s d'upload VDSL, les mêmes 500 Go prennent 2,3 jours. À 1 Gbit/s symétrique : ~1 h 10.

Sur ces durées, les coupures ne sont pas l’exception mais la règle : réseau qui tombe, laptop qui s’endort, box qui redémarre. Si chaque incident renvoie à zéro, le backup initial ne se termine jamais — la spirale de la mort des gros premiers backups. Et à l’inverse, un backup qui sature le lien du bureau à 14 h se fait désinstaller.

Ce que l’architecture donne déjà

  • La reprise est un sous-produit de la dédup. L'unité d'upload est le chunk. Après une coupure, le client relance le même backup : le scan repasse en quelques minutes, les fichiers changés sont re-chunkés localement, et le check d'existence répond « déjà là » pour tout ce qui était monté avant la coupure → seul le reste part. Les 400 Go déjà uploadés sont acquis, sans état de reprise ni protocole spécial.
  • Rien de visible avant la fin. Construction en post-order → la racine du snapshot est écrite en dernier. Backup interrompu = des chunks orphelins en attente (protégés du GC par les époques), aucun snapshot partiel, jamais. Un snapshot existe entièrement ou pas du tout.

Décision 1 — Reprise par dédup, pas de sessions d’upload

On n’introduit pas de protocole d’upload résumable avec état (sessions multipart façon S3, identifiants qui expirent, nettoyage de sessions mortes). La reprise, c’est un re-run idempotent + les checks d’existence — le mécanisme existe déjà, il est correct par construction, et il n’y a rien à synchroniser ni à faire expirer.

Corollaire assumé : pas de reprise intra-chunk. Un chunk de 8 Mio interrompu en vol se renvoie en entier — quelques secondes perdues, contre une vraie complexité de protocole. La seule notion de « session » vit côté serveur : l’époque GC qui protège les chunks fraîchement arrivés tant qu’aucune racine ne les référence (le point ouvert de la section C).

Décision 2 — Le coût de reprise assumé : re-chunker localement

Reprendre exige de relire et re-chunker les fichiers changés pour recalculer leurs adresses — il n’y a pas d’état local, c’est le contrat stateless. On l’assume : le disque local est des ordres de grandeur plus rapide que le WAN. Re-chunker 2 To ≈ 2–3 h de lecture locale, contre des jours de WAN économisés par la dédup.

Le cas pathologique — l’image VM géante interrompue cinq fois — a sa vraie réponse ailleurs : le CBT de la section P, qui ne relit que les blocs modifiés. Pas un mécanisme de checkpoint qui violerait le contrat stateless. Le cache éphémère peut mémoriser la progression — optionnel et jetable, comme toujours.

Décision 3 — Le pipeline d’upload

scan → chunk → compresse → chiffre → check d'existence (par lots)upload (N flux parallèles)ack durable
# fenêtre en vol bornée (RAM NAS) · N petit par défaut · ack = contrat « durable avant visible »

Les checks d’existence partent par lots de centaines d’adresses — la forme exacte de l’API est le point ouvert de la section B, le client est déjà conçu pour le batch. L’ack serveur suit le contrat de durabilité : ce qui est acquitté est sur disque.

Décision 4 — Throttling : token-bucket + plages horaires

En clair — le robinet Le backup partage le tuyau avec le travail de la journée. On lui met un robinet : filet d'eau aux heures de bureau, plein débit la nuit. Sans ça, la visio de 14 h rame, et le backup se fait désinstaller — la pire issue possible pour un outil dont la valeur dépend de sa régularité.

Limites montée/descente configurables, avec un planning (ex. 10 Mbit/s de 8 h à 19 h, illimité la nuit), implémentées en token-bucket côté client sur les flux d’upload. Le throttling adaptatif (céder la place quand le réseau est occupé) est noté pour plus tard — le planning couvre l’essentiel du besoin réel. Le rate-limiting côté serveur (quotas, protection d’abus) est un sujet distinct de la section B.

Décision 5 — Discipline de retry

  • Backoff exponentiel + jitter, par chunk — les échecs transitoires (réseau, 5xx) se retentent tout seuls.
  • Transitoire ≠ fatal — erreur d'authentification ou quota dépassé : arrêt franc avec diagnostic, pas une boucle de retry qui masque le problème.
  • Réveil de laptop = simple continuation — rien à faire.
  • Coupure longue — le run échoue proprement après une deadline, et le run planifié suivant est la reprise : même commande, pas de « mode reprise ». La dédup fait le reste.

Fichiers spéciaux & métadonnées étendues spec Décidé

Un fichier n'est pas que son contenu : propriétaires, cinq familles d'ACL incompatibles, xattrs, flags d'inode, flux annexes, liens, trous. Règle d'or : capturer fidèlement, restaurer au mieux, ne jamais mentir.

En clair — le déménageur consciencieux Un carton n'est pas que son contenu. Il porte une étiquette de propriétaire, une liste de qui a le droit de l'ouvrir, des post-its collés dessus, parfois un scellé « ne pas ouvrir ». Et certains « objets » ne sont même pas des cartons : le panneau indicateur (symlink — on déménage le panneau, pas ce qu'il pointe), le robinet dans le mur (device — on note qu'il y a un robinet, on ne déménage pas l'eau). Le déménageur qui perd les post-its en silence, on s'en aperçoit six mois plus tard — trop tard. D'où le bordereau : tout ce qui n'a pas été déménagé est écrit dessus.

La règle d’or

Capturer fidèlement · restaurer au mieux · ne jamais mentir Au backup : tout ce qui est lisible est capturé, en octets bruts (pas d'interprétation qui peut se tromper). À la restauration : tout ce que l'OS cible et les privilèges permettent est appliqué. Et tout ce qui a été sauté — dans un sens ou dans l'autre — apparaît dans le rapport, jamais en silence. Les outils de backup perdent la confiance de leurs utilisateurs une métadonnée à la fois ; le silence est le vrai bug.

L’inventaire par type de fichier

TypeAu backupÀ la restauration
Fichier ordinairecontenu (flux) + méta
Répertoireentrées + méta
Symlinkla cible (chaîne brute, jamais suivie) + métarecréé tel quel
Lien durgroupe dev+inode → contenu stocké une fois1ʳᵉ occurrence = contenu, suivantes = lien
Sparsetrous détectés (SEEK_HOLE) → entrées holetrous recréés, zéro octet stocké
Device (char/bloc)méta seulement (major/minor) — on ne lit jamais le contenurecréé si root, sinon rapporté
FIFOméta seulementrecréé
Socketignoré (artefact d’exécution)— signalé au scan

Deux lignes assumées : les devices (lire /dev/sda par accident serait catastrophique — on note le robinet, pas l’eau) et les sockets (rien à restaurer, mais dit dans le rapport, pas tu).

L’inventaire des attributs — exhaustif

Identité & droits — il n’y a pas une langue des permissions

En clair — les dialectes d'ACL On croit que « les permissions », c'est un seul système. En réalité il en existe cinq dialectes incompatibles, et la cible NAS de Cairn les croise tous : un partage Synology ne parle pas la même langue qu'un partage Windows ni qu'un QNAP récent. Un backup qui traduit d'un dialecte à l'autre trahit : la seule fidélité possible est de stocker chaque ACL dans sa langue d'origine, taguée par famille, et de la restaurer à l'identique sur un système du même type.
  • mode POSIX — rwx + setuid / setgid / sticky bit.
  • uid / gid + noms — les deux, pour le mapping à la restauration ([décidé](manifests.md)).
  • ACL POSIX (access + default) — Linux classique, via system.posix_acl_*.
  • ACL NFSv4 / richacl — macOS, ZFS (QNAP QuTS hero), montages NFS (system.nfs4_acl).
  • SynoACL — le système propriétaire de Synology (DSM monte ext4/btrfs avec l'option synoacl, sémantique façon Windows, stocké en xattrs dédiés, géré par synoacltool). Les outils standard (rsync…) le massacrent — le capturer fidèlement est un différenciateur pour la cible NAS.
  • NT ACL QNAP — QTS (ext4) maintient une émulation NT ACL propriétaire en plus des ACL POSIX ; les deux peuvent entrer en conflit à la migration.
  • Security descriptor Windows — owner SID, group SID, DACL, et SACL (règles d'audit — lecture soumise au privilège SeSecurityPrivilege : capturée si l'agent l'a, rapportée sinon).
  • Capabilities Linuxsecurity.capability (voir l'encart rouge).
  • Contexte SELinuxsecurity.selinux.

Temps

  • mtime, ctime — capturés en ns ; le ctime n'est pas restaurable (aucune API), on le garde pour l'audit.
  • birthtime / crtime — capturé (statx btime, natif Windows/macOS) ; restaurable sur Windows et macOS, pas d'API Linux → rapporté.
  • atime — optionnel (off par défaut : coûteux, peu utile, et le lire le modifie ailleurs).

Flags — trois mécanismes distincts

  • Flags d'inode Linux (chattr/lsattr) : immutable, append-only, nodump, noatime… — ce ne sont pas des xattrs : ioctl FS_IOC_GETFLAGS, un chemin de capture séparé. L'immutable exige CAP_LINUX_IMMUTABLE à la restauration.
  • Flags BSD/macOS (chflags) : uchg, schg, hidden, opaque… — la suite de tests BackupBouncer a montré que même restic en rate ; on vise la fidélité complète ici.
  • Attributs Windows : hidden, system, readonly, archive, compressed, encrypted, temporary, offline, not-content-indexed.

Contenus annexes & structure

  • xattrs, tous namespacesuser.*, security.*, system.*, trusted.* (root requis), com.apple.* (FinderInfo, tags/labels, quarantine…). Octets bruts, namespace inclus.
  • Flux annexes — ADS Windows, resource forks macOS : couverts par les [flux nommés](manifests.md) du modèle. Exemple d'ADS que tout le monde croise : Zone.Identifier, la marque « téléchargé d'Internet » qui déclenche l'avertissement de sécurité — restaurée fidèlement comme n'importe quel flux.
  • Extended Attributes NTFS ($EA) — le vrai jumeau des xattrs sous Windows, à ne pas confondre avec les ADS : petites paires clé→valeur (~64 Ko max par fichier), héritage OS/2, accessibles uniquement par l'API native (NtQueryEaFile — pas même une API Win32 normale). Quasi inutilisés… sauf par WSL : les distributions Linux y rangent uid/gid/mode ($LXUID, $LXGID, $LXMOD) et les xattrs Linux (LX.*). Les rater = restaurer un environnement WSL aux permissions détruites, silencieusement. Capturés.
  • Reparse points Windows — symlinks, junctions, mount points : capturés avec leur type exact (un junction n'est pas un symlink) ; les placeholders (OneDrive & co) ne sont jamais « hydratés » de force — capturés comme reparse, rapportés.
Le piège security.capability — le binaire silencieusement cassé Un binaire doté de capabilities (setcap — un ping sans setuid, un service qui ouvre un port < 1024) les porte dans l'xattr security.capability. Restauré sans cet attribut, le binaire est silencieusement cassé : il se lance, puis échoue à faire son travail, souvent des semaines plus tard. Même famille de piège que l'ACL Synology perdue par rsync : la métadonnée invisible dont l'absence ne se voit qu'à l'usage. C'est exactement ce que la règle du rapport interdit : si une capability ou une SynoACL n'a pas pu être restaurée, c'est écrit noir sur blanc.

Les cas retors — vérifiés, décidés

La compression transparente macOS — le piège du post-it qui ment macOS compresse certains fichiers de façon invisible : les données compressées vivent dans l'xattr com.apple.decmpfs (ou le resource fork), le flag BSD UF_COMPRESSED est posé, et toute lecture normale décompresse à la volée. Le piège mortel pour un backup « fidèle » : lire le contenu (déjà décompressé) et recopier bêtement l'xattr decmpfs + le flag → à la restauration, un fichier dont le post-it dit « je suis compressé » alors que le contenu ne l'est plus = fichier corrompu. Règle : contenu capturé décompressé, decmpfs/fork compressé/UF_COMPRESSED jamais restaurés tels quels (recompression optionnelle façon ditto --hfsCompression, plus tard). L'exception qui confirme la règle d'or : ici, la fidélité aveugle serait le bug.
  • Fichiers chiffrés par le FS — EFS Windows, fscrypt Linux. Lus normalement, ils livrent le clair (si l'agent a la clé) ou rien. EFS a une API dédiée de backup (ReadEncryptedFileRaw/WriteEncryptedFileRaw, privilège SeBackupPrivilege) : capture du chiffré + métadonnées $EFS sans détenir la clé — restaurable uniquement avec les clés d'origine, et dit tel quel dans le rapport. fscrypt n'a pas d'équivalent : contenu capturé si déverrouillé, sinon rapporté. Dans les deux cas, jamais de faux espoir silencieux.
  • Noms courts 8.3 Windows (FILENA~1.TXT) — des applications legacy et des entrées de registre y font référence. Capturés ; restauration best-effort (SetFileShortName, privilèges requis), rapportée sinon.
  • Object IDs NTFS ($OBJECT_ID, résolution des raccourcis par Distributed Link Tracking) — capturés, restaurés si possible.
  • Clones & reflinks (clonefile APFS, reflink btrfs/XFS) — deux fichiers partageant leurs blocs. La relation de clone n'est pas préservée (les fichiers redeviennent indépendants à la restauration — ils peuvent donc occuper plus de place localement) ; côté serveur, la dédup neutralise de toute façon le doublon. Assumé et documenté.
  • Project IDs (quotas projet XFS/ext4, via FS_IOC_FSGETXATTR) — capturés où lisibles, rapportés sinon.
  • Whiteouts overlayfs (couches Docker : char device 0:0 + xattrs trusted.overlay.*) — déjà couverts par la capture des devices et des xattrs trusted.* ; noté explicitement car un NAS qui héberge des conteneurs en est plein.

Privilèges — la dégradation propre

Agent non-root : certaines métadonnées sont illisibles (trusted.*, SACL, certaines ACL) → capturé ce qui peut l’être, le reste listé dans le rapport de backup. Restauration non-root : ownership, devices, flags immutables inapplicables → appliqués si possible, rapportés sinon. Jamais d’échec global pour une méta inapplicable, jamais de silence non plus. (La question « l’agent tourne-t-il en root/SYSTEM ? » appartient au sujet Forme.)

Restauration croisée — entre dialectes

Restaurer un backup Synology vers un Linux vanilla, ou un partage Windows vers un QNAP : le contenu passe toujours ; les métadonnées de la mauvaise famille sont conservées dans le snapshot (rien n’est perdu), non appliquées, et rapportées. Une traduction best-effort entre familles d’ACL (SynoACL → POSIX, richacl → DACL…) est envisageable plus tard comme option explicite — jamais silencieuse, la traduction fidèle n’existant pas. Le détail par plateforme (APIs, formats exacts) appartient au sujet Multi-plateforme.

Fichiers ouverts & verrouillés vss Décidé

Snapshot d'abord, sur chaque plateforme — VSS activé par défaut sous Windows, APFS sous macOS, btrfs/ZFS/LVM sous Linux — avec une échelle de dégradation propre, des hooks pour la cohérence applicative, et le niveau de cohérence réellement obtenu gravé dans chaque snapshot.

En clair — photographier une scène en mouvement Sauvegarder une machine vivante, c'est photographier une scène où tout bouge. Trois qualités de photo :

1. La photo prise pendant que tout bouge (lecture directe) — risque de flou : un fichier en cours d'écriture est capturé moitié ancienne version, moitié nouvelle.
2. Figer toute la scène au même instant (snapshot du système de fichiers) — la photo est nette et cohérente dans son ensemble : c'est exactement l'état qu'aurait laissé une coupure de courant. Les applications à journal (bases de données avec WAL) savent s'en remettre au démarrage. On dit crash-consistent.
3. Demander à tout le monde de prendre la pose (snapshot + quiescing) : les applications vident leurs tampons avant le cliché — la restauration ne nécessite aucune récupération. On dit app-consistent.

Cairn vise le niveau 2 partout par défaut, le niveau 3 là où c'est possible — et surtout, note sur chaque photo la qualité réellement obtenue.

Le problème — deux saveurs très différentes

  • Windows : le verrou. Les fichiers peuvent être verrouillés (sharing violations) — le PST Outlook ouvert, la base SQL : illisibles, point. Sans mécanisme dédié, le backup les saute entièrement.
  • Linux/macOS : la lecture déchirée. Pas de verrous obligatoires — tout se lit, mais un fichier en cours d'écriture donne une lecture déchirée (torn read) : la lecture « réussit », les données sont de la bouillie. Plus sournois que le verrou, car rien ne signale l'erreur.

Décision 1 — Snapshot d’abord, partout, avec échelle de dégradation

PlateformeMécanismeNotes
WindowsVSS, activé par défautL’agent est requester ; snapshot par volume ; restic ne le fait qu’en opt-in, on vise mieux
macOSSnapshot APFS (fs_snapshot_create)root + Full Disk Access requis
Linux — btrfsSnapshot de sous-volumeInstantané — cas Synology, mais root requis : en paquet non-root (nominal), c’est l’échelle de dégradation qui s’applique
Linux — ZFSSnapshot ZFSLe cas QNAP QuTS hero
Linux — LVMSnapshot LVMSi extents libres ; device-mapper gèle le FS automatiquement
Linux — ext4 nuaucun snapshot possible→ échelle de dégradation ci-dessous

Les fichiers sont lus depuis le snapshot (les chemins d’origine sont enregistrés, pas ceux du point de montage du snapshot), puis le snapshot est supprimé.

L'échelle de dégradation — jamais silencieuse Pas de snapshot possible (ext4 nu, VSS en échec) ? Lecture directe + le contrôle stat/re-stat du scan (N tentatives). Toujours instable ? Capturé quand même mais marqué « instable » dans le rapport — ou sauté, selon la config. Verrouillé Windows sans VSS ? Sauté et rapporté. La règle d'or s'applique telle quelle : dégrader oui, mentir jamais.

Décision 2 — La cohérence applicative : hooks autour de l’instant du snapshot

Sous Windows, les VSS writers font le travail : SQL Server, Exchange et tout l’écosystème Microsoft s’abonnent au freeze/thaw du service → app-consistency gratuite au moment du snapshot.

Sous Linux/macOS, pas de writers → hooks configurables pre-snapshot / post-snapshot : FLUSH TABLES WITH READ LOCK, quiesce applicatif, ou dump natif (pg_dump) vers un fichier qui sera sauvegardé.

En clair — on fige la pose, pas la journée Le point que tout le monde rate : les hooks encadrent l'instant du snapshot — quelques secondes — pas la durée du backup (des heures). La base de données prend la pose, clic, elle respire à nouveau ; le backup lit ensuite tranquillement la photo figée. Sans snapshot, il faudrait geler la base pendant tout l'upload — inacceptable, et c'est exactement pourquoi le snapshot est la fondation et pas une option.

Décision 3 — Le niveau de cohérence est une métadonnée du snapshot

Chaque racine de snapshot enregistre ce qui a été réellement obtenu : snapshot FS utilisé ou non (et lequel), hooks exécutés ou non, liste des fichiers instables ou sautés. À la restauration — et dans le dashboard — on sait si c’était app-consistent, crash-consistent ou best-effort. Prolongement direct de « ne jamais mentir » : la qualité de la photo est écrite au dos de la photo.

Décision 4 — Les échecs VSS sont la norme, pas l’exception

Les timeouts de writers VSS (fenêtre de freeze de 60 s, flush-and-hold de 10 s) sont le grand classique des backups Windows sur serveurs chargés (Exchange, SQL). Politique :

  • Dégrader + rapporter plutôt qu'échouer tout le backup — un poste de travail préfère un backup crash-consistent à pas de backup du tout.
  • Mode strict require-snapshot pour les serveurs où crash-consistent ne suffit pas : là, échec franc et alerte.
  • Hygiène des snapshots — suppression après backup, et détection des orphelins d'un run planté au démarrage suivant. Le snapshot LVM oublié qui se remplit jusqu'à figer le volume est un footgun célèbre ; un snapshot APFS/btrfs oublié épingle de l'espace disque en silence.

Deux renvois : tout ceci exige root/SYSTEM → sujet Forme (l’agent-daemon) ; et pour les VM, le proxy image-level de la section P contourne entièrement le problème — le snapshot se fait au niveau de l’hyperviseur, l’invité n’est même pas sollicité.

Multi-plateforme plat Décidé

Un seul moteur Rust, cinq carrosseries : binaires statiques musl cross-compilés, quatre écosystèmes de signature traités comme infrastructure de release, et une stratégie Synology assumée — non-root par dépôt tiers comme chemin nominal.

En clair — la valise autonome Un binaire « dynamique » classique emprunte ses outils au système qui l'héberge — et sur un NAS de 2018, la boîte à outils (la glibc) est vieille : le programme refuse de démarrer. Un binaire statique voyage avec sa propre valise : tout est dedans, il ne demande rien à personne. C'est le choix musl : le même exécutable tourne sur le NAS d'il y a huit ans et sur le serveur d'hier. Et comme tout Cairn est écrit en Rust, on fabrique ces valises pour chaque plateforme depuis le même code, en CI.

Décision 1 — Un binaire statique par cible

Cross-compilation Rust vers x86_64- et aarch64-unknown-linux-musl (outil cross) → binaires statiques, zéro dépendance glibc, mêmes fonctionnalités partout. Un seul codebase, pas de version « NAS » au rabais.

Décision 2 — La matrice de cibles v1

PlateformeFormatServiceSignature
Linuxbinaire statique + deb/rpmsystemdchecksums / sigstore
Windows x86_64MSIService Windows (SYSTEM)Authenticode EV — sinon SmartScreen effraie les clients
macOSPKG, binaire universel arm64+x86_64launchd daemonDeveloper ID + notarisation (obligatoire depuis 10.15)
SynologySPK (métadonnées DSM 7)init DSM, non-rootdépôt tiers — voir décision 3
QNAPQPKG (QDK)init QTSsignature QPKG (self-signed possible)

Architectures : x86_64 + aarch64 en v1 (couvre les NAS récents) ; armv7 (vieux modèles d’entrée de gamme) en best-effort plus tard.

Décision 3 — Synology : non-root par dépôt tiers, chemin nominal

Depuis DSM 7, un paquet ne peut tourner root que signé par Synology. Après analyse, ce n’est pas un problème — le chemin nominal est ailleurs :

  • Distribution par dépôt tiers — mécanisme officiel de DSM : l'utilisateur ajoute la source de paquets dans le Centre de paquets, le paquet apparaît dans l'onglet « Communauté » (DSM 7 affiche un avertissement « éditeur tiers » ; DSM 6 demande en plus de régler le niveau de confiance). Chemin éprouvé commercialement par des produits de backup existants : zéro gatekeeper, contrôle total du rythme de release.
  • Le non-root suffit pour le cas d'usage NAS. Le paquet reçoit l'accès aux dossiers partagés à sauvegarder — et qui peut lire un fichier peut lire ses xattrs : les SynoACL sont capturées sans root (vérifié empiriquement). Les snapshots btrfs ? Peu pertinents ici : un NAS est un serveur de fichiers, pas un serveur Exchange — l'échelle stat/re-stat couvre le fichier occasionnellement modifié, et les applis hébergées (Docker, bases) relèvent des hooks pre/post.
  • Ce qui reste réellement root-only — fichiers système DSM, xattrs trusted.*, création de snapshots — est marginal, et la règle « rapporter, jamais mentir » l'affiche proprement.
  • Signature / partenariat Synology : reclassé « seulement si un vrai besoin root émerge un jour ». Rien à engager maintenant.
  • Hors fondation : les contournements root (tâche planifiée, script sudo) — acceptables en dépannage documenté, jamais comme socle d'un produit : Synology peut fermer la porte à la prochaine mise à jour DSM.

Décision 4 — Les signatures comme infrastructure de release

En clair — quatre douanes, quatre passeports Chaque plateforme a sa douane : Apple exige un passeport (Developer ID) et un visa tamponné à chaque voyage (la notarisation de chaque build) ; Windows note la réputation du voyageur (SmartScreen — sans certificat EV, l'écran rouge fait fuir le client) ; Synology et QNAP ont leurs propres tampons. Quatre comptes, quatre certificats, quatre étapes de CI — à mettre en place dès le premier build public : rétrofitter la signature est toujours douloureux.

À traiter comme un composant du pipeline de release (section L) : certificats gérés comme des secrets d’infrastructure, notarisation (notarytool + stapler) et signatures automatisées en CI.

Décision 5 — Les privilèges par plateforme, consolidés

Tous les renvois accumulés (snapshots, xattrs) atterrissent ici :

PlateformeL’agent tourne enCe que ça permetCe qui manque sinon
Linux serveurrootsnapshots LVM/btrfs/ZFS, trusted.*, tout le FS
WindowsSYSTEMVSS, tous les fichiers, SACLsans SYSTEM : pas de VSS ni fichiers verrouillés
macOSroot + Full Disk Access (TCC)tout le FS — sans FDA, macOS cache Mail, Photos… même à rootFDA à accorder (déployable par MDM)
Synologyutilisateur de paquet (nominal)partages accordés + leurs SynoACL/xattrsfichiers système, trusted.*, snapshots — rapportés
QNAPselon QPKG (root possible)équivalent Linux si root

Périmètre

Les spécificités sémantiques des systèmes de fichiers — sensibilité à la casse, normalisation Unicode NFC/NFD, MAX_PATH Windows, noms réservés — restent au sujet dédié de la section O : cette page traite du packaging et du runtime, pas des chemins.

Config client cfg Décidé

La config vit côté serveur et se tire au début de chaque run — éditable depuis le dashboard web comme depuis la CLI, par la même API. En local : un bootstrap d'identité, et rien d'autre. Les hooks, eux, restent local-only.

En clair — le règlement est affiché à l'entrée, pas dans la poche L'employé ne trimballe pas une photocopie du règlement qui se périme dans sa poche : il lit le panneau à l'entrée en arrivant chaque matin. Le client Cairn pareil : à chaque backup, il tire sa config fraîche du serveur avant de commencer. Et l'astuce qui rend ça imparable : s'il ne peut pas joindre le serveur pour lire le panneau… il ne peut de toute façon pas travailler — le serveur est la destination du backup. Donc pas de copie locale à synchroniser, pas de config périmée, pas de conflit local/serveur : le problème de synchronisation que tous les agents se traînent disparaît par construction. C'est l'une des raisons d'être du choix stateless.

Décision 1 — Côté serveur, éditée par les deux bouts

La config vit dans la DB de contrôle (PostgreSQL), à côté des tenants et des refs de snapshots. Deux interfaces, une seule source de vérité :

  • le dashboard web (section H) l'édite via l'API ;
  • la CLI locale l'édite via la même APIcairn config set … parle au serveur, jamais à un fichier local qui divergerait.

Ce qui reste en local se réduit au bootstrap d’identité : URL du serveur, identité machine, credentials — le minimum pour frapper à la porte. Un petit fichier TOML, créé à l’enrôlement, et c’est tout.

Décision 2 — Le contenu, et surtout ce qui n’y est pas

Dedans :

  • Sources — les chemins à sauvegarder.
  • Include / exclude — sémantique gitignore (connue de tous), plus le respect de CACHEDIR.TAG, une taille max optionnelle, et le flag « ne pas traverser les frontières de systèmes de fichiers ».
  • Planification — horaires des runs, avec rattrapage (voir décision 5).
  • Réseau — throttling et plages horaires (uploads).
  • Politiquesfast/paranoid/force du scan, require-snapshot des fichiers ouverts, politique de collision de la restauration.

Explicitement hors config :

  • Les profils épinglés (chunker, compression, hashsplit) — les changer forke l'espace d'adresses : c'est une migration, pas un réglage. Ils vivent au niveau tenant, sous procédure dédiée.
  • Le matériel de clés — le canal config ne transporte jamais de secret. L'enrôlement des clés est le flux custody de la section F.

Décision 3 — Trois étages de politique, façon MDM

En clair — le bail et ses clauses Un bail de location a deux sortes de clauses : celles que le propriétaire impose (non négociables) et celles qu'il propose par défaut (aménageables par le locataire). La config Cairn pareil, sur trois étages : les défauts produit, la politique du revendeur, les réglages de la machine. Chaque champ posé par le revendeur est marqué locked (imposé — pense plages horaires ou rétention chez un client géré) ou default (la machine peut ajuster). C'est le modèle GPO/MDM éprouvé depuis vingt ans.

Se branche directement sur la hiérarchie de comptes (éditeur → revendeur → client, section G) et le RBAC (section I) : qui a le droit de verrouiller quoi.

Décision 4 — Les hooks sont local-only

La frontière données / code — à ne jamais franchir La config distante ne transporte que des données. Les hooks pre/post-snapshot sont des commandes : les rendre configurables depuis le dashboard transformerait celui-ci en exécution de code à distance sur toutes les machines d'un tenant — et un compte revendeur compromis en botnet clef en main. Règle : les hooks se déclarent dans le fichier local de la machine, par quelqu'un qui a déjà la main sur cette machine. Le dashboard peut voir qu'un hook est configuré (rapport), jamais le définir.

Décision 5 — La mécanique : polling, révision, accusé

  • Polling, pas de push — les clients sont derrière NAT : le daemon tire la config à intervalle modeste, au démarrage, et avant chaque run. (Un canal de commandes « backup maintenant » depuis le dashboard suivra la même mécanique — file de jobs pollée.)
  • Révision + accusé — chaque config porte un numéro de révision ; le client accuse réception. Le dashboard affiche à jour / en retard / injoignable depuis X — fini le « j'ai changé le réglage, est-ce pris en compte ? ».
  • Planification rattrapable (façon anacron) — le laptop éteint à 3h du matin fait son backup au réveil, au lieu de sauter la nuit. Pour un parc de portables, c'est la différence entre « planifié » et « réellement sauvegardé ».
  • Audit — chaque changement de config est journalisé : qui (revendeur ou client), quoi, quand. Alimente l'audit logging de la section I.

Sur le nom du dashboard — candidat dans le thème Cairn : la Table d’orientation (la vue panoramique au sommet, qui montre tout le paysage d’un coup d’œil). À trancher avec la section H.

Forme de l’agent form Décidé

Un daemon obligatoire, et trois bouches pour un seul cerveau : CLI, GUI locale (Tauri) et dashboard web parlent tous à l'agent, jamais à sa place. Mises à jour auto signées avec rollout progressif — et un heartbeat, parce que le silence est la pire panne d'un backup.

En clair — le gardien, l'interphone, l'écran du hall Le daemon est le gardien de l'immeuble : il habite sur place, détient les clés (credentials, privilèges), fait ses rondes aux heures prévues, et pointe auprès de la société de télésurveillance. La CLI est l'interphone pour lui parler ; la GUI locale, l'écran d'accueil dans le hall ; le dashboard web, la société de télésurveillance qui voit tous les immeubles. Aucun des trois ne fait de ronde lui-même : un seul cerveau, trois bouches.

Décision 1 — Un daemon + des clients IPC, un seul binaire

Le daemon est obligatoire — tout ce qui précède le suppose : planification rattrapable, polling de config, plages de throttling, requester VSS, heartbeat, et les privilèges de la table multi-plateforme.

  • Un seul binaire cairn (un artefact à signer par plateforme), sous-commandes : daemon, backup, restore, status, config, enroll
  • IPC local — la CLI et la GUI parlent au daemon via socket Unix (vérification des credentials du pair : qui est root, qui est admin) / named pipe Windows (security descriptor). C'est le daemon qui détient les credentials serveur et exécute ; les clients IPC ne font que demander.
  • Mode one-shot (cairn backup --oneshot sous cron) conservé pour serveurs minimalistes et debug.

Décision 2 — La GUI locale : Tauri, client léger du daemon

  • Stack : Tauri — backend Rust (réutilise les crates du projet : types IPC, client du daemon), UI web sur la webview système (WebView2 / WKWebView / WebKitGTK) : binaires de quelques Mo là où Electron en pèse 200, systray et notifications natives, installeurs alignés sur la matrice de packaging.
  • L'argument décisif : un seul kit UI, deux surfaces. L'UI Tauri étant du web, la GUI locale et la Table d'orientation (le dashboard, section H) partagent design system et composants — TypeScript + Svelte (affinité assumée ; léger, ce qui colle à l'esprit Tauri. Décision finale avec le sujet « stack front » de la section H). On ne construit pas deux produits visuels.
  • Périmètre — statut & progression (systray), pause / throttle, navigateur de restauration (parcourir les snapshots, sélectionner, restaurer), assistant d'enrôlement au premier lancement, notifications natives, affichage lecture seule des hooks locaux.
  • Client léger strict — la GUI parle à l'IPC du daemon, point : zéro logique métier, zéro credential serveur. Même API que la CLI.
  • Faiblesse assumée — WebKitGTK sur desktop Linux est la moins bonne des trois webviews ; la CLI reste complète en repli. Sur NAS : pas de GUI locale, l'UI du paquet DSM/QTS ouvre le dashboard web.

Décision 3 — Mises à jour auto : canaux natifs d’abord, self-update sinon

Un produit de flotte ne survit pas aux mises à jour manuelles — le revendeur avec 500 NAS n’ira jamais cliquer 500 fois.

  • Canaux natifs des plateformes quand ils existent — le dépôt SPK/QPKG (le Centre de paquets vérifie les mises à jour tout seul — infrastructure qu'on a déjà), dépôts apt/rpm.
  • Self-update intégré pour Windows et le binaire Linux nu : télécharger → vérifier la signature → swap atomique → health-check au redémarrage → rollback automatique si la nouvelle version ne tient pas.
  • Règles transverses — jamais de mise à jour pendant un backup (différée) ; canaux stable/beta ; rollout progressif piloté serveur (10 % → 100 %, arrêt si ça casse) ; la politique de mise à jour est un champ de config à trois étages (le revendeur peut verrouiller « auto : on ») ; chaque agent rapporte sa version → le dashboard voit la flotte.
Le canal de mise à jour est LA surface d'attaque Fais le calcul : l'updater a le droit de remplacer un binaire qui tourne root/SYSTEM sur toutes les machines de tous les clients de tous les revendeurs. C'est la leçon SolarWinds. Règles absolues : la signature est vérifiée par l'agent lui-même avant le swap — pas juste du TLS : compromettre le serveur de mise à jour ne doit pas suffire ; la clé de signature vit hors ligne, jamais sur l'infra de release ; et le rollout progressif sert de pare-feu de dernier recours. Chaque nouvelle version porte un sceau que l'ancienne vérifie avant de lui céder la place.

Décision 4 — La supervision du daemon : le silence est l’ennemi

En clair — le veilleur qui pointe Un backup qui échoue fait du bruit — une erreur, une alerte. Un agent mort ne fait rien du tout : pas de backup, pas d'erreur, rien. C'est la pire panne d'un produit de sauvegarde, celle qu'on découvre le jour de la restauration. La parade du gardien de nuit : pointer. Ce n'est pas l'absence d'alarme qui rassure, c'est le pointage régulier — et c'est son absence qui alerte.
  • Superviseur local — systemd (Restart=always + watchdog), launchd (KeepAlive), recovery actions du Service Windows, init DSM/QTS : l'agent planté est relancé.
  • Heartbeat — l'agent pointe régulièrement auprès du serveur, indépendamment de tout backup planifié. Le dashboard alerte « agent silencieux depuis X » — la machine débranchée, le daemon tué, le NAS mort : tout se voit, même quand rien ne devait tourner.
  • Diagnostic — logs locaux rotatifs, cairn status pour l'état complet en local ; la télémétrie d'exploitation relève de la section K.

Protocole & API B

La surface entre client et serveur, complète : HTTP/2 + CBOR, session-promesse, rehash à la porte, clés par machine, quotas exacts — et aucun endpoint de suppression.

  • TransportHTTP/2 + CBOR sur 443, TLS hybride X25519+ML-KEM-768 ; H3 en upgrade opportuniste ; gRPC écartéwire
  • API de vérification d'existence — l'oracle interneLots de 1 000 + bitmap ; « présent » = promesse épinglée à la session jusqu'au commit ; scoping tenantexist
  • API upload / download de chunksRehash à la porte, idempotent, deux classes d'endpoints, batch-get piloté serveurblob
  • Authentification client → serveurPaire de clés par machine + tokens courts ; enrôlement à code unique ; mTLS et clés statiques écartésauth
  • Sessions & auth stateless, quotas, rate limitingToken auto-porteur, quota exact par tenant, échec propre, 3 axes de rate limitinglim
  • Versionnage du protocoleAdditif d'abord, 12 mois de compat, les données survivent au protocolelim

Transport wire Décidé

HTTP/2 + CBOR sur le port 443, TLS hybride post-quantique — le colis postal standard, parce qu'on ne contrôle pas les routes. gRPC écarté (deuxième langue de sérialisation, fragile en réseau hostile), HTTP/3 noté comme upgrade opportuniste, TCP maison disqualifié.

En clair — on ne contrôle pas les routes L'agent est installé chez les autres : derrière leurs pare-feu, leurs proxies d'entreprise, leurs boîtiers d'inspection TLS, le wifi de l'hôtel. Le critère caché de toute la décision : le trafic doit ressembler à ce que toutes ces routes acceptent. HTTP/2 + CBOR, c'est le colis postal standard — tous les centres de tri du monde savent le traiter. gRPC, le coursier privé en uniforme — ultra-efficace entre deux entrepôts qu'on possède, mais le réceptionniste de l'immeuble refuse parfois l'uniforme (proxies qui cassent les trailers HTTP/2). QUIC, le drone — pas d'embouteillage, mais certains quartiers tirent à vue (UDP bloqué). TCP maison, construire sa propre route — chaque poste-frontière arrête les véhicules non immatriculés.

La décision

HTTP/2 + CBOR. Des endpoints HTTPS ordinaires (PUT /v1/chunks/{addr}, POST /v1/chunks/exists), les structures en CBOR, les chunks en octets bruts, le multiplexage H2 pour les flux parallèles.

  • Indistinguable du trafic web normal sur 443 — proxies d'entreprise, DPI, hôtels : tout passe, parce que c'est du HTTPS ordinaire. Pour un produit installé chez des PME qu'on ne contrôle pas, l'argument n°1.
  • Dégradation gracieuse — un boîtier qui force HTTP/1.1 ne casse rien : le protocole reste correct, juste moins multiplexé. gRPC, lui, casse net.
  • Une seule langue de sérialisation — CBOR partout : les nœuds, les requêtes, les réponses. Les chunks voyagent nus, zéro enveloppe.
  • Débuggable au curl, codes de statut standards, compatible avec chaque reverse-proxy / LB / WAF de la planète.
  • Le « contrat généré » de gRPC, on l'a autrement — client et serveur vivent dans le même workspace Rust : une crate de types partagés fait le même travail, sans générateur ni deuxième schéma.
  • TLS hybride post-quantique obligatoire — X25519 + ML-KEM-768 (exigence gravée).
  • Versionnage par préfixe /v1/ + un endpoint de capacités (le serveur annonce : taille max de batch, features, transports) — les vieux clients continuent de marcher.

HTTP/3 : upgrade opportuniste, pas fondation. Ses gains visent exactement notre charge (pas de head-of-line blocking sur liens avec pertes, migration de connexion wifi→4G en plein backup) — mais l’UDP est bloqué dans assez de réseaux d’entreprise pour que le fallback H2 soit obligatoire de toute façon. Deux transports = double matrice de tests : on l’ajoutera derrière l’endpoint de capacités le jour où le gain le justifie.

C’est quoi, CBOR — et pourquoi lui

En clair — du JSON binaire, normalisé Même modèle de données que JSON (objets, listes, nombres), mais encodé en octets compacts : {"taille": 512} fait 15 caractères en JSON, ~8 octets en CBOR. C'est une norme IETF (RFC 8949) — c'est par exemple le format interne des passkeys/WebAuthn. Deux atouts décisifs : les octets bruts sont natifs (nos adresses et clés de 32 o voyagent telles quelles — JSON les gonflerait de +33 % en base64), et surtout le mode canonique est défini par la norme (clés triées, encodages minimaux) — la raison de son choix initial : chez nous adresse = hash(octets), le même contenu doit produire les mêmes octets partout, pour toujours. Protobuf est explicitement non-déterministe, MessagePack n'a pas de profil canonique normalisé : CBOR avait le déterminisme déjà spécifié. Le réutiliser pour l'API = une seule langue dans tout le produit.

Et par rapport à un protocole TCP maison ?

En clair — zéro gain de sécurité, ~1 % de performance, dans le bruit Chiffrement : strictement identique. Un protocole maison utiliserait… TLS aussi — le même TLS 1.3, les mêmes suites, le même hybride ML-KEM. Il n'existe pas de « TLS plus fort » réservé aux protocoles custom. Et la sécurité des données ne vient pas du transport : les chunks sont chiffrés côté client avant de toucher le réseau — TLS n'est que la seconde enveloppe.

Performance : l'overhead HTTP/2 sur un chunk de 2 Mio ≈ 0,01 % (9 octets de frame + headers compressés HPACK). Les goulots réels sont des ordres de grandeur au-dessus : le WAN, le disque, le fsync, le CPU de chunking. Le mythe « custom = rapide » vient des domaines à millions de minuscules messages (trading, jeu) — pas du transfert de gros blobs, où le framing s'amortit. Et H2 fait même gagner : une connexion multiplexée au lieu d'une par flux (handshakes TLS économisés). Un TCP maison, c'est payer l'enfer des réseaux d'entreprise pour un 1 % qu'on ne verra jamais.

gRPC — pourquoi pas, en détail

  • Une deuxième langue de sérialisation — protobuf en plus de notre CBOR canonique : deux schémas, deux toolchains, pour toujours. Le contre le plus lourd.
  • Fragile en réseau hostile — exige HTTP/2 de bout en bout, trailers compris ; proxies et boîtiers d'inspection les cassent régulièrement → tickets de support indéboguables à distance.
  • Pas fait pour les gros colis — optimisé pour beaucoup de petits messages, pas des blobs de 8 Mio (limites de message, fragmentation à gérer soi-même).
  • Outillage dédié requis (grpcurl, reflection), grpc-web pour le navigateur, bizarreries de load-balancing.

L’écosystème Rust

RôleCrateNote
Runtime asynctokioLe standard de facto
HTTP/2 serveuraxum (sur hyper + tower)Middlewares tower : timeouts, rate-limit, retry
HTTP/2 clientreqwest (ou hyper direct)Pooling, multiplexage H2
TLSrustls + provider aws-lc-rsX25519MLKEM768 (hybride PQ) disponible
CBORminicborDéjà choisi pour les nœuds — encodeur canonique partagé
HTTP/3 (futur)quinn + h3Pour l’upgrade opportuniste, le jour venu

La stack tokio/hyper/axum/rustls est la plus éprouvée de l’écosystème Rust — c’est celle qui fait tourner une part significative de l’infrastructure web moderne.

API d’existence & sessions exist Décidé

La question qui fait exister la dédup — « as-tu déjà ce chunk ? » — posée par liasses de 1 000, répondue par checklist. Et la subtilité qui fait tout : « présent » n'est pas une information, c'est une promesse à durée de session.

À quoi elle sert

Avant chaque envoi, le client demande à l’entrepôt lesquels de ses cartons existent déjà — et n’expédie que les manquants. C’est la question qui matérialise la dédup et la reprise après coupure. Un backup d’1 To en pose ~500 000, d’où le format industriel :

POST /v1/chunks/exists # corps : tableau CBOR d'adresses (lots de ~1 000)
→ réponse : bitmap # 1 bit par adresse — 1 000 réponses = 125 octets

C’est aussi l’endroit où l’oracle de confirmation — accepté en principe côté crypto — devient un objet technique concret : la concevoir avec soin, c’est garantir qu’elle ne répond jamais plus que ce qu’on a décidé d’accepter.

Le piège mortel — la course avec l’équipe de ménage

Le film de 22h00 — la corruption silencieuse en quatre actes L'entrepôt a une équipe de ménage (le GC) qui jette les cartons que plus aucun inventaire ne réclame. 22h00 : tu téléphones — « vous avez le carton X ? » — « Oui ! ». Tu ne l'expédies donc pas. 22h05 : un vieil inventaire est supprimé (rétention) ; le carton X n'était réclamé que par lui. 23h00 : l'équipe de ménage passe — carton X, benne. 6h00 : ton déménagement se termine, tu déposes ton inventaire final… qui référence le carton X. Qui n'existe plus. Un backup qui a l'air complet, avec un trou dedans — découvert le jour de la restauration. Et chaque acteur a fait son travail correctement : c'est la fenêtre temporelle entre la réponse et le dépôt de l'inventaire qui tue.

La solution — le ticket de consigne

En clair — « présent » est une promesse, pas un renseignement Quand le vestiaire te dit « oui, ton manteau est là », il ne te donne pas une information — il te tend un ticket de consigne : le manteau ne bougera pas entre le dépôt et le retrait. Notre API pareil : chaque « présent » épingle le chunk à la session en cours. L'équipe de ménage ne touche jamais un carton autocollé.

La mécanique complète :

  • Ouverture — le backup commence par POST /v1/sessions : un bracelet numéroté à l'entrée. Toutes les requêtes du run le référencent.
  • Épinglage — chaque réponse « présent » colle l'autocollant « réservé — déménagement n°42 en cours » sur le chunk. Les chunks fraîchement uploadés pendant le run — eux non plus réclamés par aucun inventaire — naissent avec le même autocollant. Un seul mécanisme protège les deux cas.
  • CommitPOST /v1/sessions/{id}/commit avec la racine du snapshot : les autocollants tombent, c'est désormais l'inventaire lui-même qui réclame les cartons. Atomique — la page d'album entre dans l'album entière, ou pas du tout.
  • Expiration — le run meurt en route ? Le bracelet expire (TTL), les autocollants tombent, les chunks redeviennent collectables — aucune fuite de réservations fantômes. À la reprise, le run suivant ouvre une nouvelle session, redemande « présent ? », re-colle les autocollants : la reprise par dédup fonctionne inchangée.

Deux notes d’architecture :

  • Le client reste stateless — c'est le serveur qui tient la session, parfaitement dans le contrat : l'état vit chez le serveur, comme toujours.
  • C'est LE point de contact avec le GC (l'autre nœud dur, section C) : en concevant l'API comme une promesse-à-durée-de-session, on donne au futur GC exactement la primitive dont il aura besoin — les époques protégées. Le problème difficile est simplifié avant d'être attaqué.

Le scoping tenant — défense en profondeur

Deux tenants ne peuvent même pas nommer les cartons l’un de l’autre (CK différentes → adresses jamais identiques, par construction). L’API ajoute quand même un second verrou : elle ne répond que pour l’espace de dédup du tenant authentifié. Si les adresses du tenant B fuient un jour par un autre canal (machine compromise, logs), un compte du tenant A ne peut pas s’en servir comme sonde. L’isolation existe dans la crypto et dans le protocole — deux verrous indépendants.

Le tic-tac de l’horloge

« Absent » est répondu par le portier à mémoire floue (bloom, RAM, instantané), « présent » exige d’ouvrir le registre (LSM) : les temps de réponse diffèrent. Fuite ? Seul le tenant interroge son propre espace, et le chrono ne lui apprend rien de plus que la réponse elle-même. Nulle dans notre modèle de menace. Le rate limiting (un client compromis qui sonde en masse) relève de la prévention d’abus, section I.

Et côté serveur — comment on répond vite ?

Ce n’est pas le sujet de cette page, et c’est voulu : le contrat (ce que l’API promet) vit ici, la mécanique (comment le serveur tient la promesse) est déjà gravée dans l’architecture du stockage. Rappel en une ligne : bloom filter en RAM (écarte instantanément les absents — la majorité pendant un backup de données neuves) → index LSM (réponse authoritaire, lookup ponctuel pur sur des clés uniformément aléatoires — le travail idéal d’un LSM) → et jamais, jamais de « présent » sur la foi du bloom seul.


En une phrase : l’API d’existence n’est pas un moteur de recherche, c’est un vestiaire — elle ne dit jamais « oui » sans tendre le ticket qui garantit que le manteau sera encore là au moment du retrait.

API upload & download blob Décidé

Le videur rehache chaque colis à la porte — l'empoisonnement est impossible par construction. Deux familles d'endpoints par classe de segment, un batch-get piloté par le serveur qui réalise « les courses par rayon », et le protocole de données est complet.

Upload — le videur à la porte

En clair — personne n'entre sous un faux nom À la réception de chaque colis (PUT /v1/chunks/{addr}), le serveur rehache le corps et vérifie BLAKE3(corps) == addr avant d'indexer. Mismatch = rejet sec. Conséquence énorme : l'empoisonnement est impossible par construction — on ne peut pas ranger un mauvais contenu sous une bonne adresse, puisque l'adresse est le hash du contenu. Une machine compromise du tenant peut uploader des déchets (un problème de quota), jamais corrompre un chunk que les autres machines dédupliquent.

Les propriétés :

  • Idempotent — re-uploader un chunk existant = succès no-op. Livrer deux fois le même carton n'est pas une erreur : c'est la reprise qui fonctionne.
  • Durable avant ack — le contrat de durabilité, appliqué au protocole.
  • Taille max ≈ 8 Mio + marge (le max du chunker) — au-delà, rejet.
  • Parallélisme par multiplexing H2, fenêtre en vol bornée côté client (pipeline).

Deux familles d’endpoints, par classe. Le serveur doit savoir dans quelle caisse ranger (segments données vs métadonnées) : /v1/chunks/* → segments données (HDD) ; /v1/meta/* (manifestes, répertoires, racines) → segments métadonnées (SSD). Les manifestes — convergents — ont leur check d’existence (POST /v1/meta/exists) ; les répertoires et racines, jamais (toujours neufs par construction). Même videur, mêmes règles partout.

Download — la liste de courses remise au magasinier

En clair — le picking dans l'ordre des rayons La restauration promettait « les courses par rayon » : télécharger les 500 000 chunks dans l'ordre des segments pour des lectures quasi séquentielles. Mais le client ne connaît pas les emplacements — seul l'index serveur sait que le colis b291 est dans la caisse 42. La réalisation propre : le client remet sa liste de courses (POST /v1/chunks/batch-get, tableau d'adresses), et le magasinier fait le picking dans l'ordre de ses rayons — le serveur streame les blobs dans son ordre optimal (trié segment/offset), en flux framé [addr | len | payload]. C'est lui qui connaît l'entrepôt.

Les GET unitaires (GET /v1/chunks/{addr}, GET /v1/meta/{addr}) restent pour la navigation, l’arbre parent du scan (petits nœuds, SSD, cachés en RAM) et le debug.

Le point d’entrée des racines. Le client stateless doit trouver la racine du snapshot parent : GET /v1/snapshots — la liste des refs, servie depuis la DB de contrôle (l’équivalent de git branch). C’est le seul endpoint qui touche la DB de contrôle sur le chemin des données ; tout le reste est du CAS pur.

Le protocole de données, complet

EndpointRôleParticularité
POST /v1/sessions · …/commitouvrir / committer un backupsession-promesse, commit atomique
POST /v1/chunks/exists · /v1/meta/existsdédup par lotsbitmap, épinglage à la session
PUT /v1/chunks/{addr} · /v1/meta/{addr}uploadrehash à la porte, idempotent, durable avant ack
POST /v1/chunks/batch-getrestaurationpicking par rayon, flux framé
GET /v1/chunks/{addr} · /v1/meta/{addr}lecture unitairenavigation, arbre parent, debug
GET /v1/snapshotsrefs des racinesseul contact avec la DB de contrôle
GET /v1/capabilitiesnégociationtailles de batch, features, versions (transport)

Sept familles d’endpoints : toute la surface dont l’agent a besoin pour sauvegarder et restaurer. Config, heartbeat et jobs s’y ajoutent (même style, polling) ; l’authentification est le sujet suivant. Et remarque ce qui n’existe pas : aucun endpoint de suppression — le client n’a pas la gomme.

Authentification auth Décidé

Une paire de clés par machine, née à l'enrôlement, qui ne quitte jamais le disque — et des tickets courts pour travailler. Le secret durable ne voyage jamais ; ce qui voyage expire vite. mTLS et clés d'API statiques écartés.

Deux populations très différentes

  • Les machines — le cas central : des milliers d'agents par revendeur, sans humain devant. Identité longue durée, révocation indispensable (machine volée, décommissionnée). C'est le sujet de cette page.
  • Les humains — admins revendeur, clients finaux : sur le dashboard, avec SSO/OIDC + MFA (sections H/I, RBAC). Frontière nette : la CLI et la GUI locales parlent au daemon (IPC), qui porte l'identité machine ; ce qui dépasse les droits de la machine (déverrouiller un champ locked de la config…) se fait sur le dashboard, avec une identité humaine.

Les options écartées

mTLS — le sceau de cire qui ne survit pas à la réouverture Le certificat client mTLS prouve l'expéditeur… tant que personne ne rouvre l'enveloppe. Or les proxies d'inspection TLS des entreprises ouvrent et recachettent chaque enveloppe (MITM assumé) : le sceau du client est détruit au passage, l'authentification casse — précisément dans les réseaux que le transport a été conçu pour traverser. mTLS reste excellent entre nos services internes ; côté agents installés chez les autres, c'est une source de tickets sans fin. Écarté pour les agents. Les clés d'API statiques, elles, font transiter un secret éternel à chaque requête — une fuite = accès complet jusqu'à révocation manuelle. Le modèle des années 2010. Écarté aussi.

La décision — la carte d’identité et le ticket journalier

En clair — rien de durable ne voyage, rien de volé ne dure À l'enrôlement, la machine fabrique sa carte d'identité : une paire de clés Ed25519 — la privée ne quitte jamais la machine. Pour travailler, elle se présente au guichet, prouve son identité en signant un défi, et reçoit un ticket journalier (token court, ~1 h) qu'elle joint à chaque requête. Un ticket qui fuite ? Il expire tout seul. On décommissionne la machine ? On invalide la carte côté serveur — plus aucun ticket ne sera émis. C'est le modèle moderne (AWS SigV4, OAuth client assertion), en headers standards : passe tous les proxies.

Note post-quantique : ces signatures sont du temps réel (pas de harvest-now-decrypt-later possible) — Ed25519 suffit, hybridable plus tard comme les signatures d’update.

L’enrôlement — le code d’invitation à usage unique

# attacher une machine à un tenant
1. dashboard / revendeur → token d'enrôlement # usage unique, durée courte
2. cairn enroll <serveur> <code> # l'agent fabrique sa paire de clés
3. agent → serveur : code + clé publique + métadonnées # hostname, OS — jamais la clé privée
4. serveur : consomme le code, enregistre la machine sous le tenant du code

Le bootstrap TOML (config) est écrit ; la clé privée va dans le keystore OS quand il existe (Keychain, DPAPI), en fichier à permissions strictes sinon (NAS). Le provisioning du matériel de chiffrement (CK, CMK) chevauche ce moment mais roule sur son propre rail — le flux custody de la section F : le canal d’authentification ne transporte jamais de secret de chiffrement.

Ce que le ticket porte — l’autorisation

Les claims du token : tenant, machine, scopes. C’est ici que le scoping de l’API d’existence se matérialise — le serveur lit le tenant dans le token, jamais dans la requête. Scopes machine : écrire/lire le CAS de son tenant, lire sa config, heartbeat, sessions. Pas de scope de suppression — l’endpoint n’existe pas. Les quotas et le rate limiting s’accrochent aux mêmes claims (par machine, par tenant) — sujet suivant de la section B.

Ce que font les gros — vérifié

Veeam vs Acronis — deux époques Veeam Cloud Connect (le produit le plus proche de notre modèle revendeur) : des comptes username/password statiques par tenant (format TENANT\SOUS-TENANT), saisis dans l'agent, rotation manuelle — et un port TCP custom 6180 via leur « cloud gateway » : littéralement le modèle « construire sa propre route » qu'on a disqualifié, avec les tickets de pare-feu qui vont avec. Un modèle antérieur au zero-trust, tenu par la position de marché.

Acronis Cyber Protect Cloud : token d'enrôlement à l'installation (qui passe l'identité sans stocker les credentials), identité machine gérée, access tokens style OAuth2 sur API REST en HTTPS standard — notre modèle, à deux crans près : chez nous le secret ne transite jamais (seule la clé publique voyage) et les codes d'enrôlement sont strictement à usage unique.

L'argument commercial en prime : « aucun port à ouvrir dans votre pare-feu » (443 standard) face au 6180 de Veeam — un vrai différenciateur terrain pour les revendeurs qui déploient chez des PME.

Quotas, limites & versionnage lim Décidé

L'auth est stateless (le ticket est auto-porteur), seul le backup a une session. Les quotas se comptent exactement par tenant, les dépassements échouent proprement, et les évolutions sont additives — les données survivent au protocole.

L’auth est stateless — seul le backup a une session

En clair — le contrôleur lit le billet sans appeler la billetterie Le ticket journalier est auto-porteur : ses claims (tenant, machine, scopes) sont signés dedans — le serveur le valide sans consulter la moindre base à chaque requête. Il n'y a donc aucune session d'authentification côté serveur. La seule vraie session est la session de backup — parce qu'elle seule a un état réel à tenir (les épinglages). Jolie symétrie : de l'état là où il y en a, nulle part ailleurs.

Décision 1 — Le quota se compte en octets stockés, par tenant

Un détail élégant gagné par construction : la portée de dédup = le tenant → chaque chunk appartient à exactement un tenant. La somme des chunks du tenant est donc un compteur exact, trivial à tenir à l’ingest — pas d’ambiguïté « qui paie le chunk partagé ? » au niveau de l’enforcement (la question tarifaire entre les clients d’un même revendeur reste un sujet de facturation, section G). Sous-quotas par machine possibles (allocation du revendeur, via la config à trois étages).

Décision 2 — Seuils doux, arrêt net, jamais de casse

En clair — pas de cartons éventrés dans le couloir Avertissements à 80 et 90 % (dashboard + rapport). Au plafond dur : uploads rejetés avec une erreur explicite → le backup échoue proprement (erreur fatale, pas de boucle de retry — la discipline existante). Et la beauté du système de sessions : un backup stoppé par le quota ne laisse aucun débris — pas de racine committée (le snapshot n'existe pas à moitié), et les chunks déjà montés redeviennent collectables à l'expiration de la session. Le déménagement interrompu ne laisse pas de cartons éventrés dans le couloir : ils repartent à la benne, proprement.

Décision 3 — Rate limiting sur trois axes, accroché aux claims

  • Checks d'existence — plafond par machine. Un backup légitime fait peu de requêtes (lots de 1 000) → un plafond bas attrape le client compromis qui sonde l'oracle en masse. Le sujet « prévention d'abus » de la section I reçoit ici son mécanisme.
  • Bande passante serveur — token-bucket par tenant, côté serveur : l'équité entre tenants (un client qui sature n'affame pas les autres). Complète le throttling client — lui protège le réseau du client, ici on protège le serveur.
  • Enrôlement — anti-brute-force sur les codes (déjà courts et à usage unique : ceinture et bretelles).

Réponses standard : 429 + Retry-After — le backoff client existe déjà.

Décision 4 — Une seule session de backup active par machine

Deux backups simultanés de la même machine n’ont aucun sens — le second reçoit un 409. Les restaurations, elles, sont libres. Zéro coût, et ça évite toute interaction étrange entre épinglages et commits concurrents.

Versionnage — additif d’abord

  • Décision 5 — Additif par défaut. CBOR à clés entières + la règle « les champs inconnus sont ignorés » = la quasi-totalité des évolutions sont additives (nouveau champ optionnel avec défaut, nouvel endpoint), sans changer de version. Le /v2/ n'apparaît que pour un vrai changement de sémantique — et coexiste avec /v1/ pendant la fenêtre de migration.
  • Décision 6 — La politique de flotte. Les agents se mettent à jour en vague (rollout progressif) → le serveur parle toujours à des vieux clients. Compatibilité garantie 12 mois glissants ; au-delà, erreur explicite « client trop ancien, mise à jour requise » — que l'auto-update rend rarissime. La négociation fine (tailles de batch, features H3…) passe par l'endpoint de capacités.
Décision 7 — Les données survivent au protocole La version du protocole est indépendante de la version du format des nœuds (l'octet de version CBOR gravé dans chaque nœud). Un snapshot écrit par un client v1 reste lisible pour toujours, quel que soit le protocole du jour. Le protocole est un tuyau qu'on peut remplacer ; les données sont un patrimoine qu'on ne renégocie jamais. C'est le sujet « format sur disque & versionnage » de la section O, vu depuis le réseau.

Backend / services serveur C

Les services internes — ingest, index, métadonnées, GC. Le garbage collection inter-clients est la partie réellement difficile.

  • IngestRéception des chunks — validation, stockage atomique
  • Service d'indexhash → emplacement (segment, offset, len)
  • Service métadonnéesManifestes, snapshots, racines — séparé de l'index chunks
  • Coordination de dédup côté serveur
  • Garbage collection inter-clients — le plus durMark-and-sweep par tenant, bloom côté sortie, quarantaine, sans verrougc
  • Comptage de références / mark-and-sweepRefcounting écarté (fragile) ; mark-and-sweep auto-réparant, époques = sessionsgc
  • Isolation multi-tenant (application effective)
  • AutorisationQui lit / écrit quoi — rôles, scopes
  • Files de jobs asynchronesGC, compaction, vérification — orchestration
  • Scrubbing d'intégrité en tâche de fondDétection bit rot, re-vérification périodique
  • Lifetimes Rust pour les blobs — piste de rechercheGarantir des invariants du GC (épinglage, quarantaine) par le système de types Rust — à explorer

Garbage collection inter-clients gc Décidé

Mark-and-sweep par tenant, auto-réparant, sans jamais un verrou exclusif — le magasin reste ouvert pendant l'inventaire. Le bloom filter revient côté sortie, où ses erreurs tombent du côté sûr, et rien n'est détruit sans quarantaine.

Le problème en une phrase : on ne peut supprimer un chunk que si plus personne ne le référence — or tout le monde le partage, et des backups tournent pendant qu’on compte. Les voisins y ont tous laissé des plumes : restic a historiquement exigé un verrou exclusif pour le prune (le dépôt ferme, les backups attendent), borg maintient des caches de comptage qui se désynchronisent, kopia s’appuie sur des marges horloge sensibles au clock skew. Mais Cairn arrive au combat armé : les racines sont recensées, les sessions épinglent déjà tout backup en cours, l’index a son champ époque, les segments métadonnées sont séparés sur SSD, et la dédup est scopée au tenant.

Décision 1 — Mark-and-sweep, pas de comptage de références

En clair — l'inventaire tournant, pas les tickets de caisse Le comptage de références, c'est additionner les tickets au fil de l'eau : chaque upload incrémente, chaque prune décrémente. Une seule erreur de caisse — un crash entre deux écritures, un bug — et le total est faux pour toujours : c'est la plaie des caches borg. L'inventaire tournant, lui, ne fait jamais confiance aux totaux d'hier : il recompte depuis les registres officiels (les racines) à chaque passage. Un bug du GC ? Le cycle suivant le corrige tout seul. Auto-réparant par construction — et zéro coût sur le chemin chaud : l'ingest n'entretient aucun compteur.

Décision 2 — Par tenant : l’avantage structurel

Chaque chunk appartient à exactement un tenant (dédup scopée) → le GC tourne tenant par tenant : travail borné, parallélisable, ordonnancé en tournée (chaque tenant tous les N jours, ou déclenché par le volume pruné). Jamais de « stop the world » global. restic, borg et kopia sont des outils mono-dépôt — ils ne peuvent pas faire ça ; c’est l’architecture multi-tenant qui l’offre.

Décision 3 — Le protocole anti-course : l’horizon d’époque

La règle d'or — dans un seul sens Faussement vivant un cycle de plus = acceptable. Faussement mort = jamais. Toutes les courses se résolvent par cette asymétrie : un backup qui référence un vieux chunk pendant le GC ? Impossible — le « présent » de l'API d'existence l'a épinglé (le ticket de consigne, conçu pour exactement ça). Un snapshot qui committe pendant le marquage ? Ses chunks — fraîchement uploadés ou épinglés — portent tous une époque récente. La règle qui unifie : le sweep ne condamne que les chunks hors de l'ensemble vivant ET dont l'époque < horizon, où l'horizon = le début de la plus ancienne session active. Tout ce qui a bougé récemment est intouchable par principe.

Décision 4 — Le bloom filter, côté sortie

En clair — le même portier, à l'autre porte L'ensemble vivant d'un gros tenant = 50 M d'adresses × 32 o = 1,6 Go. Solution : un bloom filter — et savoure le retournement. À l'entrée (API d'existence), on a interdit au portier à mémoire floue de dire « présent » : son faux positif y causait une perte de données. À la sortie (le GC), son erreur change de sens : un faux positif marque un chunk mort comme vivant → il reste un cycle de plus — parfaitement sûr, et le cycle suivant (bloom re-semé, faux positifs ailleurs) le ramasse. Et un bloom n'a jamais de faux négatif → un chunk vivant ne sera jamais jugé mort. Le même outil, exactement inversé : ses erreurs tombent du côté sûr. L'ensemble vivant tient en ~60 Mo de RAM.

Le marquage parcourt le DAG depuis les racines — métadonnées SSD uniquement (la séparation des segments paie ici), sous-arbres partagés visités une fois — et remplit le bloom. Le sweep itère l’index du tenant et condamne ce qui n’y est pas (et dont l’époque < horizon).

Décision 5 — Quarantaine, puis compaction gloutonne

  • La benne fermée à clé. Le sweep ne détruit rien : il marque « condamné à l'époque E ». La destruction physique n'arrive qu'à la compaction, après confirmation par un cycle suivant + délai de grâce (72 h) — fenêtre d'undelete, protection contre le bug du GC lui-même et l'erreur d'opérateur. Grâce en époques logiques, pas en horloge murale (la faiblesse de kopia).
  • On ne réorganise que les caisses aux trois-quarts vides. Chaque segment a son taux de vivants ; la compaction prend les pires d'abord (le plus d'octets récupérés par octet réécrit — le glouton de la littérature log-structured), recopie les survivants dans un segment neuf, bascule l'index, supprime l'ancien. Seuil ~50 % ajustable ; les segments froids attendent d'être très morts. Ça referme le point laissé ouvert par la section D.

Décision 6 — Jamais de verrou exclusif, jamais

En clair — le magasin reste ouvert pendant l'inventaire Chez les concurrents, le dépôt ferme pendant le prune (restic : verrou exclusif, les backups attendent des heures). Chez nous : les bracelets de session protègent les clients en rayon, l'horizon d'époque protège l'inventaire des mouvements en cours — prune, mark, sweep et compaction tournent pendant les backups et les restaurations, avec throttling I/O pour ne pas affamer la production. C'est l'anti-restic — et un argument commercial de plus.

Le cycle complet

Le cycle du GC — par tenant, en tâche de fond, sans verrou

SESSIONS ACTIVES — épinglages · HORIZON D'ÉPOQUE : ce qui a bougé récemment est intouchable PRUNE la rétention retire des refs de snapshots (jamais le client) MARK racines → DAG → bloom ~60 Mo métadonnées SSD seules SWEEP condamne si hors bloom ET époque < horizon ne détruit rien COMPACT segments < 50 % vivants les pires d'abord après grâce (72 h) quarantaine cycle suivant — recompte depuis les racines : auto-réparant PAR TENANT · EN TOURNÉE · SANS VERROU EXCLUSIF backups et restaurations continuent pendant tout le cycle

Quatre étapes par tenant, en tâche de fond. Le sweep condamne, ne détruit pas — la destruction n'arrive qu'à la compaction, un cycle et une grâce plus tard. Et tout recommence depuis les racines : le GC ne fait jamais confiance à sa mémoire.

Les chiffres qui rassurent : tenant de 100 To (50 M chunks) → marquage = lecture streaming de dizaines de Go de métadonnées SSD (~minutes), sweep = scan de ~2,5 Go d’index (~minutes), bloom 60 Mo en RAM. Le GC d’un gros tenant est une affaire de minutes par cycle, en tâche de fond.

Stockage D

Segments append-only (source de vérité), index LSM reconstructible, DB de contrôle minuscule. Les blobs ne passent jamais par le LSM.

  • Blobs : empaquetage en segments append-onlyClasses séparées données (HDD) / métadonnées (SSD)cas
  • Index LSM — choix d'architectureSegments + index hash→(seg,offset,len) ; value-log écarté. Index reconstructible par scan des segmentscas
  • Backends pluggablesDisque local, objet S3-compatible, cloud (GCS, Azure Blob…) — 1 segment = 1 objet
  • Compaction, tiering chaud/froid, cycle de vieTiering décidé ; compaction gloutonne par taux de vivants, après quarantaine — réglée avec le GCgc
  • DurabilitéContrat d'écriture décidé (durable avant visible) ; réplication, erasure coding, sharding à traitercas
  • Architecture physiqueServeurs, châssis disques, SSD méta / HDD données, réseau, dimensionnement

Architecture du stockage cas Décidé

Trois couches : des segments append-only qui sont la source de vérité, un index LSM reconstructible qui n'est qu'un accélérateur, et une petite DB de contrôle — le seul état irremplaçable. Les blobs ne passent jamais par le LSM.

En clair — l'entrepôt, le fichier de fiches, le registre Imagine un entrepôt. Les segments, ce sont de grandes caisses scellées où l'on range les colis les uns à la suite des autres — on ne rouvre jamais une caisse pleine pour la réorganiser. Le fichier de fiches (l'index), c'est le meuble à tiroirs qui dit « le colis n°b291 est dans la caisse 42, position 17 » : il fait gagner du temps, mais si on le perdait, on pourrait le reconstituer en ouvrant les caisses — chaque colis porte son étiquette. Le registre du bureau (la DB de contrôle), lui, contient ce qu'aucune caisse ne contient : qui sont les clients, quels réglages ils utilisent, la liste de leurs sauvegardes. C'est le seul document qu'il ne faut jamais perdre — et il est tout petit.

Ce qu’on doit stocker — quatre classes d’objets

Les décisions du modèle de manifestes donnent quatre classes aux profils très différents :

ObjetTailleConvergent ?Check d’existence ?Lecture typique
Chunks0,5–8 Mioouioui (dédup)restauration
Manifestes80 o – 100 Kioouioui (dédup)restauration, GC
Répertoires / racinespetitsnonnon (jamais dédupliqués)début de backup, navigation, GC
État mutable (tenants, profils épinglés, refs de snapshots, époques GC, quotas)minusculetransactionnel

Les trois premières classes sont immuables et adressées par contenu → elles vont dans un magasin adressé par contenu (CAS). La quatrième est mutable et transactionnelle → une vraie base de données. On ne mélange jamais les deux.

Les trois couches

# de haut en bas
DB de contrôle (PostgreSQL) # tenants, profils épinglés, refs de snapshots, époques GC
Index CAS (LSM) # addr → (segment, offset, len, classe, époque)
Segments append-only (disque) # les blobs eux-mêmes, scellés une fois pleins

Les trois couches — chemin d'écriture & hiérarchie de vérité

DB DE CONTRÔLE · POSTGRESQL tenants · profils épinglés · refs de snapshots · époques GC · quotas mutable · transactionnel · ne voit jamais un blob SEUL ÉTAT IRREMPLAÇABLE minuscule → répliqué (HA) INDEX CAS · LSM addr → (segment, offset, len, classe, époque) fiches de ~50 o · blooms par SSTable · 100 To ≈ 2,5 Go ACCÉLÉRATEUR DÉRIVÉ jetable SEGMENTS APPEND-ONLY écrits une fois · scellés pleins · ~256 Mio–1 Gio MÉTADONNÉES · SSD manifestes · répertoires · racines petits, cache RAM agressif DONNÉES · HDD chunks 0,5–8 Mio gros, écrits séquentiellement [ addr | len | payload ] — chaque entrée se revérifie en rehachant SOURCE DE VÉRITÉ ÉCRITURE PUT chunk ① append ② fsync ③ index ④ ack après durabilité index perdu ? scan des caisses → reconstruit

L'écriture descend (durable avant visible : append, fsync, puis seulement l'index et l'ack). L'index ne fait qu'accélérer — perdu, un scan des segments le reconstruit. La DB de contrôle ne voit jamais un blob — et c'est le seul étage qu'on ne sait pas reconstruire, d'où : minuscule, répliqué.

La hiérarchie de vérité Chaque entrée d'un segment est encadrée [addr | len | payload] : on peut reconstruire l'index entier en scannant les segments, et revérifier chaque adresse en rehachant. L'index LSM n'est donc qu'un accélérateur dérivé, jetable. La DB de contrôle est le seul état irremplaçable — et comme elle est minuscule, la répliquer et la sauvegarder est trivial (le point « méta-durabilité » de la section E). C'est PostgreSQL : la haute disponibilité du plan de contrôle est une exigence, et sa réplication et son failover sont natifs et éprouvés (section J). restic a cette propriété — index reconstructible depuis les packs — et elle l'a sauvé de nombreux bugs.

Décision — segments + index, pas de blobs dans le LSM

C’était l’embranchement laissé ouvert aux points ouverts : segments avec index séparé, ou LSM à value-log (WiscKey / BlobDB) tenant les blobs directement. La décision chunks de 2 Mio le tranche.

En clair — on ne déménage pas les pianos Un LSM range ses données en strates qu'il réécrit régulièrement pour rester rapide (la « compaction » — imagine un déménageur qui réorganise les étagères en permanence). Réorganiser des fiches de 50 octets, très bien. Réorganiser des pianos de 2 Mio, encore et encore : c'est de l'amplification d'écriture massive, le disque passe sa vie à recopier les mêmes gros blobs. Les moteurs « value-log » (WiscKey, BlobDB) évitent ça en… rangeant les gros objets à part dans un journal append-only — c'est-à-dire en réinventant nos segments à l'intérieur du moteur de quelqu'un d'autre, avec moins de contrôle sur le GC.

Donc : les blobs vont dans des segments append-only (~256 Mio–1 Gio, scellés une fois pleins), le LSM n’indexe que des fiches de ~50 octets (addr → segment, offset, len). Le LSM est parfait pour ce travail : écritures massives pendant les backups, lookups ponctuels, clés uniformément aléatoires (des hashes — donc jamais de range scan). Ordre de grandeur : 100 To stockés ≈ 50 M de chunks ≈ 2,5 Go d’index. Dérisoire.

Segments données vs segments métadonnées

Les manifestes vont dans le même CAS que les chunks — convergents, immuables, checkés et dédupliqués pareil — mais pas dans les mêmes caisses :

  • Segments données — les chunks. Gros, écrits séquentiellement, peuvent vivre sur HDD.
  • Segments métadonnées — manifestes, répertoires, racines. Entrées petites, empaquetées ensemble, sur SSD, cachées agressivement en RAM.
Pourquoi séparer — le catalogue reste près du comptoir Trois chemins chauds ne lisent que des métadonnées : le début de chaque backup (le client stateless télécharge l'arbre du snapshot parent — racine + nœuds répertoire — pour détecter les changements), la navigation / restauration partielle (descendre le DAG avant de toucher un seul chunk), et la phase mark du GC (parcourir tous les arbres). Si les manifestes étaient éparpillés au milieu de caisses de chunks de 2 Mio, chacun de ces parcours paierait des seeks à travers des téraoctets de données froides. Une bibliothèque garde son catalogue près du comptoir, pas dispersé dans les rayonnages. Précédent direct : restic sépare tree packs et data packs, kopia préfixe q (métadonnées) vs p (données).

Les répertoires et racines (non convergents) vont aussi dans le CAS — même adressage, même GC, même vérification d’intégrité — simplement sans check d’existence : ils sont toujours neufs par construction. Les racines sont en plus référencées depuis la DB de contrôle (la liste des snapshots d’un tenant — l’équivalent des refs de Git), et ce sont les racines du GC.

Bloom filters — accélérateur, jamais oracle

En clair — le portier à la mémoire floue Un bloom filter, c'est un portier qui a une mémoire compacte mais floue. Il sait dire deux choses : « celui-là, je ne l'ai jamais vu, certain » (toujours vrai), ou « il me dit quelque chose… » (parfois faux — c'est le faux positif). On ne lui demande jamais de confirmer une présence, seulement d'écarter vite les absents.
Le piège de correction — à ne jamais franchir Si le serveur répondait « présent » sur la foi du bloom seul, un faux positif ferait sauter au client l'upload d'un chunk que le serveur n'a pas. Résultat : corruption silencieuse, découverte le jour de la restauration — le pire scénario possible pour un système de sauvegarde. La règle absolue : le bloom n'a le droit de répondre que « absent, c'est sûr » (fast path : upload). Toute réponse « présent » vient de l'index authoritaire. Un faux positif ne coûte alors qu'un lookup LSM inutile — jamais la correction.

Deux étages utiles : les blooms par SSTable du LSM (pendant un backup de données neuves, la majorité des checks sont des miss — sans bloom, chaque miss toucherait tous les niveaux du LSM), et éventuellement un bloom global en RAM devant l’API d’existence pour répondre « absent » sans toucher le disque.

Le contrat de durabilité

# chemin d'écriture d'un blob — l'ordre est vital
append au segment ouvert → fsync (groupé) → insertion index → ack au client
« Présent » est une promesse La dédup transforme chaque réponse « présent » en promesse de durabilité : le client, sur cette foi, ne réuploadera pas le chunk. Si le serveur répond « présent » (ou ack « stocké ») avant que le blob soit réellement sur disque, un crash au mauvais moment = un chunk que tout le monde croit sauvegardé et que personne n'a. D'où l'ordre : durable d'abord, visible ensuite. Le fsync groupé (plusieurs blobs par sync) garde le débit.

Ce qui reste ouvert

  • Moteur LSM concret — RocksDB (éprouvé, blooms intégrés, dépendance C++) vs pur Rust (fjall). Choix d'implémentation, pas d'architecture.
  • Taille des segments et seuils de compaction — liés à la mécanique du GC (un segment dont le taux d'entrées vivantes tombe sous un seuil est réécrit ; les époques protègent les uploads en cours). Se décide avec le sujet GC.
  • Backends objet (S3 & co) — les segments s'y mappent naturellement (1 segment = 1 objet), mais le multi-backend reste le sujet « backends pluggables » de la section D.
  • La forme de l'API d'existence — batch, latence, ce qu'elle révèle : toujours le point ouvert de la section B. Le stockage lui donne son socle : bloom → LSM → réponse authoritaire.

Bases de données & métadonnées E

La DB de contrôle — distincte de l'index LSM — gère comptes, tenants, snapshots, facturation. Et sa propre durabilité.

  • DB de contrôle : PostgreSQLLa HA est une exigence → réplication et failover natifs. Tenants, profils épinglés, refs de snapshots, époques GC, quotas — le seul état irremplaçable, jamais l'index chunkscas
  • Schéma & migrationsStratégie zero-downtime, compatibilité avant/arrière
  • Méta-durabilitéSauvegarder la DB du système de sauvegarde lui-même

Cryptographie & clés F

Le cœur du système. Architecture complète décidée — chiffrement convergent, hiérarchie CK/CMK/KEK, custody managé + ZK, coffre OpenBao souverain.

  • Chiffrement convergent à cléSecret par tenant, BLAKE3_keyed + XChaCha20-Poly13059af2
  • Portée / secret par tenantOracle confiné à la flotte du revendeur4d8e
  • Hiérarchie CK / CMK / KEKDédup et récupération à deux étages séparésa8e0
  • Rotation de cléLoi de la pyramide : KEK instantanée, CMK bornée, CK par attrition (époques)a8e0
  • KDF Argon2idParamètres avec le sel, calibrés à l'enrôlement (~1-2 s), plancher RFC 9106 (64 Mio)9af2
  • Custody / récupérationManagé (défaut) + Zéro-connaissance à la demandea8e0
  • Coffre de clés (KMS/HSM)OpenBao auto-hébergé (Transit) — HSM optionnel selon exigences contractuellesf1c0
  • Résistance post-quantiqueDonnées au repos nativement résistantes (tout symétrique 256 bits) ; TLS hybride ML-KEM et signatures hybrides à venirpq

La tension fondamentale 0a55

Chiffrement × déduplication × stateless. On ne peut pas avoir la version naïve des trois à la fois.

La dédup exige du déterminisme : pour reconnaître deux contenus identiques, on hache et on compare — même contenu, même adresse. Le chiffrement correct, lui, veut du hasard : chiffrer deux fois la même donnée doit produire deux résultats non corrélables. Ces deux exigences se percutent de face.

Si chaque client chiffre avec sa propre clé aléatoire, le même fichier chez deux machines devient deux chiffrés sans rapport : le serveur voit deux blobs étrangers, et la dédup inter-clients meurt.

Résolution Le chiffrement convergent — dériver la clé du contenu lui-même. Deux machines qui hachent le même fichier dérivent la même clé, produisent le même chiffré, et le serveur peut dédupliquer. Et rien n'est stocké en clair.

Mais cette résolution a un prix, et c’est tout l’objet du point suivant.

Convergence & attaque par confirmation e1c4

La clé est une fonction déterministe et publique du contenu. Donc on peut deviner un fichier et vérifier sa présence — sans rien déchiffrer.

Comme la transformation clair → chiffré est fixe et publique (on ne mise jamais sur le secret de l’algorithme), n’importe qui possédant un fichier candidat peut recalculer lui-même son chiffré, puis vérifier s’il existe dans le stockage. C’est l’attaque par confirmation de fichier, et elle ne demande aucun déchiffrement.

Pire cas — faible entropie Quand le contenu possible est petit ou prévisible (un modèle de document où seul varie un salaire, une config qui n'est qu'une des N versions connues, un chunk d'en-tête plein de zéros), l'attaquant n'a même pas besoin de détenir le fichier : il énumère toutes les valeurs candidates et trouve la correspondance.

C’est un résultat prouvé : le chiffrement convergent — formalisé sous le nom de message-locked encryption par Bellare, Keelveedhi & Ristenpart — n’offre de confidentialité que pour des messages imprévisibles. Solide pour des données réellement uniques ; quasi nul pour tout ce qui est devinable.

Toutes ces attaques ont besoin d’un oracle — un moyen de tester « ce chiffré existe-t-il ? ». Or un client stateless en fournit un par construction : sans index local, il doit demander « as-tu déjà X ? » avant d’uploader. Et même le simple fait que l’upload soit sauté trahit l’existence (canal auxiliaire documenté par Harnik et al., 2010).

Le principe de portée 4d8e

Portée du secret de convergence = portée de la déduplication = portée de l'oracle de confirmation. Une seule décision les fixe toutes les trois.

« Aucun clair sur le disque » bloque l’attaque qui lit tes octets — mais pas celle qui les devine et les confirme. Ce sont deux menaces différentes ; le chiffrement au repos ne fait rien contre un oracle. Ton exigence est donc nécessaire mais pas suffisante. Ce qui tranche, c’est à qui tu acceptes de tendre l’oracle :

Aucun secret — convergence pure oracle ouvert

Dédup mondiale, maximale. Mais l'oracle est ouvert à tous : l'opérateur du stockage et n'importe quel inconnu.

Secret global unique oracle interne

Partagé par tous les clients, inconnu de l'opérateur — qui perd l'oracle. Mais comme tous les clients partagent le secret, un client malveillant garde l'oracle contre les autres.

Secret par tenant oracle confiné

Dédup au sein de ta flotte seulement. L'oracle est confiné aux détenteurs de ton secret — tes propres machines. Les inconnus sont verrouillés : leurs adresses dérivées diffèrent.

Le gain de la dédup mondiale, ce sont les fichiers partagés — OS, bibliothèques communes. En pratique ils sont déjà présents dans ta propre flotte, donc la dédup par tenant les capte quand même. C’est presque toujours le bon compromis.

La solution retenue 9af2 Décidé

Chiffrement convergent à clé, avec un secret par tenant. Pour démarrer, « ton tenant » = toi seul.

Un secret maître par tenant, dérivé d’une passphrase via Argon2id, qui ne quitte jamais le client. Puis, pour chaque chunk :

# secret maître, jamais transmis secret = Argon2id(passphrase, sel) # par chunk — tout est déterministe clé_chunk = BLAKE3_keyed(secret, chunk) chiffré = XChaCha20-Poly1305(clé_chunk, chunk) adresse = BLAKE3(chiffré)

Deux machines de ta flotte qui voient le même chunk dérivent la même clé → même chiffré → même adresse → dédup. Un autre tenant a un secret différent → tout diffère, ni dédup ni oracle entre vous. L’opérateur, qui détient blobs et index mais pas le secret, ne peut pas calculer l’adresse d’un fichier candidat : il perd l’attaque par confirmation.

Deux précisions qui comptent Nonce déterministe sur les chunks. La convergence impose un chiffré identique pour un clair identique. Comme la clé est unique par contenu, un nonce fixe est sûr — le choix de l'AEAD (XChaCha20 plutôt qu'AES-GCM-SIV) est détaillé au point aead.
Métadonnées — régime différencié Répertoires et racines de snapshots : jamais convergents. Ils portent les noms, la structure et les dates — très identifiants, très devinables, et de dédup négligeable (les mtimes diffèrent toujours entre machines) : nonce aléatoire sous une clé du tenant. Les manifestes de fichier, eux, sont convergents : purs contenus (ni nom ni date), impossibles à deviner sans posséder le fichier entier — auquel cas l'oracle des chunks suffit déjà. Les rendre convergents n'ajoute aucune surface d'attaque et offre la dédup des recettes entre machines. Détail et justification au point dag.

Chemin simple : v1 = un seul tenant (toi). La crypto est exactement la même ; passer au multi-tenant plus tard se fait en provisionnant d’autres secrets, sans refonte.

Ce qui fuit encore, pour être honnête : l’opérateur reste aveugle au contenu et à la confirmation, mais pas aux tailles ni au timing des blobs (analyse de trafic). Résidu classique, qu’on réduit par du padding le jour où ça entre dans le modèle de menace.

Les paramètres Argon2id — le hachoir volontairement lent

En clair — pourquoi ralentir exprès Une passphrase humaine est devinable : dictionnaires, variations, fuites. Or en mode managé, la plateforme stocke la CMK emballée sous une clé dérivée de la passphrase : qui vole ce blob peut essayer des milliards de passphrases hors ligne, sur ses GPU, sans être vu. La seule défense : rendre chaque essai coûteux. Argon2id est un hachoir à trois réglages — la mémoire (le hachoir exige un plan de travail immense : des centaines de Mo — c'est la défense anti-GPU, car une carte graphique a des milliers de petites lames mais de minuscules plans de travail), le temps (le nombre de passages), le parallélisme (combien de bras tournent la manivelle). L'asymétrie magique : le légitime paie une fois (1-2 s au déverrouillage, imperceptible) ; l'attaquant paie à chaque essai × des milliards × privé de ses GPU = des siècles. Et le sel rend chaque serrure unique — aucun dictionnaire précalculé ne sert deux fois.

Les décisions — et la subtilité qui compte :

  • Les paramètres voyagent avec le sel. Ils ne sont pas secrets (il les faut pour re-dériver) : ils sont enregistrés dans l'enveloppe, à côté du sel. Chaque déverrouillage lit « m=512 Mio, t=4, p=4 » et applique. Conséquence : aucun paramètre global figé — pas de rigidité, pas de migration de format.
  • Calibrage à l'enrôlement. La machine benchmarke : combien de mémoire et de passes pour atteindre ~1-2 s ici ? Un desktop moderne montera à 512 Mio–1 Gio ; un NAS à 512 Mio de RAM totale descendra vers le plancher en compensant par plus de passes.
  • Les planchers, sourcés RFC 9106 — premier choix recommandé : m=2 Gio, t=1, p=4 ; environnement contraint en mémoire : m=64 Mio, t=3, p=4. 64 Mio est notre plancher absolu (profil NAS) : en dessous, on refuse de dériver.
  • Mise à niveau opportuniste. Chaque changement de passphrase = une re-dérivation = un re-calibrage gratuit vers le haut. La rotation de KEK et la montée en force du KDF sont la même opération.

Hiérarchie de clés & récupération a8e0 Architecture décidée

Le déclic : séparer la dédup (clé de convergence) de la récupération (clé d'enveloppe). Personne ne mémorise de secret par client — on empile les clés.

On ne demande pas à chaque client de conserver un secret. On met en place une hiérarchie de clés (envelope encryption), où dédup et récupération vivent à des étages différents :

  • CK — clé de convergence, par revendeur. C'est elle qui fait converger les chunks identiques. Partagée dans le pool, gérée par la plateforme, jamais mémorisée par un humain.
  • Les clés de chunk = BLAKE3_keyed(CK, chunk), stockées dans les manifestes. C'est obligé : on ne peut pas re-dériver une clé convergente sans le clair, donc la recette de chaque fichier embarque les clés de ses propres chunks.
  • CMK — clé maître du client final. Chiffre SES manifestes et sa racine. C'est ça, « les clés de mes données ».
  • KEK — clés d'enveloppe qui emballent la CMK. C'est ici, et uniquement ici, que vit la politique de récupération.

Hiérarchie de clés — du haut (récupération) au bas (données)

RÉCUPÉRATION · EMBALLE LA CMK code client KEK revendeur KEK seuil 2/3 CMK — clé maître du client Propre à chaque client final — déverrouille SES manifestes. déchiffre Manifestes (clés de chunk incluses) Recettes de fichiers, chiffrées sous la CMK. fournit clé_chunk clé_chunk = BLAKE3_keyed( CK , chunk) CK · convergence (par revendeur), gérée par la plateforme. déchiffre Chunks chiffrés · pool partagé Dédupliqués entre clients du revendeur. L'opérateur ne voit que ça.

Pour lire un fichier, il faut descendre toute la chaîne : KEK → CMK → manifeste → clé_chunk → chunk. La récupération agit tout en haut (sur la CMK) ; la dédup agit tout en bas (sur les chunks).

Conséquence — deux risques à deux étages Le pool de chunks est partagé au niveau revendeur, mais les manifestes de chaque client sont chiffrés sous SA CMK. Un voisin peut, avec CK, confirmer l'existence d'un chunk qu'il devine (l'oracle, au niveau chunk) — mais pour lire le fichier entier d'un voisin, il lui faudrait son manifeste. Les deux risques ne sont pas au même étage.

Deux postures — une seule implémentation. La CMK peut être emballée sous plusieurs KEK simultanément ; les deux modes ne sont que deux configurations d’emballage différentes, gérées par le même code.

Configurations d'emballage de la CMK

MODE MANAGÉ · DÉFAUT KEK passphrase Argon2id KEK managée revendeur OpenBao (Transit) CMK — clé maître client ✓ récupérable — le revendeur peut déballer via OpenBao l'éditeur n'a pas le droit ACL · il reste aveugle ZÉRO-CONNAISSANCE · À LA DEMANDE KEK passphrase Argon2id Code de récupération 256 bits · local · hors-ligne CMK — clé maître client ✓ personne côté serveur ne peut déchiffrer ⚠ perte passphrase + code = perte définitive des données

Les deux modes coexistent sur la même plateforme. L'emballage (b) — KEK managée — est simplement présent ou absent. Activer le ZK = retirer cet emballage : acte fort, irréversible sauf en ré-activant le managé.

Flux du mode managé Inscription : la CMK est générée localement, puis emballée sous (a) la KEK passphrase et (b) la KEK managée du revendeur ; les deux CMK-emballées sont stockées dans les métadonnées. La KEK managée ne sort jamais du coffre.

Usage normal : aucun appel au coffre. Le client déverrouille sa CMK via sa passphrase localement.

Récupération : après vérification d'identité par le revendeur, le backend demande au coffre de déballer la CMK à l'intérieur (unwrap), puis la ré-emballe sous la nouvelle passphrase (wrap). Pendant cette opération la CMK transite en clair dans la RAM de l'infrastructure — c'est le prix explicite de la récupérabilité.
Mode zéro-connaissance — acte fort Basculer en ZK retire l'emballage managé. Garde-fous obligatoires : avertissement sans ambiguïté (« plus personne côté serveur ne pourra récupérer vos données »), double confirmation, tracé dans l'audit log. État du mode (managé ou ZK) visible dans le dashboard client, sur les factures et dans l'API — libellé clair : « récupérable par le support » vs « non récupérable par quiconque côté serveur ».
La loi d'airain Tu ne peux pas avoir à la fois « moi seul peux jamais déchiffrer » et « on peut restaurer mes données quand j'ai perdu mon accès ». La récupérabilité impose qu'un autre détienne — ou puisse reconstituer — du matériel de clé. Le mode managé choisit la récupérabilité ; le mode ZK choisit l'isolation totale. Les deux sont légitimes ; l'un doit être le défaut.
Décision prise — défaut produit Managé = défaut. Standard commercial (Veeam, Datto, Acronis). Récupération indolore pour le client final. Le revendeur est le tiers de confiance ; l'éditeur reste aveugle (pas de droit ACL sur les KEK des revendeurs). ZK = option à la demande : disponible pour les clients qui l'exigent, avec les garde-fous ci-dessus.

Rotation — la loi de la pyramide

En clair — trois serrures, trois chantiers Changer une clé n'a pas le même coût selon l'étage de la pyramide. En haut, la KEK : changer le code du coffre-fort où est pendue la clé maîtresse — cinq minutes, aucune serrure de l'immeuble n'est touchée. Au milieu, la CMK : refaire les serrures d'un étage — borné, faisable. En bas, la CK : changer la langue dans laquelle toutes les étiquettes de l'entrepôt sont écrites — chaque brique change de nom, tout est à réécrire. Le coût de rotation est inversement proportionnel à la hauteur dans la pyramide.
  • KEK — rotation de routine, instantanée. La CMK est emballée par les KEK — c'est tout l'intérêt de l'enveloppe : changer une passphrase ou révoquer la KEK d'un revendeur = déballer/remballer quelques octets. Aucune donnée ne bouge. À encourager, sans friction.
  • CMK — rotation bornée. Elle chiffre les annuaires et racines de ce client : la tourner = re-chiffrer ces métadonnées (petites). Opération d'heures au pire, déclenchée sur suspicion de compromission d'une machine. Les chunks ne bougent pas d'un octet.
  • CK — pas une rotation, une migration. La CK détermine les clés de chunks, donc les chiffrés, donc les adresses : la tourner = tout re-chiffrer, tout ré-uploader, dédup à zéro. Structurellement identique à changer les paramètres du chunker.
Aucune rotation imposée — sur événement, jamais sur calendrier La rotation « d'hygiène » tous les X mois vient du monde des clés qui voyagent (certificats TLS, tokens d'API — exposés en permanence, donc renouvelés). Nos clés ne voyagent jamais : la CK et la CMK ne quittent pas les clients. Les tourner par calendrier n'apporte aucune sécurité et coûte réel — le re-chiffrement qui tourne mal est un risque en soi. Si une conformité (type PCI-DSS) exige une rotation périodique, on la satisfait en tournant le haut de la pyramide — la KEK, annuellement, comme AWS KMS : coût zéro, case cochée. On ne tourne jamais le bas pour faire plaisir à un auditeur.
ÉvénementCe qui tourneCoûtFréquence réelle
Changement de passphrase, départ d’un employé du revendeurKEK (le code du coffre)~zéro, instantanéAussi souvent qu’on veut
Machine suspectée compromiseCMK (le trousseau)Borné — re-chiffrer les métadonnées du clientRare — réponse à incident
CK prouvée compromiseCK, par attrition (époques)Élevé mais étaléExceptionnel — jamais en routine
CK compromise — gravité relative, réponse par attrition D'abord relativiser : la CK volée ne déchiffre rien en masse. Elle permet l'oracle de confirmation (deviner un fichier candidat, vérifier sa présence) et de dériver la clé d'un chunk dont on possède déjà le clair — pour lire les données, il faut la CMK (le chemin racine → annuaire → manifeste). La CK est un secret de dédup, pas la clé du royaume. D'où la politique : pas de rotation planifiée (on ne paie pas une migration par hygiène — on protège la CK, provisionnée à l'enrôlement, jamais transmise ailleurs), et en cas de compromission avérée, rotation par attrition : une époque CK₂ pour les nouvelles données, les anciennes restent sous CK₁ et s'évaporent par la rétention. Perte temporaire de dédup entre époques, zéro re-upload massif — la mécanique des profils épinglés, réutilisée telle quelle.

Dernier maillon : la montée en force du KDF (voir les paramètres Argon2id) se fait gratuitement à chaque rotation de KEK — changer de passphrase re-dérive, et re-dériver est l’occasion de re-calibrer vers le haut. Les deux mécanismes se referment l’un sur l’autre.

Coffre de clés souverain (KMS/HSM) f1c0 Décidé

Un coffre-fort à passe-plat : la clé ne sort jamais. On envoie une donnée, le coffre chiffre ou déchiffre à l'intérieur, et renvoie le résultat. Coffre retenu : OpenBao auto-hébergé — souverain, open-source, Linux Foundation.

Deux définitions à ne pas confondre.

  • KMS (Key Management System) — la couche logicielle qui gère le cycle de vie des clés : création, rotation, révocation, ACL, audit. C'est l'interface. OpenBao/Vault est un KMS.
  • HSM (Hardware Security Module) — le coffre physique durci : la clé ne sort jamais du boîtier, les opérations cryptographiques s'y exécutent, tamper-resistant, certifiable FIPS 140-2/3. C'est le matériel. Un HSM peut vivre derrière un KMS.

Ce que le coffre garde uniquement : les KEK managées par revendeur. Jamais les données client, ni les CMK, ni les chunks. Volume d’opérations minuscule — un wrap à l’inscription, un unwrap à la récupération.

Choix souverain — OpenBao Projet souverain : pas d'AWS KMS / Google Cloud KMS / Azure Key Vault — ces services sont chez des tiers qui peuvent couper l'accès. Coffre retenu : OpenBao (fork libre de HashiCorp Vault après changement de licence, gouvernance Linux Foundation), auto-hébergé, moteur Transit (chiffre/déchiffre sans jamais exposer la clé). Alternative : HashiCorp Vault (même API, licence BSL — à évaluer selon posture juridique).

Option HSM matériel. Quand un client exige un niveau de garantie plus élevé (certifications, obligations contractuelles), deux façons d’intégrer un HSM :

Cas A vs Cas B — où vit la protection HSM

CAS A — HSM PROTÈGE LA RACINE (SEAL) HSM clé racine de descellement descelle au boot OpenBao / Vault gère KEK · ACL · audit · rotation wrap/unwrap KEK KEK managée (par revendeur) CMK client (emballée) ✓ HSM sollicité au démarrage seulement ✓ débit élevé, usage normal indépendant △ KEK transitent en RAM quand Vault est descellé CAS B — HSM EN DIRECT (PKCS#11) HSM (Thales Luna, Utimaco…) PKCS#11 · FIPS 140-2/3 KEK managée (par revendeur) vit dans le HSM · ne sort jamais wrap/unwrap via PKCS#11 chaque opération dans le boîtier CMK client (emballée) ✓ KEK ne transitent jamais en RAM ✓ FIPS de bout en bout · garantie maximale △ plus lent · plus cher · HSM dans le chemin critique △ fragile : panne HSM = service indisponible

Cas A protège la racine ; Cas B protège chaque KEK. Position du projet : OpenBao seul pour démarrer → Cas A quand une garantie matérielle/souveraine est demandée → Cas B réservé aux exigences contractuelles FIPS strictes.

Intégration — abstraction KeyVault Le code métier ne connaît jamais « OpenBao » directement. Il passe par un contrat unique : KeyVault { wrap(cmk, key_id) → cmk_emballée ; unwrap(cmk_emballée, key_id) → cmk }. Derrière, on branche OpenBao (soft), OpenBao + HSM (seal Cas A), ou HSM direct PKCS#11 (Cas B) — sans toucher au code métier.
Isolation par revendeur Chaque revendeur dispose de son propre espace de clés cloisonné dans OpenBao (namespaces + ACL). La KEK du revendeur A ne peut pas déballer les CMK des clients du revendeur B. L'éditeur héberge le coffre mais n'a pas le droit ACL de déclencher un unwrap sur l'espace d'un revendeur — il reste aveugle tout en fournissant le service. Un revendeur qui veut son propre coffre peut pointer son implémentation KeyVault vers sa propre instance OpenBao ou son HSM.
Glossaire CMK — Client Master Key, clé maître du client final, chiffre ses manifestes.
KEK — Key-Encryption Key, clé d'enveloppe qui emballe la CMK.
KMS — Key Management System, logiciel de gestion du cycle de vie des clés (OpenBao).
HSM — Hardware Security Module, boîtier physique durci, la clé n'en sort jamais.
PKCS#11 — API standard (C) pour piloter un HSM depuis une application.
FIPS 140-2/3 — standard américain de certification des modules cryptographiques.
Seal/Unseal — mécanisme OpenBao/Vault : le coffre démarre scellé (données illisibles), on le descelle avec la clé racine (HSM en Cas A, ou saisie manuelle).
Transit — moteur OpenBao/Vault : expose wrap/unwrap/encrypt/decrypt sans jamais sortir la clé de la mémoire du processus.
OpenBao — fork libre de HashiCorp Vault (licence MPL 2.0), gouvernance Linux Foundation, API 100 % compatible.

Résistance post-quantique pq Décidé

La protection des données au repos est déjà quantum-résistante par construction — tout est symétrique 256 bits, aucune crypto asymétrique dans le chemin des données. Restent deux retouches : le transport (TLS hybride ML-KEM) et les signatures de mise à jour.

En clair — le passe-partout et le devineur accéléré Un ordinateur quantique (quand il existera à l'échelle) casse la crypto de deux façons très inégales. Shor, le passe-partout : il casse totalement la crypto asymétrique — RSA, courbes elliptiques, tout ce qui repose sur « factoriser, c'est dur ». Ces serrures-là tombent, point. Grover, le devineur accéléré : sur la crypto symétrique, il ne fait qu'accélérer la devinette par force brute — en racine carrée, ce qui divise par deux les bits de sécurité. Une clé de 256 bits garde 128 bits de sécurité quantique : toujours inviolable. C'est pourquoi NIST, ANSSI et BSI considèrent AES-256 et ChaCha20-256 comme nativement résistants au quantique. Moralité : le quantique est une apocalypse pour l'asymétrique, une égratignure pour le symétrique bien dimensionné.

L’inventaire Cairn face au quantique

BriqueTypeVerdict quantique
XChaCha20-Poly1305 (256 bits)symétriquerésistant natif (Grover → 128 bits, largement assez)
BLAKE3 (adresses, dérivation)hash 256 bits✅ résistant natif
Argon2idKDF memory-hard✅ résistant (la mémoire-dure gêne aussi le quantique)
Pyramide CK / CMK / KEKemballages symétriques✅ résistant natif
TLS transportasymétrique (X25519)⚠️ cassable par Shor → à hybrider
Signatures de mise à jourasymétrique (Ed25519)⚠️ cassable par Shor → à hybrider

Le constat remarquable : toute la protection des données au repos est symétrique — il n’y a aucune crypto asymétrique dans le chemin des données. C’est un sous-produit du chiffrement convergent (clés dérivées par hash, emballages symétriques), et la plupart des concurrents ne peuvent pas en dire autant.

La menace qui compte : harvest now, decrypt later

Le backup est LA cible parfaite de cette attaque L'adversaire n'attend pas d'avoir son ordinateur quantique : il enregistre aujourd'hui le trafic chiffré, et le déchiffrera en 2035. Or un produit de sauvegarde est la cible idéale : des données à rétention longue (10 ans et plus), qui transitent chaque nuit, en volume. Chez Cairn la défense est double : le TLS hybride post-quantique protège le tuyau, et même un TLS cassé rétroactivement ne livrerait que des blobs déjà chiffrés en symétrique côté client — l'attaque ne rapporte que du métadata de transport (tailles, timing). La couche qui compte le plus est déjà immunisée.

Les deux retouches

  • TLS hybride post-quantique — échange de clés X25519 + ML-KEM-768 (FIPS 203) : l'hybride combine la courbe éprouvée et le post-quantique, de sorte qu'il faudrait casser les deux. Déployé par défaut chez Cloudflare, Google et AWS depuis 2024-2025, supporté par l'écosystème rustls. À inscrire comme exigence du sujet Transport de la section B.
  • Signatures de mise à jour hybrides — l'updater signe en Ed25519, forgeable par un quantique futur. Contrairement au harvest-now, forger exige le quantique au moment de l'attaque — moins urgent — mais le format de signature est agile dès le premier jour : double signature Ed25519 + ML-DSA (FIPS 204), ou SLH-DSA (FIPS 205, à base de hashes — philosophiquement cohérent avec notre design tout-hash, le choix le plus conservateur qui existe).

Le pitch — chaque mot est vrai

Ce qu'on peut vendre sans quantum-washing « Les données sont chiffrées exclusivement en cryptographie symétrique 256 bits, nativement résistante au quantique selon le NIST et l'ANSSI. Le transport et les signatures utilisent de la cryptographie post-quantique hybride (ML-KEM, FIPS 203). Même un adversaire qui enregistre votre trafic aujourd'hui pour le déchiffrer dans dix ans n'obtient rien. »

Multi-tenant & commercial G

Le modèle commercial éditeur → revendeur → client. Dédup, facturation, branding, rôles. L'architecture de tenant est posée — le reste à construire.

  • Modèle de tenantTenant = revendeur retenu, curseur configurable3d71
  • Onboarding / provisioning revendeurCréation du tenant, génération de la CK, initialisation des quotas
  • White-label / branding par revendeur
  • Facturation & métragePiège : qui paie un chunk partagé ? Comptabiliser les économies de dédup par revendeur
  • Plans, quotas, limites
  • Hiérarchie de comptes & rôlesÉditeur → revendeur → client → utilisateurs
  • Coût de la dédup par tenant vs globaleChiffrer la perte de ratio due au scoping tenant — le prix de l'assurance anti-oracle

Le modèle de tenancy 3d71 Recommandé

Le tenant n'est pas une fatalité, c'est un curseur — et il faut le séparer de la récupération. Le bon défaut : le revendeur.

Dans la chaîne éditeur → revendeur → client final, là où tu places la frontière du tenant tu fixes du même coup la portée de la dédup et celle de l’oracle (point 4d8e). Les trois options sont littéralement des portées emboîtées :

Éditeur Rev. A Rev. B A1 A2 B1 B2

tenant = client final

Isolation maximale. Quasi aucune dédup — rien n'est partagé entre clients d'un même revendeur.

isolation max
Éditeur Rev. A Rev. B A1 A2 B1 B2

tenant = revendeur

Dédup sur tout le parc du revendeur (OS, docs communs → grosses économies). Oracle confiné à l'intérieur.

recommandé
Éditeur Rev. A Rev. B A1 A2 B1 B2

tenant = éditeur

Dédup maximale, mais l'oracle traverse les revendeurs concurrents. À éviter.

oracle global

Le gain de la portée éditeur — les fichiers d’OS et bibliothèques communes — est de toute façon déjà présent à l’intérieur du parc d’un revendeur. La portée revendeur le capte donc sans ouvrir l’oracle aux concurrents.

Et c'est configurable Un revendeur qui sert des entreprises mutuellement méfiantes bascule sur tenant = client final, au prix de la dédup partagée. Le curseur reste un paramètre, pas une décision gravée.

Interfaces web H

Trois portails distincts — client, revendeur, super-admin — avec des besoins de stack et d'auth différents.

  • Dashboard clientÉtat des backups, liste des snapshots, lancer une restauration
  • Portail admin revendeurGestion des clients, quotas, facturation, branding
  • Console super-admin (éditeur)Vue globale, gestion des revendeurs, incidents
  • UI de restaurationParcourir les snapshots, sélectionner des fichiers, déclencher le téléchargement
  • Reporting & alertesStockage utilisé, économies de dédup, santé des backups
  • Auth / SSO, i18n, choix de stack front

Sécurité I

Au-delà de la crypto — modèle de menace, RBAC, conformité. L'attaque par confirmation est documentée ; le reste reste à formaliser.

  • Modèle de menace documentéAdversaires, surfaces, hypothèses — formaliser ce qui est déjà implicite
  • Attaque par confirmation / oracleAnalysée et mitigée par la portée du secrete1c4
  • Fuite par analyse de traficTailles et timing des blobs — padding évoqué, non conçu9af2
  • RBAC / audit logging
  • Prévention d'abusClient malveillant qui sonde l'oracle, flood, scraping de l'index
  • ConformitéRGPD, résidence des données, rétention, droit à l'effacement
  • Revue de sécurité / pentest
  • Certifications viséesISO 27001, HDS, SOC 2, SecNumCloud ? — compatibilité du système à évaluer

Haute disponibilité & durabilité du service J

Durabilité et disponibilité du service lui-même — pas des données clients, mais de l'infrastructure qui les sert.

  • Réplication stockage + DB, failover automatiquePlan de contrôle : PostgreSQL retenu (réplication + failover natifs). Réplication du stockage à traitercas
  • RPO / RTO du service, cohérence des vues
  • Multi-région / géo-distribution, load balancing
  • Plan de reprise d'activité (DR), scaling horizontal

Observabilité & exploitation K

Logs, métriques, tracing. Ce qui permet de savoir ce qui se passe — et de tenir un SLA.

  • Logs, métriques, tracing distribué, alerting
  • Health checks, dashboards ops
  • SLA / SLO — définition et mesure

Déploiement & distribution L

Comment le serveur se déploie, comment le client atterrit sur chaque plateforme. SaaS central ou revendeur auto-hébergé.

  • Packaging serveurConteneurs, orchestration (Kubernetes, Nomad…)
  • Distribution clientInstalleurs par plateforme — Linux, macOS, Windows, NAS
  • CI/CD, IaC, gestion des environnements
  • Modèle de déploiementSaaS central géré vs revendeur auto-hébergé — implications sur la clé CK

Cycle de vie des données & politiques M

Rétention, pruning, immuabilité anti-ransomware. L'immuabilité est un argument de vente majeur — à concevoir tôt.

  • Rétention & pruningGrille GFS par machine, plafond par plan (90 j inclus / 1 an option), pruning côté serveur uniquementgfs
  • Immuabilité / anti-ransomwarePréavis de destruction + plancher WORM décidés côté rétention ; mécanique stockage (object-lock…) à détaillergfs
  • Legal hold, suppression / droit à l'oubliTension entre immuabilité et RGPD
  • Vérification de restaurationUne sauvegarde non restaurable ne vaut rien — tests automatiques

Rétention & pruning gfs Décidé

Une grille GFS par machine, plafonnée par le plan — 90 jours inclus, 1 an en option — appliquée exclusivement côté serveur, avec préavis sur les raccourcissements et plancher d'immuabilité en option. Le client n'a pas la gomme.

La rétention n’est pas une durée, c’est une grille

En clair — les archives du journal Le kiosque garde tous les numéros de la semaine ; la bibliothèque, un par semaine du trimestre ; les archives, un par mois des dernières années. Personne ne stocke chaque numéro pour toujours — la mémoire s'estompe par paliers, et c'est exactement ce qu'il faut : on veut la version d'hier précisément, celle d'il y a trois mois approximativement.

C’est le schéma standard du métier, dit GFS (grand-père / père / fils) : garder les N derniers + 1/jour × D + 1/semaine × W + 1/mois × M. La politique s’exprime en grille ; les offres commerciales sont des préréglages nommés de cette grille.

Le compromis assumé — « et le mardi d’il y a trois semaines ? »

L’objection légitime : sous une grille dégressive, à trois semaines en arrière il ne reste qu’un snapshot par semaine — le mardi précis a disparu, il ne reste que les dimanches qui l’encadrent. Trois réponses :

  • La courbe réelle des restaurations est brutalement asymétrique — l'écrasante majorité, c'est « hier » (la suppression accidentelle, remarquée vite). Les demandes lointaines sont presque toujours « avant la corruption », où un point d'ancrage approximatif suffit.
  • Le préréglage de base pousse les quotidiens à un mois plein — le « mardi d'il y a trois semaines » reste donc disponible dans l'offre incluse (voir la table).
  • La grille n'interdit rien, elle tarife. « Chaque jour pendant 90 jours » est une grille valide (quotidiens × 90) — et grâce à la dédup, pas si chère pour des données calmes : chunks et nœuds d'arbre inchangés sont partagés entre snapshots par référence. Là où ça pique : le churn — le client qui dump sa base de 10 Go chaque nuit garderait 900 Go en intégral contre ~300 en grille. D'où : la grille dégressive en défaut, l'intégral en réglage (voire en cran d'offre) — son coût dépend du churn du client.

Les préréglages

PréréglageGrille~SnapshotsCe que le client retrouve
Inclus — 90 jours7 derniers + 1/jour × 30 + 1/sem × 9~42n’importe quel jour du dernier mois, n’importe quelle semaine des 2 mois d’avant
Option — 1 anidem + 1/mois × 12~54+ n’importe quel mois de l’année
Réglablechaque paramètre, sous le plafond du planjusqu’à l’intégral quotidien si le client y tient

Le pitch au client final, honnête et lisible : « chaque jour du dernier mois, chaque semaine du dernier trimestre, chaque mois de l’année ».

Le tout par machine, via la config à trois étages : le plan fixe le plafond (l’horizon maximal — c’est l’attribut commercial, section G), le revendeur règle et peut verrouiller, la machine ajuste sous le plafond. Et un argument de vente que la dédup offre gratuitement : allonger la rétention ne coûte que ce qui change — les données statiques coûtent quasi pareil à garder un an ou 90 jours.

Le serveur tient la gomme, jamais le client

On ne confie pas la gomme à celui qu'on protège Si la machine sauvegardée pouvait effacer son propre historique, un ransomware qui la compromet effacerait les backups avant de chiffrer les données — c'est littéralement leur mode opératoire. Donc : le client écrit des snapshots, il n'en supprime jamais. Le pruning est un job serveur qui applique la politique (suppression des refs en DB de contrôle), le GC ramasse ensuite les chunks orphelins. L'API cliente n'a même pas d'endpoint de suppression — on ne peut pas abuser d'un droit qui n'existe pas.

Les garde-fous anti-sabotage

Le ransomware sophistiqué ne supprime pas les backups : il raccourcit la rétention — vole les credentials admin du dashboard, passe la rétention à 1 jour, attend le prune. Parades :

  • Préavis de destruction — tout raccourcissement de rétention s'applique avec un délai (72 h), notifié, annulable, audité. L'allongement, lui, est immédiat — il ne détruit rien.
  • Plancher d'immuabilité (option par client) — les X derniers jours sont WORM : même un admin ne peut pas les pruner. C'est le sujet « anti-ransomware » de la section M — et un argument de vente majeur.
  • Legal hold — suspend le pruning d'un client, point. Prioritaire sur toute grille.

Politique vs mécanisme

Pruner = retirer des refs de snapshots ; c’est le GC (le nœud dur restant, section C) qui fait le vrai travail de récupération — mark-and-sweep depuis les racines survivantes. La rétention est la politique, le GC est le mécanisme : les concevoir séparément garde les deux simples. Et les sessions de l’API d’existence protègent déjà les backups en cours pendant qu’un prune tourne.

Qualité, tests & correction N

Tests de restauration et chaos engineering — pas juste des tests unitaires. Un backup doit se restaurer, pas juste s'écrire.

  • Tests unit / intégration / e2e, matrice multi-plateforme
  • Tests de restauration & de corruption / récupérationSimuler corruption, perte de chunks, index corrompu
  • Chaos / injection de fautes, benchmarks perf

Transverse O

Sujets qui traversent toutes les couches — format sur disque, pipeline de données, Unicode, documentation.

  • Format sur disque / sur fil & versionnageÉvoluer sans casser les vieilles données ou les vieux clients
  • Pipeline de donnéesL'ordre exact des opérations — à formaliser comme section à part entière
  • Gestion des chemins / Unicode cross-OS, horloges / tempsNormalisation NFC/NFD, séparateurs, timestamps tz-aware
  • DocumentationUtilisateur final, API, runbooks ops, guide revendeur

Virtualisation & bare metal P

Au-delà des fichiers : sauvegarder des machines entières. Intégration image-level avec les quatre hyperviseurs — Proxmox VE, XCP-ng, Hyper-V, VMware vSphere — et restauration bare metal. Le périmètre est acté ; les mécanismes restent à concevoir.

L’agent fichier couvre l’intérieur d’un système. Ici on vise le niveau bloc : lire les disques d’une VM (ou d’une machine physique), suivre les blocs modifiés (CBT) pour des incrémentaux efficaces, et faire passer le flux dans le même pipeline chunk → dédup → chiffrement. C’est le cas où la dédup inter-clients paie le plus : cent VMs issues du même modèle d’OS ne stockent leurs blocs communs qu’une fois. Le mécanisme commun est détaillé au point modèle image-level & CBT.

  • Agentless vs agent invitéProxy chiffrant côté client pour l'image-level ; agent dans l'invité pour le fichier et le bare metalcbt
  • Modèle image-level & CBTLire les disques, ne re-découper que le delta, réutiliser le pipeline chunk → dédup → chiffrementcbt
  • Proxmox VEDirty bitmaps QEMU via l'API Proxmox / QMP
  • XCP-ngCBT via XAPI (list_changed_blocks) + export NBD
  • Hyper-VRCT (Resilient Change Tracking) + VSS, API WMI / PowerShell
  • VMware vSphereVADP + CBT (QueryChangedDiskAreas) via VDDK — SDK propriétaire, licence à surveiller côté Broadcom
  • Bare metalImage bloc + BMR : snapshot volume (VSS / LVM), média de restauration bootable, matériel dissemblable
  • Cohérence applicativeQuiescing invité (VSS, pre/post scripts) vs crash-consistent
  • Modes de restaurationVM complète, fichier depuis image, bare metal ; instant recovery plus tard

Modèle image-level & CBT cbt

Sauvegarder une machine, c'est lire ses disques au niveau bloc, suivre les blocs modifiés (CBT) pour les incrémentaux, et faire passer le flux dans le même pipeline que les fichiers. C'est là que la dédup inter-clients paie le plus.

Agentless ou agent invité

Deux placements, complémentaires.

  • Agentless (proxy) — un proxy Cairn parle à l'API de l'hyperviseur, snapshote la VM et lit ses disques. Idéal pour l'image-level : rien à installer dans les invités. Le proxy est un client Cairn — il détient le secret du tenant et chiffre côté client, donc la propriété zéro-connaissance tient.
  • Agent invité — l'agent tourne dans la VM et fait du fichier (voir section A) ou, sur une machine physique, du bare metal. Nécessaire quand il n'y a pas d'hyperviseur à interroger.

Le flux image-level

# incrémental d'un disque de VM
snapshot = hyperviseur.snapshot(vm) # point cohérent
delta = CBT.changed_blocks(vm, depuis=dernier) # seulement les régions modifiées
chunks = FastCDC(delta) → dédup → chiffrement → adresse # pipeline habituel

Le disque virtuel est un gros blob. On le découpe en chunks, on déduplique, on chiffre — exactement comme un fichier. La différence tient au CBT (Changed Block Tracking) de l’hyperviseur : il nous dit quelles régions ont bougé depuis la dernière sauvegarde, donc on ne relit et ne re-découpe que le delta. Les chunks inchangés sont déjà présents → dédup gratuite.

Là où la dédup inter-clients brille Cent VMs issues du même modèle d'OS partagent l'essentiel de leurs blocs. Adressés par le contenu, ces blocs identiques ne sont stockés qu'une seule fois pour tout le parc d'un tenant. L'image-level est le cas d'usage qui rentabilise le plus la déduplication — l'argument commercial le plus fort de l'architecture.

Par plateforme

PlateformeAPICBTTransport disque
Proxmox VEAPI Proxmox / QMPdirty bitmaps QEMUstorage / NBD
XCP-ngXAPICBT XAPI (list_changed_blocks)NBD
Hyper-VWMI / PowerShellRCTSMB / API
VMware vSphereVADP (vCenter / ESXi)CBT (QueryChangedDiskAreas)VDDK : NBD / hotadd / SAN
VMware — dépendance propriétaire VADP passe par le VDDK, un SDK propriétaire de VMware, et les conditions d'accès aux API et de licence évoluent côté Broadcom. À traiter comme une dépendance à part entière, avec son propre risque, contrairement aux trois autres qui exposent des API ouvertes.

Bare metal

Sur une machine physique, pas d’hyperviseur à interroger : c’est l’agent qui fait tout.

  • Cohérence — snapshot du volume avant lecture : VSS sous Windows, LVM / dm-snapshot (ou blk-snap) sous Linux.
  • CBT côté agent — l'agent maintient lui-même un bitmap des blocs modifiés, faute d'hyperviseur pour le fournir.
  • Restauration (BMR) — un média bootable (WinPE / live Linux) embarque l'agent Cairn, repartitionne et réécrit les blocs. Restauration vers matériel dissemblable : injection de pilotes (Windows), régénération de l'initramfs (Linux).

Modes de restauration

  • VM / machine complète — réécriture de l'image entière.
  • Fichier depuis une image — monter l'image sauvegardée et en extraire des fichiers, sans tout restaurer.
  • Instant recovery — démarrer directement depuis la sauvegarde (booter la VM sur le stockage de backup le temps d'une vraie restauration). Avancé, à envisager plus tard.

À trancher

  • Granularité du CBT (blocs de taille fixe) face au découpage FastCDC (taille variable) : re-découper les régions changées, et mesurer le surcoût.
  • Où tourne le proxy agentless (par cluster ? par hôte ?) et comment il reçoit le secret du tenant sans jamais l'exposer.
  • Cohérence applicative fine (bases de données à l'intérieur des VMs) : quiescing invité vs sauvegarde applicative dédiée.

Points ouverts c3b7 À trancher

Le tableau de bord du projet : où en est chaque section, et ce qu'il reste précisément à trancher — sur une seule page, sans avoir à parcourir les seize sections une par une.

Vue d’ensemble

Où on en est 37 sujets décidés (), 6 en cours (), 58 encore à documenter (○) — sur 101 sujets recensés dans les sections A à P. Le cœur du modèle (client, cryptographie, stockage) est le plus avancé ; les couches produit (interfaces web, commercial, exploitation) restent à défricher.

Les nœuds durs — tous tombés

L'API d'existence est conçue ([la session-promesse](existence-api.md)), et le GC inter-clients aussi ([mark-and-sweep par tenant, sans verrou](gc.md)). Les deux grands risques architecturaux du système sont derrière — il reste du travail, plus aucun mystère.

Côté stockage, tout est tranché : segments append-only + index LSM (architecture), et la compaction est réglée avec le GC — gloutonne, par taux de vivants, après quarantaine.

Détail par section — ce qu’il reste à faire

Uniquement les sujets non tranchés (◐ en cours, ○ à documenter). Les sujets décidés (✓) sont sur la page de chaque section.

A · Client / agent

  • Mode synchro à la demandeBackup continu / au fil de l'eau plutôt que planifié — watchers, debouncing, interaction avec le scan

C · Backend / services serveur

  • IngestRéception des chunks — validation, stockage atomique
  • Service d'indexhash → emplacement (segment, offset, len)
  • Service métadonnéesManifestes, snapshots, racines — séparé de l'index chunks
  • Coordination de dédup côté serveur
  • Isolation multi-tenant (application effective)
  • AutorisationQui lit / écrit quoi — rôles, scopes
  • Files de jobs asynchronesGC, compaction, vérification — orchestration
  • Scrubbing d'intégrité en tâche de fondDétection bit rot, re-vérification périodique
  • Lifetimes Rust pour les blobs — piste de rechercheGarantir des invariants du GC (épinglage, quarantaine) par le système de types Rust — à explorer

D · Stockage

  • Backends pluggablesDisque local, objet S3-compatible, cloud (GCS, Azure Blob…) — 1 segment = 1 objet
  • DurabilitéContrat d'écriture décidé (durable avant visible) ; réplication, erasure coding, sharding à traitercas
  • Architecture physiqueServeurs, châssis disques, SSD méta / HDD données, réseau, dimensionnement

E · Bases de données & métadonnées

  • Schéma & migrationsStratégie zero-downtime, compatibilité avant/arrière
  • Méta-durabilitéSauvegarder la DB du système de sauvegarde lui-même

G · Multi-tenant & commercial

  • Onboarding / provisioning revendeurCréation du tenant, génération de la CK, initialisation des quotas
  • White-label / branding par revendeur
  • Facturation & métragePiège : qui paie un chunk partagé ? Comptabiliser les économies de dédup par revendeur
  • Plans, quotas, limites
  • Hiérarchie de comptes & rôlesÉditeur → revendeur → client → utilisateurs
  • Coût de la dédup par tenant vs globaleChiffrer la perte de ratio due au scoping tenant — le prix de l'assurance anti-oracle

H · Interfaces web

  • Dashboard clientÉtat des backups, liste des snapshots, lancer une restauration
  • Portail admin revendeurGestion des clients, quotas, facturation, branding
  • Console super-admin (éditeur)Vue globale, gestion des revendeurs, incidents
  • UI de restaurationParcourir les snapshots, sélectionner des fichiers, déclencher le téléchargement
  • Reporting & alertesStockage utilisé, économies de dédup, santé des backups
  • Auth / SSO, i18n, choix de stack front

I · Sécurité

  • Modèle de menace documentéAdversaires, surfaces, hypothèses — formaliser ce qui est déjà implicite
  • Fuite par analyse de traficTailles et timing des blobs — padding évoqué, non conçu9af2
  • RBAC / audit logging
  • Prévention d'abusClient malveillant qui sonde l'oracle, flood, scraping de l'index
  • ConformitéRGPD, résidence des données, rétention, droit à l'effacement
  • Revue de sécurité / pentest
  • Certifications viséesISO 27001, HDS, SOC 2, SecNumCloud ? — compatibilité du système à évaluer

J · Haute disponibilité & durabilité du service

  • Réplication stockage + DB, failover automatiquePlan de contrôle : PostgreSQL retenu (réplication + failover natifs). Réplication du stockage à traitercas
  • RPO / RTO du service, cohérence des vues
  • Multi-région / géo-distribution, load balancing
  • Plan de reprise d'activité (DR), scaling horizontal

K · Observabilité & exploitation

  • Logs, métriques, tracing distribué, alerting
  • Health checks, dashboards ops
  • SLA / SLO — définition et mesure

L · Déploiement & distribution

  • Packaging serveurConteneurs, orchestration (Kubernetes, Nomad…)
  • Distribution clientInstalleurs par plateforme — Linux, macOS, Windows, NAS
  • CI/CD, IaC, gestion des environnements
  • Modèle de déploiementSaaS central géré vs revendeur auto-hébergé — implications sur la clé CK

M · Cycle de vie des données & politiques

  • Immuabilité / anti-ransomwarePréavis + plancher WORM décidés ; mécanique stockage (object-lock…) à détaillergfs
  • Legal hold, suppression / droit à l'oubliTension entre immuabilité et RGPD
  • Vérification de restaurationUne sauvegarde non restaurable ne vaut rien — tests automatiques

N · Qualité, tests & correction

  • Tests unit / intégration / e2e, matrice multi-plateforme
  • Tests de restauration & de corruption / récupérationSimuler corruption, perte de chunks, index corrompu
  • Chaos / injection de fautes, benchmarks perf

O · Transverse

  • Format sur disque / sur fil & versionnageÉvoluer sans casser les vieilles données ou les vieux clients
  • Pipeline de donnéesL'ordre exact des opérations — à formaliser comme section à part entière
  • Gestion des chemins / Unicode cross-OS, horloges / tempsNormalisation NFC/NFD, séparateurs, timestamps tz-aware
  • DocumentationUtilisateur final, API, runbooks ops, guide revendeur

P · Virtualisation & bare metal

  • Agentless vs agent invitéProxy chiffrant côté client pour l'image-level ; agent dans l'invité pour le fichier et le bare metalcbt
  • Modèle image-level & CBTLire les disques, ne re-découper que le delta, réutiliser le pipeline chunk → dédup → chiffrementcbt
  • Proxmox VEDirty bitmaps QEMU via l'API Proxmox / QMP
  • XCP-ngCBT via XAPI (list_changed_blocks) + export NBD
  • Hyper-VRCT (Resilient Change Tracking) + VSS, API WMI / PowerShell
  • VMware vSphereVADP + CBT (QueryChangedDiskAreas) via VDDK — SDK propriétaire, licence à surveiller côté Broadcom
  • Bare metalImage bloc + BMR : snapshot volume (VSS / LVM), média de restauration bootable, matériel dissemblable
  • Cohérence applicativeQuiescing invité (VSS, pre/post scripts) vs crash-consistent
  • Modes de restaurationVM complète, fichier depuis image, bare metal ; instant recovery plus tard