Restic avec MinIO

Restic avec MinIO

Inutile en 2021 de rappeler l’importance de faire des sauvegardes. Dans ce billet on va parler de Restic et stockage objet avec MinIO.

Photo by Jason Dent on Unsplash

Après un premier article consacré à Borg, je vais vous parler maintenant de Restic. Un outil dont la philosophie est similaire mais qui propose deux atouts majeurs: il est compatible Windows et supporte de nombreuses solutions de stockage cloud.

Voici une rapide comparaison des deux solutions:

BorgBackup Restic
language Python Go
backend ssh ssh, Ceph, S3, MinIO…
déduplication oui oui
compression oui non
chiffrement facultatif obligatoire
support Windows non oui

Restic utilise un mécanisme de déduplication qui découpe les fichiers à sauvegarder en blobs de tailles variables ( entre 512KiB et 8MiB avec une taille moyenne à 1MiB). Entre deux sauvegardes, seuls les blobs modifiés sont envoyés sur le dépôt. Cela fonctionne même si des octets sont insérés ou supprimés à des positions arbitraires dans le fichier.

Serveur de sauvegarde

Puisque Restic supporte nativement le stockage objet, je vais en profiter pour mettre en place un serveur MinIO. Cette solution open source est simple à mettre en oeuvre et permet de déployer des architectures distribuées à hautes performances compatibles avec l’API S3 d’Amazon.

Sur ce petit projet, je vais me contenter d’utiliser MinIO sous la forme d’un simple conteneur Docker présenté par le traditionnel couple Traefik - Let’s Encrypt (voir mon précédent article pour l’installation de la plate-forme).

J’enfonce probablement des portes ouvertes mais assurez-vous que vos sauvegardes se trouvent dans un autre datacenter que vos données primaires. Si vous cherchez un VPS capacitif abordable, vous pouvez jeter un coup d’oeil à l’offre de cet hébergeur.

Docker-Compose

Comme d’habitude, l’installation va se limiter à créer un docker-compose :

1sudo mkdir /opt/minio
2sudo touch /opt/minio/docker-compose.yml

et à effectuer quelques modifications dans l’exemple fournit:

  • remplacer le mot de passe admin de MinIO
  • modifier le champ host du service ( backup.exemple.tld dans l’exemple )
 1version: '3'
 2services:
 3  minio:
 4    image: minio/minio:latest
 5    container_name: minio
 6    volumes:
 7      - data:/data
 8    expose:
 9      - "9000"
10    environment:
11      MINIO_ROOT_USER: admin
12      MINIO_ROOT_PASSWORD: V3rYD1ff1culT!
13    command: server /data
14    healthcheck:
15      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
16      interval: 30s
17      timeout: 20s
18      retries: 3
19    networks:
20      - "traefik_lan"
21    labels:
22      - "traefik.enable=true"
23      - "traefik.docker.network=traefik_lan"
24      - "traefik.http.routers.minio.rule=Host(`backup.exemple.tld`)"
25      - "traefik.http.routers.minio.entrypoints=websecure"
26      - "traefik.http.routers.minio.tls=true"
27      - "traefik.http.routers.minio.tls.certresolver=letsencrypt"
28
29networks:
30  traefik_lan:
31    external: true
32
33volumes:
34  data:

Cette configuration utilise un named volume data pour stocker les backups. Il est localisé par défaut dans /var/lib/docker/volumes. Si vous utilisez une partition /var séparée, il faudra la dimensionner en conséquence (merci LVM…) !

Après avoir lancer le service avec la commande : docker-compose -f /opt/minio/docker-compose.yml up -d, je peux accéder à l’interface web de MinIO avec les identifiants définis dans le docker-compose.

screenshot MinIO

Par défaut, MinIO ne fournit pas d’interface d’administration. Pour cela vous pouvez ajouter la brique console MinIO. Dans mon cas d’usage, je vais me contenter du client MinIO en CLI

Minio Client:

Sur mon poste, je commence par installer le client MinIO:

1$ sudo curl -L https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
2$ sudo chmod +x /usr/local/bin/mc

Je crée un alias nommé backup pour simplifier l’accès au serveur MinIO avec les identifiants définis dans le fichier docker-compose.yml

 1mc alias set <ALIAS> [SERVER_URL] [ACCESSKEY] [SECRETKEY]
 2
 3mc alias set backup https://backup.exemple.tld admin V3rYD1ff1culT!
 4
 5# pour vérifier le bon fonctionnement:
 6mc admin info <ALIAS>
 7
 8mc admin info backup
 9●  backup.exemple.tld
