Faire du Backup-as-a-Service avec Restic et Minio

Pour mon infrastructure de backups, j’utilise depuis plusieurs années BorgBackup, un excellent outil pour chiffrer et dédupliquer ses sauvegardes, puis les stocker via SSH. Pour les sauvegardes de mes serveurs, il m’offre une complète satisfaction. Mais je voulais explorer un nouvel outil dont on m’avait dit beaucoup de bien, et je voulais essayer de faire du Backup-as-a-Service.

Les besoins sont simple : les utilisateurs ayant un compte sur mon service de SSO devraient pouvoir se connecter sur un service, créer une clé dédiée à une application, puis utiliser cette clé pour backup une machine, serveur ou PC, et envoyer les données sur mon serveur central. Les backups créés devraient être chiffrés, dédupliqués, et avec une gestion des droits pour ne pas permettre à un utilisateur d’écraser ou de modifier les backups des autres (tout étant chiffré, l’accès en lecture est moins critique). Si possible, le service devrait pouvoir définir des quotas par utilisateur ; malheureusement, la solution que j’ai trouvée ne le permet pas.

Les différentes étapes de l’article sont :

Faire ses backups simplement

Restic est une excellente alternative à BorgBackup, implémentée en Go. Il permet l’utilisation de plusieurs types de backend de stockage, dont du SFTP, une API REST, ou divers protocols cloud comme AWS S3 et l’équivalent chez la concurrence, au prix d’une compression moindre et d’un plus gros usage en ressources, notamment en RAM, que BorgBackup.

Sur chaque machine, nous allons installer Restic, puis écrire un petit script backup.sh autour du binaire pour backup facilement la machine. Je suis repartie de mes scripts existants, eux-même basés sur celui proposé dans la doc de BorgBackup :

$ sudo apt install restic
#!/bin/sh

info() { printf "\n%s %s\n\n" "$( date )" "$*" >&2; }
trap 'echo $( date ) Backup interrupted >&2; exit 2' INT TERM
info "Starting backup"

source .env

# Prehook start
# You can write here some commands to run before the backup
# For example, a database dump, or a service stop
# Prehook end

restic backup /path/to/folder1 /path/to/folder2
backup_exit=$?

restic forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 12
prune_exit=$?

# Posthook start
# You can write cleanup commands here, or you can restart your services
# Posthook end

# use highest exit code as global exit code
global_exit=$(( backup_exit > prune_exit ? backup_exit : prune_exit ))
if [ ${global_exit} -eq 0 ]; then
    echo "Backup and Prune finished successfully"
    curl -m 10 --retry 5 https://health.chapoline.me/ping/<UUID>
elif [ ${global_exit} -eq 1 ]; then
    echo "Backup and/or Prune finished with warnings"
    curl -m 10 --retry 5 https://health.chapoline.me/ping/<UUID>/1
else
    echo "Backup and/or Prune finished with errors"
    curl -m 10 --retry 5 https://health.chapoline.me/ping/<UUID>/2
fi
exit ${global_exit}
$ chmod +x backup.sh

J’utilise une instance de healthchecks pour monitorer mes backups. Je vous encourage à y jeter un coup d’oeil, c’est très pratique pour monitorer les tâches ponctuelles.

Le fichier .env, qui contient les informations de connexion au backend de stockage, sera créé par la suite. Il est justement temps de choisir ce backend.

Stocker ses backups quelque part

Parmi les différents backends possible, j’ai fait le choix d’utiliser MinIO. Ce n’est pas un outil que j’aime beaucoup utiliser, mais sa compatibilité à l’API S3 le rend très pratique, et je voulais construire une solution entièrement auto-hébergée.

Son installation n’est pas très simple sous Debian, mais une installation par Docker est possible (et beaucoup plus facile). Pour ma part, je l’ai installé sur un serveur sous NixOS :

{ config, lib, pkgs, ... }:

{
  services.minio = {
    enable = true;
  };

  networking.firewall = {
    enable = true;
    allowedTCPPorts = [ 22 9000 9001 ];
  };
}

