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.
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:
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: