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
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.
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érenceplus 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
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 ?
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.
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
]
}
addrpour aller chercher le blob,clépour le déchiffrer,len_clairpour 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
holedit « 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.
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
{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.
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
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
| Outil | min | moyenne | max |
|---|---|---|---|
| restic | 512 Kio | ~1 Mio | 8 Mio |
| rustic | 512 Kio | ~1 Mio | 8 Mio |
| borg | 512 Kio | 2 Mio | 8 Mio |
| kopia | 2 Mio | 4 Mio | 8 Mio |
| Cairn | 512 Kio | 2 Mio | 8 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
clé_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
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.
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.
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é
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
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à.
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
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.
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.
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.
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é »
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é
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).
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.
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-runqui 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
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
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
../../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.
Le problème
Le premier backup est long — bien plus long que l’intuition ne le dit.
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
# 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
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.
La règle d’or
L’inventaire par type de fichier
| Type | Au backup | À la restauration |
|---|---|---|
| Fichier ordinaire | contenu (flux) + méta | ✓ |
| Répertoire | entrées + méta | ✓ |
| Symlink | la cible (chaîne brute, jamais suivie) + méta | recréé tel quel |
| Lien dur | groupe dev+inode → contenu stocké une fois | 1ʳᵉ occurrence = contenu, suivantes = lien |
| Sparse | trous détectés (SEEK_HOLE) → entrées hole | trous recréés, zéro octet stocké |
| Device (char/bloc) | méta seulement (major/minor) — on ne lit jamais le contenu | recréé si root, sinon rapporté |
| FIFO | méta seulement | recréé |
| Socket | ignoré (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
- 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é parsynoacltool). 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 Linux —
security.capability(voir l'encart rouge). - Contexte SELinux —
security.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é (
statxbtime, 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 : ioctlFS_IOC_GETFLAGS, un chemin de capture séparé. L'immutable exigeCAP_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 namespaces —
user.*,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.
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
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ègeSeBackupPrivilege) : 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 xattrstrusted.*; 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.
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
| Plateforme | Mécanisme | Notes |
|---|---|---|
| Windows | VSS, activé par défaut | L’agent est requester ; snapshot par volume ; restic ne le fait qu’en opt-in, on vise mieux |
| macOS | Snapshot APFS (fs_snapshot_create) | root + Full Disk Access requis |
| Linux — btrfs | Snapshot de sous-volume | Instantané — cas Synology, mais root requis : en paquet non-root (nominal), c’est l’échelle de dégradation qui s’applique |
| Linux — ZFS | Snapshot ZFS | Le cas QNAP QuTS hero |
| Linux — LVM | Snapshot LVM | Si extents libres ; device-mapper gèle le FS automatiquement |
| Linux — ext4 nu | aucun 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é.
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é.
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-snapshotpour 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.
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
| Plateforme | Format | Service | Signature |
|---|---|---|---|
| Linux | binaire statique + deb/rpm | systemd | checksums / sigstore |
| Windows x86_64 | MSI | Service Windows (SYSTEM) | Authenticode EV — sinon SmartScreen effraie les clients |
| macOS | PKG, binaire universel arm64+x86_64 | launchd daemon | Developer ID + notarisation (obligatoire depuis 10.15) |
| Synology | SPK (métadonnées DSM 7) | init DSM, non-root | dépôt tiers — voir décision 3 |
| QNAP | QPKG (QDK) | init QTS | signature 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
À 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 :
| Plateforme | L’agent tourne en | Ce que ça permet | Ce qui manque sinon |
|---|---|---|---|
| Linux serveur | root | snapshots LVM/btrfs/ZFS, trusted.*, tout le FS | — |
| Windows | SYSTEM | VSS, tous les fichiers, SACL | sans SYSTEM : pas de VSS ni fichiers verrouillés |
| macOS | root + Full Disk Access (TCC) | tout le FS — sans FDA, macOS cache Mail, Photos… même à root | FDA à accorder (déployable par MDM) |
| Synology | utilisateur de paquet (nominal) | partages accordés + leurs SynoACL/xattrs | fichiers système, trusted.*, snapshots — rapportés |
| QNAP | selon 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.
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 API —
cairn 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).
- Politiques —
fast/paranoid/forcedu scan,require-snapshotdes 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
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
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.
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 --oneshotsous 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.
Décision 4 — La supervision du daemon : le silence est l’ennemi
- 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 statuspour 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é.
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
{"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 ?
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ôle | Crate | Note |
|---|---|---|
| Runtime async | tokio | Le standard de facto |
| HTTP/2 serveur | axum (sur hyper + tower) | Middlewares tower : timeouts, rate-limit, retry |
| HTTP/2 client | reqwest (ou hyper direct) | Pooling, multiplexage H2 |
| TLS | rustls + provider aws-lc-rs | X25519MLKEM768 (hybride PQ) disponible |
| CBOR | minicbor | Déjà choisi pour les nœuds — encodeur canonique partagé |
| HTTP/3 (futur) | quinn + h3 | Pour 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 :
→ 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
La solution — le ticket de consigne
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.
- Commit —
POST /v1/sessions/{id}/commitavec 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
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
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
| Endpoint | Rôle | Particularité |
|---|---|---|
POST /v1/sessions · …/commit | ouvrir / committer un backup | session-promesse, commit atomique |
POST /v1/chunks/exists · /v1/meta/exists | dédup par lots | bitmap, épinglage à la session |
PUT /v1/chunks/{addr} · /v1/meta/{addr} | upload | rehash à la porte, idempotent, durable avant ack |
POST /v1/chunks/batch-get | restauration | picking par rayon, flux framé |
GET /v1/chunks/{addr} · /v1/meta/{addr} | lecture unitaire | navigation, arbre parent, debug |
GET /v1/snapshots | refs des racines | seul contact avec la DB de contrôle |
GET /v1/capabilities | négociation | tailles 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
lockedde la config…) se fait sur le dashboard, avec une identité humaine.
Les options écartées
La décision — la carte d’identité et le ticket journalier
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
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é
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
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
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.
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
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
Décision 4 — Le bloom filter, côté sortie
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
Le cycle complet
Le cycle du GC — par tenant, en tâche de fond, sans verrou
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.
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 :
| Objet | Taille | Convergent ? | Check d’existence ? | Lecture typique |
|---|---|---|---|---|
| Chunks | 0,5–8 Mio | oui | oui (dédup) | restauration |
| Manifestes | 80 o – 100 Kio | oui | oui (dédup) | restauration, GC |
| Répertoires / racines | petits | non | non (jamais dédupliqués) | début de backup, navigation, GC |
| État mutable (tenants, profils épinglés, refs de snapshots, époques GC, quotas) | minuscule | — | — | transactionnel |
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
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é
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é.
[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.
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.
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
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é
append au segment ouvert → fsync (groupé) → insertion index → ack au client
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.
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.
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 :
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.
aead.
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
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)
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).
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
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é.
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é.
Rotation — la loi de 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.
| Événement | Ce qui tourne | Coût | Fréquence réelle |
|---|---|---|---|
| Changement de passphrase, départ d’un employé du revendeur | KEK (le code du coffre) | ~zéro, instantané | Aussi souvent qu’on veut |
| Machine suspectée compromise | CMK (le trousseau) | Borné — re-chiffrer les métadonnées du client | Rare — réponse à incident |
| CK prouvée compromise | CK, par attrition (époques) | Élevé mais étalé | Exceptionnel — jamais en routine |
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.
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 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.
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.
KeyVault vers sa propre instance OpenBao ou son HSM.
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.
L’inventaire Cairn face au quantique
| Brique | Type | Verdict quantique |
|---|---|---|
| XChaCha20-Poly1305 (256 bits) | symétrique | ✅ résistant natif (Grover → 128 bits, largement assez) |
| BLAKE3 (adresses, dérivation) | hash 256 bits | ✅ résistant natif |
| Argon2id | KDF memory-hard | ✅ résistant (la mémoire-dure gêne aussi le quantique) |
| Pyramide CK / CMK / KEK | emballages symétriques | ✅ résistant natif |
| TLS transport | asymétrique (X25519) | ⚠️ cassable par Shor → à hybrider |
| Signatures de mise à jour | asymé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
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
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 :
tenant = client final
Isolation maximale. Quasi aucune dédup — rien n'est partagé entre clients d'un même revendeur.
isolation maxtenant = revendeur
Dédup sur tout le parc du revendeur (OS, docs communs → grosses économies). Oracle confiné à l'intérieur.
recommandétenant = éditeur
Dédup maximale, mais l'oracle traverse les revendeurs concurrents. À éviter.
oracle globalLe 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.
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
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églage | Grille | ~Snapshots | Ce que le client retrouve |
|---|---|---|---|
| Inclus — 90 jours | 7 derniers + 1/jour × 30 + 1/sem × 9 | ~42 | n’importe quel jour du dernier mois, n’importe quelle semaine des 2 mois d’avant |
| Option — 1 an | idem + 1/mois × 12 | ~54 | + n’importe quel mois de l’année |
| Réglable | chaque paramètre, sous le plafond du plan | — | jusqu’à 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
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
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.
Par plateforme
| Plateforme | API | CBT | Transport disque |
|---|---|---|---|
| Proxmox VE | API Proxmox / QMP | dirty bitmaps QEMU | storage / NBD |
| XCP-ng | XAPI | CBT XAPI (list_changed_blocks) | NBD |
| Hyper-V | WMI / PowerShell | RCT | SMB / API |
| VMware vSphere | VADP (vCenter / ESXi) | CBT (QueryChangedDiskAreas) | VDDK : NBD / hotadd / SAN |
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
| Section | ✓ | ◐ | ○ | |
|---|---|---|---|---|
| A | Client / agent | 14 | 0 | 1 |
| B | Protocole & API | 6 | 0 | 0 |
| C | Backend / services serveur | 2 | 0 | 9 |
| D | Stockage | 3 | 1 | 2 |
| E | Bases de données & métadonnées | 1 | 0 | 2 |
| F | Cryptographie & clés | 8 | 0 | 0 |
| G | Multi-tenant & commercial | 1 | 0 | 6 |
| H | Interfaces web | 0 | 0 | 6 |
| I | Sécurité | 1 | 1 | 6 |
| J | Haute disponibilité & durabilité du service | 0 | 1 | 3 |
| K | Observabilité & exploitation | 0 | 0 | 3 |
| L | Déploiement & distribution | 0 | 0 | 4 |
| M | Cycle de vie des données & politiques | 1 | 1 | 2 |
| N | Qualité, tests & correction | 0 | 0 | 3 |
| O | Transverse | 0 | 0 | 4 |
| P | Virtualisation & bare metal | 0 | 2 | 7 |
| Total | 37 | 6 | 58 |
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