Aller au contenu

Sauvegardes chiffrées

Le SDK peut sauvegarder votre base de données ou n'importe quel fichier/dossier dans le coffre zéro-knowledge de Quiet Guard. Tout est chiffré sur votre serveur avant l'envoi, avec une clé que seule votre équipe peut déverrouiller. Le serveur ne stocke qu'un blob opaque et ne voit jamais ni vos données ni votre clé.

Cette page décrit la dernière version du paquet. Les commandes et options apparues après la v0.1 portent la version qui les a introduites ; si l'une d'elles répond « command not found » ou « option does not exist », c'est que votre installation est plus ancienne. Vérifiez la vôtre avec composer show quiet-guard/laravel-monitor, et mettez à jour avec composer update quiet-guard/laravel-monitor.

Prérequis

  • Le chiffrement doit être activé pour votre équipe dans le dashboard Quiet Guard (cela génère la paire de clés de l'équipe et est protégé par une passphrase d'équipe). Le coffre de sauvegarde est une fonctionnalité payante soumise à un quota de stockage. Voir la documentation du serveur.
  • PHP compilé avec libsodium (ext-sodium, fourni par défaut depuis PHP 7.2).
  • Pour les bases non-SQLite, le binaire de dump correspondant sur l'hôte : mysqldump (MySQL / MariaDB) ou pg_dump (PostgreSQL). Les bases SQLite sont copiées directement.

Créer une sauvegarde

bash
# Sauvegarder la connexion de base de données par défaut
php artisan monitor:backup --database

# Sauvegarder un fichier ou un dossier
php artisan monitor:backup --path=/var/www/storage/app

# Ajouter un libellé lisible
php artisan monitor:backup --database --name="nightly"

# Ne garder que les sept sauvegardes de base les plus récentes
php artisan monitor:backup --database --name="nightly" --keep=7

Options

OptionDescription
--databaseSauvegarde la connexion de base de données par défaut (config('database.default')).
--path=Sauvegarde un fichier unique ou un dossier entier.
--name=Libellé optionnel stocké avec la sauvegarde. ASCII imprimable, sans barre oblique, 255 caractères au plus : ce libellé devient le nom de fichier sous lequel le téléchargement est proposé, et un séparateur de chemin n'est pas quelque chose qu'un en-tête HTTP sait porter. Le serveur répond 422 et nomme le champ lorsqu'il en refuse un.
--keep=Depuis v0.2.1. Après un envoi réussi, conserve les N sauvegardes les plus récentes du même type et supprime les autres. Au moins 1.
--exclude-table=Depuis v0.5.0. Avec --database, garde la structure de cette table mais laisse ses lignes hors de la sauvegarde. Répétable. Lettres, chiffres, _, $ et - seulement : un nom qualifié (base.table) ou un joker est refusé, et la commande s'arrête plutôt que de sauvegarder la table quand même.

Vous devez passer soit --database, soit --path, et c'est vérifié avant l'appel réseau : une commande sans cible se l'entend dire, au lieu de recevoir une réponse sans rapport à propos du chiffrement. Lorsque MONITOR_ENABLED=false, la commande ne fait rien.

Lorsqu'une étape est refusée, la commande affiche la phrase du serveur plutôt qu'une supposition : un 422 qui nomme le champ refusé, un 413 qui nomme le quota de stockage plein, un 403 sur un forfait sans sauvegardes. Ces quatre refus arrivaient auparavant sous la forme d'un même conseil, qui n'en réparait aucun des trois derniers.

Comment l'archive est construite

  • --database: les connexions SQLite sont copiées telles quelles ; MySQL/MariaDB sont exportées avec mysqldump, PostgreSQL avec pg_dump. Le mot de passe est passé via l'environnement (MYSQL_PWD / PGPASSWORD), jamais sur la ligne de commande. Le dump s'exécute avec un timeout de 600 secondes. La sauvegarde est enregistrée avec le type database.
  • --path: un dossier est empaqueté dans une archive tar (un fichier unique est ajouté à un tar), enregistrée avec le type files.