10  Uptime: 2 weeks
11  Version: 2021-05-18T00:53:28Z
12  Network: 1/1 OK
13
14401 KiB Used, 2 Buckets, 1 Object

Attention: la création d’un alias ajoute les identifiants en clair dans le fichier de configuration .mc/config.json.

Tout fonctionne correctement. Pour autant, il n’est pas question d’utiliser le compte admin du serveur MinIO pour sauvegarder les différents serveurs. Pour chacun d’eux, je vais créer:

  • un bucket S3 dédié: <HOSTNAME>-bucket
  • un compte d’accès: <HOSTNAME>-user
  • des droits d’accès au bucket pour ce compte: <HOSTNAME>-policy
Création du bucket:
1mc mb <ALIAS>/<BUCKET-NAME>
2
3mc mb backup/myserver-bucket
4Bucket created successfully `backup/myserver-bucket`.
5
6mc ls backup
7[2021-06-02 18:40:29 CEST]     0B myserver-bucket/
Création du compte d’accès:
1mc admin user add <ALIAS> [ACCESSKEY] [SECRETKEY]
2
3mc admin user add backup myserver-user V3rYs3cr3tkeY!
4Added user `myserver-user` successfully.
5
6# pour lister les utilisateurs
7mc admin user list <ALIAS>
8mc admin user list backup
9enabled    myserver-user
Création des droits d’accès:

