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 job | zfs send | zfs receive | |
|---|---|---|
| Ce qui est copié | un second datastore, rempli snapshot par snapshot | le datastore lui-même : chunks, propriétaires, namespaces, état de vérification |
| Où se branche le support | sur le PBS qui déclare le datastore amovible | sur n'importe quelle machine ZFS ; une seule peut alimenter plusieurs disques |
| Prérequis | aucun système de fichiers imposé | ZFS des deux côtés, un dataset par datastore |
| Historique | transfer-last : seulement les N derniers snapshots par groupe | tout le contenu du dataset, sans filtre |
| Mise à jour | comparaison des index, puis envoi des chunks manquants | les blocs modifiés depuis le dernier point commun |
| Disposition sur le support | des fichiers neufs, écrits selon les réglages du support | celle 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.
S=offline-$(date -u +%Y%m%dT%H%M)
zfs snapshot rpool/datastores/clientX@$S
zfs bookmark rpool/datastores/clientX@$S rpool/datastores/clientX#$SSnapshots 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
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.2fige 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, etzpool importrefuse alors le disque : le figer d'avance, c'est garantir qu'il s'importera aussi sur un serveur moins à jour ;ashift=12explicite : 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
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 acquissend -L -cenvoie les blocs tels qu'ils sont compressés sur disque, sans les décompresser ni redécouper ceux de plus de 128 Kio ;receive -srend 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-pou-R) ;pv -Lplafonne 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.
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/clientXNe 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 :
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 erreurLe 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.
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
| Option | Ce qu'elle fait | Quand 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 -c | transporte les blocs compressés tels quels | toujours, pour le complet comme pour les incrémentaux |
send -L | conserve 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 -nP | calcule la taille du flux sans rien envoyer | avant 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 -s | garde l'état partiel en cas d'interruption | à chaque envoi, incrémentaux compris |
receive -u | ne monte pas le dataset | à chaque envoi : un dataset monté finit par être écrit |
receive -A <dataset> | abandonne un état partiel reprenable | quand la reprise n'est plus possible et qu'il faut relancer l'incrémental |
receive -F | annule les écritures faites sur la cible depuis son dernier snapshot, et peut détruire des snapshots de la cible | jamais par réflexe : seulement en sachant ce qu'on écrase |
- si
zfs receiverefuse 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 mountpointne 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
zpool export airgap-A1 # toujours avant de débrancher, jamais aprèsSi le disque est en USB, quatre vérifications, une fois par modèle de boîtier :
- UAS ou BOT (
lsusb -t, chercherDriver=uas). En modeusb-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-dne passe, ce boîtier ne permet pas de surveiller le disque ; - le numéro de série vu dans
/dev/disk/by-idest 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 serveurQuestions 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.