Exclure des tables

Depuis v0.5.0. Certaines tables ne doivent pas survivre dans une sauvegarde : des sels quotidiens qu'une politique de confidentialité promet de détruire sous quelques heures, des sessions, un cache. Une sauvegarde de nuit les garderait pendant toute la rétention du coffre.

bash
php artisan monitor:backup --database --exclude-table=salts --exclude-table=sessions --keep=7

La structure de la table est conservée, seules ses lignes restent dehors : une base restaurée à laquelle manque une table casse l'application qui la restaure, alors qu'une table vide est exactement ce qu'on a demandé.

  • MySQL / MariaDB : deux passes de mysqldump, chacune avec --single-transaction. La première écrit la structure de toutes les tables (--no-data), la seconde les lignes de toutes sauf les tables exclues (--no-create-info --skip-triggers --ignore-table=…).
  • PostgreSQL : une passe de pg_dump avec --exclude-table-data=….
  • SQLite : la copie est vidée des tables exclues puis compactée (VACUUM), sans quoi les lignes supprimées resteraient lisibles dans les pages libres du fichier. La base d'origine n'est pas touchée.

Une table qui n'existe pas est ignorée, comme mysqldump ignore un --ignore-table qui ne désigne rien : il n'y a pas de lignes à laisser partir.

Faire tourner les anciennes sauvegardes

Depuis v0.2.1.

Rien, côté serveur, ne supprime une sauvegarde à votre place : les purges de rétention couvrent les données de supervision, pas le coffre. Une planification nocturne remplit donc l'allocation de stockage du forfait, puis échoue chaque nuit, depuis le planificateur, là où personne ne lit. --keep=N referme cela :

bash
php artisan monitor:backup --database --name=nightly --keep=7

Trois propriétés méritent d'être connues :

  • la rotation n'a lieu qu'après un envoi réussi, si bien que la nuit où le dump échoue n'échange jamais un coffre plein contre un coffre vide ;
  • elle ne supprime jamais que des sauvegardes du même type : une rotation nocturne --database laisse tranquille la sauvegarde de dossier que vous avez prise à la main ;
  • une suppression qui échoue est signalée mais ne fait pas échouer la commande : la nouvelle sauvegarde est envoyée et à l'abri, le coffre est seulement plus rempli que demandé, et dire l'inverse à un planificateur reviendrait à annoncer que la sauvegarde n'a pas eu lieu.

Lister ce que contient le coffre

Depuis v0.2.1.

monitor:restore prend un identifiant de sauvegarde comme unique argument obligatoire, et le dashboard était le seul endroit où le lire. monitor:backups l'affiche depuis le shell :

bash
php artisan monitor:backups

Une ligne par sauvegarde, la plus récente d'abord, avec id, type, name, size et created, suivie de la ligne monitor:restore à copier. Un coffre vide le dit et renvoie vers monitor:backup --database. Si l'appel est refusé, ce que le serveur a répondu est affiché : une mauvaise clé, une mauvaise adresse et un forfait sans sauvegardes ne se lisent plus de la même façon.

Restaurer une sauvegarde

bash
php artisan monitor:restore <id>

# Choisir où écrire la sortie déchiffrée
php artisan monitor:restore <id> --output=/tmp/restore.sql

# Fournir la passphrase de façon non interactive (sinon elle est demandée)
php artisan monitor:restore <id> --passphrase="…"

# Déchiffrer un blob déjà téléchargé depuis le dashboard
php artisan monitor:restore --file=sauvegarde.enc --output=/tmp/restore.sql

Options

Argument / optionDescription
id (argument)L'identifiant de la sauvegarde : affiché par monitor:backups, renvoyé par monitor:backup après l'envoi, et lisible dans la colonne Identifiant de l'onglet Sauvegardes du dashboard.
--output=Où écrire le résultat déchiffré. Par défaut restored-<id>.out dans le chemin de base de l'application.
--passphrase=La passphrase d'équipe. Demandée de façon sécurisée (saisie masquée) si omise.
--file=À paraître. Déchiffre un blob que vous avez déjà téléchargé, au lieu d'aller le chercher. L'identifiant devient facultatif ; votre fichier n'est jamais supprimé.