Pour cela il faut créer un fichier myserver-policy.json et le charger sur le serveur MinIO. Dans cet exemple, j’autorise à lister le bucket myserver-bucket et toutes les opérations possibles sur les objets enfants myserver-bucket/*

 1{
 2    "Version": "2012-10-17",
 3    "Statement": [
 4        {
 5            "Sid": "ListObjectsInBucket",
 6            "Effect": "Allow",
 7            "Action": ["s3:ListBucket"],
 8            "Resource": ["arn:aws:s3:::myserver-bucket"]
 9        },
10        {
11            "Sid": "AllObjectActions",
12            "Effect": "Allow",
13            "Action": "s3:*Object",
14            "Resource": ["arn:aws:s3:::myserver-bucket/*"]
15        }
16    ]
17}

il faut maintenant charger cette policy sur le serveur MinIO:

 1mc admin policy add <ALIAS> <POLICY-NAME> </PATH/TO/JSON-FILE>
 2
 3mc admin policy add backup myserver-policy myserver-policy.json
 4Added policy `myserver-policy` successfully.
 5
 6# pour lister les policies disponibles
 7mc admin policy list backup
 8diagnostics
 9readonly
10readwrite
11myserver-policy
12writeonly
13
14# pour obtenir le détail d'une policy
15mc admin policy info backup myserver-policy
16{
17 "Version": "2012-10-17",
18 "Statement": [
19  {
20   "Sid": "ListObjectsInBucket",
21   "Effect": "Allow",
22   "Action": [
23    "s3:ListBucket"
24   ],
25   "Resource": [
26    "arn:aws:s3:::myserver-bucket"
27   ]
28  },
29  {
30   "Sid": "AllObjectActions",
31   "Effect": "Allow",
32   "Action": [
33    "s3:*Object"
34   ],
35   "Resource": [
36    "arn:aws:s3:::myserver-bucket/*"
37   ]
38  }
39 ]
40}

et l’associer à l’utilisateur myserver-user

1mc admin policy set <ALIAS> <POLICY-NAME> user=<USER-NAME>
2
3mc admin policy set backup myserver-policy user=myserver-user
4Policy `myserver-policy` is set on user `myserver-user`
5
6# pour vérifier l'association
7mc admin user list backup
8enabled    myserver-user        myserver-policy

La configuration est terminée. Je peux créer un nouvel alias (myserver) pour tester l’accès au serveur MinIO avec ce nouveau compte

 1mc alias set myserver https://backup.exemple.tld myserver-user V3rYs3cr3tkeY!
 2
 3# pour lister les accès:
 4mc ls myserver
 5[2021-06-02 11:04:46 CEST]     0B myserver-bucket/
 6
 7# pour copier un fichier dans le bucket:
 8mc cp  <FILE> <ALIAS>/<BUCKET-NAME>
 9mc cp demo.pdf myserver/myserver-bucket
10demo.pdf:          24.64 KiB / 24.64 KiB  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  257.50 KiB/s 0s

et en version web: screenshot MinIO

Le serveur MinIO est opérationnel et prêt à recevoir les sauvegardes Restic…

Serveur à sauvegarder

Installation de Restic

Comme je le disais en intro, Restic est écrit en Go. On peut directement télécharger la dernier version disponible sur le dépôt Github ou faire confiance à son gestionnaire de paquets habituel (souvent avec une version plus ancienne)

1wget https://github.com/restic/restic/releases/download/v0.12.0/restic_0.12.0_linux_amd64.bz2
2bunzip2 restic_0.12.0_linux_amd64.bz2
3mv restic_0.12.0_linux_amd64 /usr/local/sbin/restic
4chmod +x /usr/local/sbin/restic
5restic version
6restic 0.12.0 compiled with go1.15.8 on linux/amd64

Automatisation

Restic ne fournit pas d’outil pour planifier les sauvegardes. On peut utiliser un simple script shell dans un cron mais j’ai préféré opter pour runrestic qui propose un wrapper en python.

1sudo pip3 install --upgrade runrestic
2runrestic -v
3runrestic 0.5.23

Auquel il faut ajouter un fichier de configuration qui va contenir:

  • l’adresse du dépôt S3 ( https://URL/bucket_name )
  • les identifiants d’accès S3 (myserver-user)
  • la clé de chiffrement des données (RESTIC_PASSWORD)
  • la liste des arborescences à sauvegarder (/opt)
  • les périodes de rétentions (3 mois)
 1# /etc/runrestic.toml
 2repositories = [
 3    "s3:https://backup.exemple.tld/myserver-bucket"
 4    ]
 5
 6[environment]
 7RESTIC_PASSWORD = "VeRySecuredPassPhrase"
 8AWS_ACCESS_KEY_ID= "myserver-user"
 9AWS_SECRET_ACCESS_KEY= "V3rYs3cr3tkeY!"
10
11[backup]
12sources = [
13    "/opt"
14    ]
15
16[prune]
17keep-daily = 7
18keep-weekly = 4
19keep-monthly = 3

Je peux maintenant initialiser le dépôt

1runrestic init
2[(0, 'created restic repository a9d0eea4dc at s3:https://backup.exemple.tld/myserver-bucket\n\nPlease note that knowledge of your password is required to access\nthe repository. Losing your password means that your data is\nirrecoverably lost.\n')]

ajouter un service dans systemd

1# /etc/systemd/system/runrestic.service
2[Unit]
3Description=runrestic backup
4
5[Service]
6Type=oneshot
7ExecStart=/usr/local/bin/runrestic

avec un timer unit pour une exécution quotidienne

 1# /etc/systemd/system/runrestic.timer
 2[Unit]
 3Description=Run runrestic backup
 4
 5[Timer]
 6OnCalendar=daily
 7Persistent=true
 8
 9[Install]
10WantedBy=timers.target

et enfin activer le service

1sudo systemctl enable runrestic.timer
2sudo systemctl start runrestic.timer

L’option shell de runresctic permet de sourcer les variables d’environnements nécessaires pour utiliser directement les commandes de Restic.

 1runrestic shell
 2# obtenir la liste des snapshots
 3restic snapshots
 4repository a9d0eea4dc opened successfully, password is correct
 5ID        Time                 Host                   Tags        Paths
 6-----------------------------------------------------------------------
 74603f061  2021-06-20 00:00:02  myserver.exemple.tld               /opt
 8bd220334  2021-06-27 00:00:02  myserver.exemple.tld               /opt
 9ff32d4a9  2021-06-29 00:00:05  myserver.exemple.tld               /opt
1009d57259  2021-06-30 00:00:06  myserver.exemple.tld               /opt
11bb136478  2021-07-01 00:00:07  myserver.exemple.tld               /opt
12e568c376  2021-07-02 00:00:07  myserver.exemple.tld               /opt
1349d56a35  2021-07-03 00:00:08  myserver.exemple.tld               /opt
14e4812c15  2021-07-04 00:00:05  myserver.exemple.tld               /opt
158064119a  2021-07-05 00:00:06  myserver.exemple.tld               /opt
16-----------------------------------------------------------------------
17
18# obtenir les statistiques globales
19restic stats
20repository a9d0eea4dc opened successfully, password is correct
21scanning...
22Stats in restore-size mode:
23Snapshots processed:   14
24   Total File Count:   3421029
25         Total Size:   24.518 GiB
26
27# obtenir les statistiques pour un snapshot (snapshot_id)
28restic stats 09d57259
29repository a9d0eea4dc opened successfully, password is correct
30scanning...
31Stats in restore-size mode:
32Snapshots processed:   1
33   Total File Count:   244767
34         Total Size:   24.511 GiB

Restauration

Avant de procéder à une restauration, il faudra identifier le snapshot qui contient les données à restaurer en fonction d’une date ou d’une recherche plus évoluée:

 1# sourcer les identifiants et le dépôt dans la configuration de runrestic
 2runrestic shell
 3# afficher la liste des snapshots disponible
 4restic snapshots
 5# afficher le contenu d'un snapshot
 6restic ls <snapshot_id>
 7# rechercher des fichiers en particulier (nom, extension...)
 8restic ls <snapshot_id> | grep <modif>
 9# afficher les fichiers ajoutés (+), supprimés (-) ou modifiés (M) entre deux snapshots
10restic diff <snapshot_id_1> <snapshot_id_2>

Une fois le snapshot identifié, on peut choisir entre une restauration compète ou au niveau fichier:

1# sourcer les identifiants et le dépôt dans la configuration de runrestic
2runrestic shell
3# restaurer l'intégralité du snapshot
4restic restore <snapshot_id> --target /path/to/restore
5# restaurer la dernère sauvegarde en date (latest)
6restic restore latest --target /path/to/restore
7# restaurer un fichier en particulier
8restic restore <snapshot_id> --include=/path/file/you/want/to/restore --target=/path/to/restore

l’autre alternative consiste à monter directement le dépôt localement pour en explorer le contenu à grand coup de find et de grep. Libre à vous ensuite de copier les fichiers qui vous intéressent.

 1# sourcer les identifiants et le dépôt dans la configuration de runrestic
 2runrestic shell
 3# monter le dépôt
 4mkdir /mnt/restic_tmp
 5restic mount /mnt/restic_temp
 6repository a9d0eea4dc opened successfully, password is correct
 7Now serving the repository at /mnt/restic_temp
 8When finished, quit with Ctrl-c or umount the mountpoint.
 9# parcourir le dépôt (dans un autre terminal)
10ls -l /mnt/restic_temp
11total 0
12dr-xr-xr-x 1 root root 0  5 juil. 18:12 hosts
13dr-xr-xr-x 1 root root 0  5 juil. 18:12 ids
14dr-xr-xr-x 1 root root 0  5 juil. 18:12 snapshots
15dr-xr-xr-x 1 root root 0  5 juil. 18:12 tags
16ls -l snapshots
17ls -l snapshots/
18total 0
19dr-xr-xr-x 2 root root 0 20 juin  03:30 2021-06-20T03:30:02+02:00
20dr-xr-xr-x 2 root root 0 27 juin  00:00 2021-06-27T00:00:02+02:00
21dr-xr-xr-x 2 root root 0 30 juin  00:00 2021-06-30T00:00:06+02:00
22dr-xr-xr-x 2 root root 0  1 juil. 00:00 2021-07-01T00:00:07+02:00
23dr-xr-xr-x 2 root root 0  2 juil. 00:00 2021-07-02T00:00:07+02:00
24dr-xr-xr-x 2 root root 0  3 juil. 00:00 2021-07-03T00:00:08+02:00
25dr-xr-xr-x 2 root root 0  4 juil. 00:00 2021-07-04T00:00:05+02:00
26lrwxrwxrwx 1 root root 0  4 juil. 10:09 latest -> 2021-07-04T00:00:05+02:00

Conclusion:

Restic est une solution de sauvegarde légère et facile à mettre oeuvre. Elle souffre toutefois du même défaut que Borg avec des sauvegardes qui ne sont pas vraiment hors ligne. En effet si un attaquant obtient une élévation de privilège sur le serveur, il peut à loisir détruire ou altérer les données primaires et celles de sauvegardes (accès aux identifiants du dépôt). Pour palier à cela, on pourrait imaginer lancer le script de sauvegarde depuis le serveur de backup à travers un tunnel SSH ou un dépôt avec un système de fichiers supportant les snapshots comme BTRFS ou ZFS. Dans le même logique qui limite un peu Restic dans les environnements critiques, il n’existe pas de solution native pour lever des alertes en cas d’erreur de sauvegarde. Là encore, il faut passer par du scripting ou exploiter les options Prometheus de Runrestic.

Pour aller plus loin: