Retour au blogAir gap · Guide technique

    Mettre un datastore PBS hors ligne avec zfs send

    Copie hors ligne d'un datastore Proxmox Backup Server par zfs send et zfs receive over SSH : la copie tire, la production ne pousse jamais. Commandes, options des incrémentaux, pièges.

    11 min de lecture

    Une copie hors ligne ne vaut que si la production ne peut pas l'atteindre. Quand le datastore Proxmox Backup Server est posé sur ZFS, la façon la plus directe de la faire est de répliquer le dataset lui-même, par zfs send et zfs receive à travers SSH, vers un disque qu'on débranche ensuite.

    C'est une partie de la méthode que nous utilisons pour mettre hors ligne les datastores de nos clients, notamment pour AirGapped Drive PBS. La voici, commandes comprises.

    Deux façons de sortir un datastore PBS

    PBS propose sa propre voie : un datastore amovible alimenté par un sync job. La réplication ZFS en est une autre. Elles ne copient pas la même chose.

    Datastore amovible + sync jobzfs send | zfs receive
    Ce qui est copiéun second datastore, rempli snapshot par snapshotle datastore lui-même : chunks, propriétaires, namespaces, état de vérification
    Où se branche le supportsur le PBS qui déclare le datastore amoviblesur n'importe quelle machine ZFS ; une seule peut alimenter plusieurs disques
    Prérequisaucun système de fichiers imposéZFS des deux côtés, un dataset par datastore
    Historiquetransfer-last : seulement les N derniers snapshots par groupetout le contenu du dataset, sans filtre
    Mise à jourcomparaison des index, puis envoi des chunks manquantsles blocs modifiés depuis le dernier point commun
    Disposition sur le supportdes fichiers neufs, écrits selon les réglages du supportcelle d'origine : un recordsize différent sur le disque ne redécoupe rien

    Le sync job n'a rien d'un second choix : il ne demande pas ZFS, et transfer-last divise le volume quand la copie hors ligne n'a pas besoin de tout l'historique. Nous avons retenu ZFS parce que nos datastores y sont déjà, et que la copie est alors le datastore même : rien à reconstituer le jour où il faut le relire.

    Et un rsync du répertoire .chunks ? Possible sur un datastore à l'arrêt. Pendant que PBS écrit ou que le garbage collector tourne, un parcours de millions de fichiers n'a aucun point de cohérence. Le snapshot ZFS, lui, est atomique : c'est ce qui permet de copier à chaud.

    Le principe : c'est la copie qui tire

    La machine qui porte le disque va chercher les données, en SSH. La production ne pousse jamais. Ce n'est pas un détail d'implémentation, c'est ce qui fait tenir le reste :

    • la production ne détient aucun identifiant vers la copie, et n'a pas de route vers elle ;
    • si la production est compromise, un attaquant qui y obtient root peut voir qu'une copie vient se servir — une clé autorisée, des connexions dans les journaux —, mais il n'a ni identifiant ni route pour ouvrir une connexion vers elle ;
    • c'est exactement le modèle d'un support qu'on débranche — il vient se servir, puis il disparaît.

    Côté pare-feu, une seule ouverture : de la copie vers la production, en SSH. Les snapshots, et leur destruction, restent décidés par la production elle-même.

    1. Sur la production : une clé SSH pour la copie

    La copie se connecte à la production avec une clé SSH autorisée sur la production. C'est tout ce dont le montage a besoin, et la production, elle, n'a aucune clé vers la copie. Restreignez cette clé autant que votre exploitation le permet (restrict, from=, commande forcée) : sans restriction, qui prend la main sur la machine de copie la prend aussi sur la production.

    La façon dont nous gérons nos propres accès fait partie de notre exploitation : elle n'est pas détaillée ici.

    Qui prend les snapshots ? La production, par son propre timer : elle garde la main sur ses snapshots. Le même timer pose un bookmark sur chaque snapshot : cela ne coûte rien, et c'est ce qui servira d'origine aux envois suivants.

    Sur la production, par timer (systemd ou cron)
    S=offline-$(date -u +%Y%m%dT%H%M)
    zfs snapshot rpool/datastores/clientX@$S
    zfs bookmark rpool/datastores/clientX@$S rpool/datastores/clientX#$S

    Snapshots courts, bookmarks longs

    Un snapshot gardé des semaines retient tous les blocs que le garbage collector de PBS libère : le pool de production gonfle en silence. Les snapshots se détruisent donc après quelques jours ; les bookmarks, qui ne retiennent aucun bloc, se gardent tant qu'un disque de la rotation en dépend. Un envoi complet, lui, part toujours d'un snapshot encore présent.

    2. Sur la copie : préparer le disque

    Sur la machine qui porte les disques
    ls -l /dev/disk/by-id/ | grep -v part           # le disque, par son identifiant stable
    
    zpool create -o ashift=12 -o compatibility=openzfs-2.2 -O atime=on airgap-A1 /dev/disk/by-id/<disque>
    zfs create -o mountpoint=/mnt/datastore airgap-A1/copies
    • /dev/disk/by-id, jamais /dev/sdX, qui change d'un branchement à l'autre ;
    • compatibility=openzfs-2.2 fige les fonctionnalités du pool à celles d'OpenZFS 2.2 (ou le niveau de votre choix, cf. /usr/share/zfs/compatibility.d/). Un pool créé sur un OpenZFS plus récent peut activer des fonctionnalités qu'un OpenZFS plus ancien ne connaît pas, et zpool import refuse alors le disque : le figer d'avance, c'est garantir qu'il s'importera aussi sur un serveur moins à jour ;
    • ashift=12 explicite : un pont USB ou un LUN annonce parfois des secteurs de 512 octets sur un disque qui travaille en 4 Kio ;
    • atime=on : le garbage collector de PBS s'appuie sur l'atime des chunks, et PBS le contrôle à la déclaration du datastore ;
    • le point de montage sur le parent : les datasets reçus atterriront directement sous /mnt/datastore/<nom>.

    Un disque qui quitte la salle peut se perdre : chiffrez le support, et ne rangez jamais la clé avec le disque. Le chiffrement côté client ne dispense pas de le faire — il protège le contenu des sauvegardes, pas le support, et il reste optionnel dans PBS.

    Chez Nimbus, les disques que nous mettons au coffre sont chiffrés, que vous chiffriez vos sauvegardes de votre côté ou non. Si vous le faites — ce que nous recommandons —, les deux protections s'empilent sans dépendre l'une de l'autre : la nôtre protège le support, la vôtre protège le contenu. La façon dont nous chiffrons et gardons les clés fait partie de notre exploitation ; elle n'est pas publiée ici, et les commandes de cet article sont données en clair.

    3. La première copie

    Sur la copie — rien n'est initié depuis la production
    set -o pipefail
    SRC=rpool/datastores/clientX
    PROD="-i /root/.ssh/id_ed25519_copie pbs-production"
    
    ssh $PROD zfs list -t snapshot,bookmark -o name,creation -r $SRC    # ce qui est disponible
    ssh $PROD zfs send -nP -L -c $SRC@offline-20260914T0700                # volume estimé, rien n'est transféré
    
    ssh $PROD zfs send -L -c $SRC@offline-20260914T0700 \
      | pv -L 20m \
      | zfs receive -s -u -x mountpoint airgap-A1/copies/clientX
    echo "PIPESTATUS=${PIPESTATUS[*]}"                                  # trois 0, sinon rien n'est acquis
    • send -L -c envoie les blocs tels qu'ils sont compressés sur disque, sans les décompresser ni redécouper ceux de plus de 128 Kio ;
    • receive -s rend la réception reprenable : si la liaison tombe, on repart où l'on s'était arrêté au lieu de tout recommencer ;
    • -u : le dataset reçu n'est pas monté ;
    • -x mountpoint : le dataset reçu prend le point de montage de son parent local, même si le flux transporte des propriétés (envoi avec -p ou -R) ;
    • pv -L plafonne le débit, pour ne pas manger la ligne pendant les sauvegardes ;
    • PIPESTATUS : trois commandes, trois codes de retour. Un seul regard sur le dernier ne suffit pas.
    Reprendre un envoi interrompu
    TOKEN=$(zfs get -H -o value receive_resume_token airgap-A1/copies/clientX)
    ssh $PROD zfs send -t $TOKEN | pv -L 20m | zfs receive -s -u airgap-A1/copies/clientX

    Ne vous fiez pas aux premières minutes : l'envoi commence par parcourir les métadonnées de millions de chunks, cache froid, et démarre lentement avant de trouver son régime.

    4. Vérifier la copie avant qu'elle parte au coffre

    On ne déclare jamais le dataset reçu lui-même dans PBS pour un contrôle de routine : un datastore en service écrit (verrous, atime), le dataset diverge, et l'incrémental suivant est refusé. On relit un clone jetable — ce qui suppose que PBS soit installé sur la machine qui porte les disques, ZFS seul ne suffit pas :

    Sur la copie
    zfs clone -o readonly=off -o mountpoint=/mnt/controle \
      airgap-A1/copies/clientX@offline-20260914T0700 airgap-A1/controle
    
    proxmox-backup-manager datastore create controle /mnt/controle --reuse-datastore true
    proxmox-backup-manager verify controle --ignore-verified false
    
    proxmox-backup-manager datastore remove controle   # retire la déclaration, pas les données
    zfs destroy airgap-A1/controle
    zpool status airgap-A1                             # 0 erreur

    Le verify qui ne vérifie rien

    L'état de vérification voyage avec la réplication : la copie arrive avec l'historique de verify de la source. Un verify ordinaire répond alors SKIPPED: … (recently verified) sans lire un octet, et l'on croit avoir contrôlé un disque qu'on n'a pas relu. --ignore-verified false force la relecture réelle des chunks. Au quotidien, cet héritage est pratique ; pour tester un support, il est trompeur.

    5. Au retour d'un disque : l'incrémental

    Chaque disque revient avec un retard différent, donc repart de son dernier envoi. Le dernier snapshot présent sur le disque porte le même nom qu'un bookmark côté production : c'est l'origine de l'incrémental.

    Sur la copie
    zpool import airgap-A1
    zfs list -t snapshot -o name airgap-A1/copies/clientX       # dernier snapshot reçu par CE disque
    
    ssh $PROD zfs send -nP -L -c -i $SRC#offline-20260829T0700 $SRC@offline-20260914T0700   # volume du delta
    ssh $PROD zfs send -L -c -i $SRC#offline-20260829T0700 $SRC@offline-20260914T0700 \
      | pv -L 20m | zfs receive -s -u airgap-A1/copies/clientX
    echo "PIPESTATUS=${PIPESTATUS[*]}"

    Les options qui comptent pour un incrémental

    OptionCe qu'elle faitQuand s'en servir
    send -i <origine> <snap>envoie seulement la différence entre l'origine et le snapshot. L'origine peut être un bookmark.le cas normal : origine = bookmark du dernier envoi sur ce disque
    send -I <origine> <snap>envoie aussi tous les snapshots intermédiaires. L'origine doit être un snapshot, pas un bookmark.rarement utile ici : le datastore porte déjà son propre historique
    send -ctransporte les blocs compressés tels quelstoujours, pour le complet comme pour les incrémentaux
    send -Lconserve les blocs de plus de 128 Kio. Sans lui, -c les décompresse et les redécoupe.toujours, avec -c : sans effet quand les blocs font 128 Kio ou moins, indispensable si le recordsize du datastore dépasse ou a dépassé 128 Kio
    send -nPcalcule la taille du flux sans rien envoyeravant chaque rotation, pour savoir ce qu'elle va coûter
    send -t <jeton>reprend un envoi interrompu là où la réception l'a laisséaprès une coupure, avec le receive_resume_token de la cible
    receive -sgarde l'état partiel en cas d'interruptionà chaque envoi, incrémentaux compris
    receive -une monte pas le datasetà chaque envoi : un dataset monté finit par être écrit
    receive -A <dataset>abandonne un état partiel reprenablequand la reprise n'est plus possible et qu'il faut relancer l'incrémental
    receive -Fannule les écritures faites sur la cible depuis son dernier snapshot, et peut détruire des snapshots de la ciblejamais par réflexe : seulement en sachant ce qu'on écrase
    • si zfs receive refuse parce que la destination a été modifiée, quelqu'un a écrit sur le dataset reçu — le plus souvent en le montant ;
    • si le bookmark a disparu côté production, il n'y a plus de point commun : on repart sur une copie complète ;
    • -x mountpoint ne sert qu'au premier envoi, qui crée le dataset ;
    • sur le disque, seul le dernier snapshot reçu sert au prochain incrémental. Les précédents retiennent des blocs que PBS a libérés depuis.

    6. Débrancher

    Sur la copie
    zpool export airgap-A1        # toujours avant de débrancher, jamais après

    Si le disque est en USB, quatre vérifications, une fois par modèle de boîtier :

    • UAS ou BOT (lsusb -t, chercher Driver=uas). En mode usb-storage, une commande à la fois : le débit plafonne. Si c'est le noyau qui a désactivé UAS pour ce pont, il a ses raisons — changez de boîtier plutôt que de forcer ;
    • SMART à travers le pont : smartctl -a -d sat. Si aucun -d ne passe, ce boîtier ne permet pas de surveiller le disque ;
    • le numéro de série vu dans /dev/disk/by-id est parfois celui du pont, identique pour tous les disques. Fiez-vous au nom du pool, que ZFS lit sur le disque lui-même ;
    • un boîtier alimenté pour un 3,5 pouces. Une déconnexion en pleine écriture suspend le pool : zpool clear, puis reprise au jeton.

    Ce que cette copie apporte, et ce qu'elle n'apporte pas

    • Débranché, le disque est hors d'atteinte : un ransomware ne chiffre ni n'efface un support connecté à rien ;
    • une production compromise ne mène pas à la copie : la copie tire, et la production n'a ni identifiant ni route vers elle ;
    • un disque perdu ou volé ne livre rien sans sa clé de chiffrement, qui ne voyage jamais avec lui ;
    • avec le chiffrement client activé, le contenu reste illisible pour qui n'a pas votre clé — nous compris ;
    • ce qui a été sauvegardé depuis le dernier envoi n'est pas sur le disque : la copie hors ligne a l'âge de sa dernière mise à jour ;
    • une suppression sur la production se recopie : si les sauvegardes y sont effacées, l'incrémental suivant reporte l'effacement sur le disque rafraîchi. C'est voulu — la copie est le datastore lui-même. Ce qui protège alors, ce sont les autres supports, restés au coffre avec l'état d'avant ;
    • une copie jamais relue n'est pas une sauvegarde : le verify forcé, puis une vraie restauration de temps en temps, sont la seule preuve.

    Et après ?

    Le jour où il faut s'en servir, le disque sort du coffre et son datastore doit repartir ailleurs. Voici comment nous réactivons un simple datastore sur un nouveau PBS : import, déclaration, accès, Proxmox VE, et les preuves qu'on peut restaurer.

    Réactiver un datastore PBS après la perte du serveur

    Questions fréquentes

    Pourquoi zfs send plutôt qu'un sync job PBS pour la copie hors ligne ?

    Le sync job vers un datastore amovible est la voie officielle, et il a de vrais atouts : il ne demande pas ZFS, et l'option transfer-last limite la copie aux derniers snapshots de chaque groupe. zfs send copie le datastore lui-même, avec ses propriétaires, ses namespaces et son état de vérification, et une seule machine peut alimenter plusieurs disques. Nos datastores sont déjà sur ZFS : la production n'a alors qu'un flux à fournir.

    Un PBS de production compromis peut-il atteindre la copie hors ligne ?

    Pas par ce montage. C'est la copie qui se connecte à la production en SSH, jamais l'inverse : la production ne détient aucun identifiant vers la copie et n'a pas de route vers elle. Un attaquant qui obtient root sur la production peut constater qu'une copie vient se servir, mais il n'a aucun moyen d'ouvrir une connexion vers elle. Et une fois le disque débranché, il n'est plus connecté à rien.

    Pourquoi un bookmark plutôt qu'un snapshot conservé côté production ?

    Un snapshot conservé des semaines retient tous les blocs que le garbage collector de PBS libère entre-temps : le pool de production gonfle. Un bookmark ne retient aucun bloc, et il suffit comme origine d'un envoi incrémental avec zfs send -i. Chaque disque en rotation a besoin du bookmark de son dernier envoi.

    Pourquoi zfs receive refuse-t-il l'incrémental avec « destination has been modified » ?

    Parce que le dataset reçu a été écrit depuis son dernier snapshot — typiquement parce qu'il a été monté et déclaré dans PBS, qui écrit des verrous et met à jour l'atime. C'est pour cela qu'on contrôle la copie sur un clone jetable. zfs receive -F annulerait ces écritures, mais il peut aussi détruire des snapshots de la cible : à n'utiliser qu'en sachant ce qu'on écrase.

    Faut-il refaire un verify quand le disque revient du coffre ?

    Oui si l'on veut une preuve de lisibilité, et il faut alors le forcer. L'état de vérification voyage avec la réplication : la copie arrive avec l'historique de verify de la source, et un verify ordinaire répond « recently verified » sans rien lire. proxmox-backup-manager verify <datastore> --ignore-verified false relit réellement les chunks.

    Le hors ligne, sans les disques à gérer

    AirGapped Drive PBS : copie en ligne et rotation hors ligne au coffre, à 34 € HT/To/mois. Les disques, leur contrôle et leur rotation sont notre affaire.

    Vous voulez être seul sur vos supports, et pouvoir vous les faire prêter — disques dédiés ou PBS hors ligne qui démarre, sous réserve de caution ? Les supports dédiés se chiffrent sur devis.