Le bouton Télécharger du dashboard rend le fichier .enc, c'est-à-dire le blob scellé : le serveur ne détient rien d'autre, et rien ne l'ouvre à la main. --file= est ce qui le déchiffre. La matière de clé reste demandée au serveur, puisque la clé privée n'existe qu'enveloppée par votre passphrase et n'est jamais stockée localement ; sans identifiant, la somme de contrôle n'est comparée à rien et la commande le dit plutôt que de le laisser croire.

Avant tout déchiffrement, le client compare le blob téléchargé au sha256 que le serveur publie pour lui. Une somme qui ne correspond pas arrête là, avec un message qui lui est propre, et rien n'est écrit vers --output. Un téléchargement abîmé échouait auparavant sur « Decryption failed », c'est-à-dire exactement ce que produit une mauvaise passphrase, et ces deux causes ne se réparent pas du tout de la même façon. Lorsque le serveur ne publie pas de somme pour cette sauvegarde, ou que la liste est hors d'atteinte, la restauration se poursuit : ce contrôle protège d'un transfert abîmé, il n'est pas une autorisation.

La sortie déchiffrée est l'archive brute : un dump SQL (ou un fichier SQLite) pour les sauvegardes --database, ou une archive tar pour les sauvegardes --path. Comme la sortie par défaut est restored-<id>.out, passez --output avec une extension appropriée pour l'utiliser directement, par exemple --output=restore.sql ou --output=files.tar.

Comment le chiffrement fonctionne

Les sauvegardes utilisent un chiffrement hybride en flux, partagé avec tous les autres clients Quiet Guard via le cœur quiet-guard/monitor-php :

  1. Une clé symétrique aléatoire chiffre l'archive avec le secretstream de libsodium (AEAD XChaCha20-Poly1305 par blocs de 64 Kio), les gros dumps sont ainsi traités en flux, jamais chargés en entier en mémoire.
  2. Cette clé symétrique est scellée vers la clé publique X25519 de votre équipe (crypto_box_seal). La clé scellée est préfixée au blob.
  3. Le blob chiffré est envoyé sur /api/v1/backups. Le serveur le stocke de façon opaque, avec ses métadonnées (type, nom, taille), et applique votre quota de stockage.

Pour restaurer, le SDK récupère votre matériel de clé depuis /api/v1/encryption-key (clé publique, clé privée enveloppée par la passphrase, et sel KDF), puis déverrouille la clé privée localement à partir de votre passphrase : une dérivation Argon2id (limites MODERATE) reproduit la clé de chiffrement de clé et ouvre une secretbox libsodium, exactement comme le fait l'enveloppe côté serveur. La clé déverrouillée ouvre la clé symétrique scellée, qui déchiffre le flux.

C'est zéro-knowledge : la passphrase et la clé privée déverrouillée ne quittent jamais votre serveur, et l'opérateur ne peut jamais lire vos sauvegardes. La contrepartie : une passphrase perdue signifie une sauvegarde irrécupérable, il n'y a pas de réinitialisation.

Planification

Planifiez des sauvegardes de base de données nocturnes depuis le planificateur de votre application :

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('monitor:backup --database --name=nightly --keep=7')->dailyAt('02:00');

Passez --keep sur une sauvegarde planifiée, sauf si vous supprimez les anciennes autrement. Sans cela le coffre ne fait que croître, et la première chose que vous apprendrez de l'allocation de stockage sera la nuit où toutes les sauvegardes se mettront à échouer.

Lien avec le coffre

Le coffre, le quota en Go et l'interface du blob chiffré vivent côté serveur. Consultez la documentation du serveur pour parcourir les sauvegardes, télécharger le blob chiffré brut et gérer le stockage.

Vous lisez la documentation Laravel SDK v1.0.