Dans un premier temps, nous allons utiliser les identifiants du compte minioadmin, pour vérifier que tout fonctionne. Attention, ne faites pas cela en production. On peut revenir sur l’autre machine, et créer le fichier .env :

export RESTIC_PASSWORD=<mot de passe de chiffrement des backups>
export RESTIC_REPOSITORY=s3:http://<url de minio>:9000/<bucket>
export AWS_ACCESS_KEY_ID=minioadmin
export AWS_SECRET_ACCESS_KEY=<Mot de passe du compte minioadmin>

Enfin, on peut tester ces informations de connexion :

$ source .env
$ restic init
created restic repository f1c6108821 at [url]
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
$ ./backup.sh

Tue Jun 11 08:55:41 UTC 2024 Starting backup

repository f1c6108821 opened successfully, password is correct

...

Tue Jun 11 08:55:45 UTC 2024 Backup and Prune finished successfully

Le source .env est nécessaire pour lancer restic init la première fois, ou pour faire des opérations manuelles sur les backups, mais pas pour lancer ./backup.sh.

Une fois que tout fonctionne, on peut créer un cron pour lancer automatiquement le backup :

0 4 * * * /path/to/backup.sh

Techniquement, un Timer Systemd est mieux et a plus de fonctionnalités, mais la force de l’habitude est forte.

Et ma SSO dans tout ça ?

Une fois un service de SSO configuré sur MinIO (laissé à titre d’exercice au lecteur),

Modification au 20/06/2024 :
La configuration du SSO sur MinIO est moins simple que je le pensais et mériterait sa propre section. Il est en effet nécessaire de rédiger une Policy, associée à la configuration OpenID, pour définir les droits d’accès des utilisateurs connectés via SSO. Par exemple, pour reprendre la méthode utilisée ici et autoriser chaque utilisateur à créer un bucket en son nom, vous pouvez donner les droits s3:* sur arn:aws:s3:::${aws:username}, et s3:List* sur arn:aws:s3:::*.
A noter cependant, que Gitlab a un bug qui l’empêche d’être utilisé pour cet usage.

la solution précédente, en plus de ne pas être sécurisée, n’est plus possible : MinIO ne manipule plus de mot de passe pour le compte utilisateur. Il faut donc utiliser une Access Key. Malheureusement, utiliser directement l’Access Key dans la configuration précédente à la place des identifiants du compte ne fonctionne pas. Il va falloir rajouter un outil : aws-cli. Rassurez vous, bien que développé par AWS pour ses API, cet outil fonctionne parfaitement avec les autres solutions compatibles S3 et n’envoie aucune information à AWS.

Commençons par nous connecter avec notre compte SSO à MinIO, puis par y générer une Access Key. Installons ensuite aws-cli sur la machine à backup, et configurons le :

Cette section est basée sur https://min.io/docs/minio/linux/integrations/aws-cli-with-minio.html.

$ sudo apt install awscli
$ aws configure
AWS Access Key ID [None]: Q3AM3UQ867SPQQA43P2F
AWS Secret Access Key [None]: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG
Default region name [None]: ENTER
Default output format [None]: ENTER
$ aws configure set default.s3.signature_version s3v4

Enfin, éditez le fichier .env, pour remplacer les informations de connexion par le profile AWS que l’on vient de créer :

export RESTIC_PASSWORD=<mot de passe de chiffrement des backups>
export RESTIC_REPOSITORY=s3:http://<url de minio>:9000/<bucket>
export AWS_PROFILE=default

Vous pouvez retester que tout fonctionne comme précédemment. Il faudra probablement utiliser un autre bucket, ou supprimer le bucket du test précédent.

Concernant les quotas, MinIO ne propose pas de quota par utilisateur, uniquement des quotas par bucket, et ne permet pas de définir des quotas par défaut pour tout nouveau bucket créé. Cette solution n’est donc possible qu’avec des utilisateurs de confiance, et nécessite un monitoring de l’espace de stockage du serveur MinIO.

Pour des backups encore plus sûrs, vous pouvez mettre en place un cluster MinIO, et/ou répliquer les buckets vers un autre serveur de stockage (auto-hébergé ou dans le Cloud) avec rclone.

Theme forked from Moonwalk, mixed with Catppuccin