Sauvegardes via Internet, automatisées et auto-hébergées avec Backintime et Docker

Faire des sauvegardes régulières est crucial. Mais rares sont les personnes qui le font sérieusement. Il faut donc les automatiser et les rendre faciles d’utilisation.

Pour résister à un cambriolage, incendie ou autre gros pépin, le mieux est que cette sauvegarde soit externalisée. Dans ce cas, il est important que les données soient chiffrées, et que le flux réseau ne soit pas excessif.

Ca faisait longtemps que j’avais l’idée de faire des sauvegardes croisées et auto-hébergées : je stocke les sauvegardes d’un copain, et il stocke les miennes. Et je pourrais ainsi fournir une solution de sauvegarde simple et automatique à toutes les personnes pour qui j’ai installé Linux.

C’est BackInTime que j’ai choisi pour faire ça côté client, et Docker m’a permis de mieux sécuriser les choses côté serveur.

Sommaire

Pourquoi Backintime?

En termes de fonctionnalité, c’est l’outil qui m’a paru le plus complet et le plus ergonomique. Il garde plusieurs versions (« snapshots »), que l’on peut facilement parcourir. On peut restaurer un fichier individuel, comparer plusieurs versions etc.

Les sauvegardes sont incrémentales, chiffrées, lançables automatiquement à intervalle régulier etc.

J’avais testé un peu DejaDup mais il avait moins de fonctionnalités (notamment la possibilité de parcourir les éléments d’une sauvegarde donnée), et surtout je suis tombé sur beaucoup de bugs qui m’ont paru bloquants quand je l’avais testé.

D’un autre côté, il n’y a pas beaucoup d’activité sur Backintime depuis quelques mois, j’espère que le projet va repartir.

Pré-requis

Il faut un serveur avec Docker, hébergé si possible chez un copain, et avec de l’espace disque pour stocker tout ce qu’il y aura à sauvegarder.

Il faut installer Backintime sur chacune des machines à sauvegarder. Pour Ubuntu, je conseille d’utiliser le PPA (voir https://github.com/bit-team/backintime/blob/master/README.md). L’objectif est d’utiliser le mode « SSH encrypted » : https://backintime.readthedocs.io/en/latest/settings.html#ssh-encrypted. Pour ce mode, il est également nécessaire d’installer les packages sshfs et encfs sur chaque machine à sauvegarder. Et il faut y générer une clé SSH (via ssh-keygen).

Il est conseillé d’avoir une bonne bande passante en upload (côté machines à sauvegarder) pour que les sauvegardes ne prennent pas des heures. J’ai 5 Mbps et ça me parait acceptable : avec le 1 Mbps d’une connexion ADSL, il faudra probablement se limiter sur les données à sauvegarder.

Mettre le serveur SSH dans un conteneur Docker

Pour faire une sauvegarde à distance, Backintime nécessite un accès SSH (et pas uniquement du SFTP). Or un accès SSH sur une machine ouvre beaucoup de possibilités, même si on n’est pas root.

Pour limiter cette problématique, j’ai pensé à mettre le serveur SSH dans un conteneur Docker. Ca met une couche d’isolation par rapport à la machine hôte, et limite fortement la surface d’attaque (il n’y a dans le conteneur que le strict minimum).

Ca permet en outre de lancer une instance par utilisateur, chacun n’ayant accès qu’à ses propres données.

J’en ai parlé sur le github de backintime, mais ça n’a pas soulevé les foules : https://github.com/bit-team/backintime/issues/879. Donc je me suis lancé pour le faire moi-même :

Création de l’image

Première version du fichier Dockerfile :

FROM arm32v7/debian:stretch

RUN apt-get update && apt-get install -y openssh-server rsync
RUN mkdir /var/run/sshd
RUN useradd --create-home backintime

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

(inspiré de https://docs.docker.com/engine/examples/running_ssh_service/, il faut adapter l’architecture arm32v7 à la machine sur laquelle vous hébergez. Dans mon cas, c’est adapté à un Olinuxino A20. Sur un A64, on peut utiliser arm64v8)

Créer l’image via :

sudo docker build -t "docker-openssh:v1" .

Et si besoin la supprimer via :

sudo docker image rm docker-openssh:v1

Externalisation de la configuration et des données

Il faut externaliser du conteneur :

  • l’emplacement des données bien sûr
  • la configuration du serveur OpenSSH (/etc/ssh, pour qu’elle ne change pas à chaque fois qu’on reconstruira le conteneur). J’y ai copié le /etc/ssh d’une machine debian stretch temporaire
  • la configuration ssh du compte avec lequel on se connecte (~/.ssh), notamment pour pouvoir alimenter le authorized_keys avec la clé publique de la machine à sauvegarder (par défaut .ssh/id_rsa.pub)

Concernant les données, plutôt que de les mettre sur la machine hôte, je compte à terme les mettre sur un partage réseau distant. Via quel protocole? SMB? SSHFS? NFS? iSCSI? D’après https://backintime.readthedocs.io/en/latest/settings.html , SMB est supporté (et mes tests semblent le confirmer), même s’il nécessite de la conf côté serveur pour supporter les symlinks, et SSHFS ne serait pas supporté.

Démarrage du conteneur

sudo docker run -p 12345:22 --mount type=bind,source=/mnt/hdd/backintime/donnees-userxxx/,target=/donnees-backintime --mount type=bind,source=/mnt/hdd/backintime/home-ssh-userxxx/,target=/home/backintime/.ssh --mount type=bind,source=/mnt/hdd/backintime/etc-ssh/,target=/etc/ssh --restart unless-stopped --cap-add=NET_ADMIN -d -P --name docker-openssh-userxxx docker-openssh:v1

(le –cap-add=NET_ADMIN est nécessaire pour faire fonctionner iptables : voir plus bas. Adapter le port 12345 et les emplacements sur la machine hôte en fonction de vos préférences)

Arrêter via :

sudo docker container stop docker-openssh-userxxx
sudo docker container rm docker-openssh-userxxx

Un peu de sécurisation des accès réseau

Dans un conteneur même minimaliste, on a accès au réseau, ce qui peut permettre des attaques sur le réseau local.

Pour restreindre cela, l’idéal serait de mettre en place des règles iptables sur l’hôte (voir par exemple https://serverfault.com/questions/615372/restricting-the-network-access-of-docker-container). Le problème, c’est que docker modifie lui-même les règles iptables, donc il faudrait lui demander de ne plus le faire, et les gérer intégralement nous-mêmes… Ca m’a paru un peu compliqué, d’autant que la gestion de Docker peut évoluer avec le temps.

J’ai donc préféré mettre des règles iptables dans le conteneur lui-même. C’est plus facile, mais un peu moins sécurisé (si le user arrivait à devenir root dans le conteneur, il pourrait les désactiver). J’ai donc fait une deuxième version du Dockerfile :

FROM arm32v7/debian:stretch

RUN apt-get update && apt-get install -y openssh-server rsync iptables
RUN mkdir /var/run/sshd
RUN useradd --create-home backintime

# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd

ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile

# Entrypoint to restrict network capabilities and start openssh
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

EXPOSE 22

Avec un fichier entrypoint.sh avec ce contenu :

#!/bin/sh

# Restrict network capabilities
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT

# Start openssh
nice -n 5 /usr/sbin/sshd -D

J’ai mis un nice au lancement d’openssh, pour que les processus de l’hôte soient considérés comme plus prioritaires que ceux lancés via SSH dans les conteneurs (il ne s’agit que de sauvegardes…)

Mise à jour le 11/03/2021 : j’ai publié cette image Docker sur le Docker Hub : https://hub.docker.com/r/mossroy/openssh-backintime.

Quotas d’espace disque

Au départ, j’avais pensé à acheter un gros disque dur, et mettre des quotas dessus pour chaque utilisateur. Malheureusement, BackInTime ne sait pas prendre en compte ces quotas : https://github.com/bit-team/backintime/issues/555. D’autre part, ça me posait des problématiques d’organisation : un gros disque dur coûte cher, il aurait fallu répartir les coûts sur les utilisateurs.

Au final, un ami m’a suggéré une meilleure idée : utiliser un disque physique par utilisateur (ou groupe d’utilisateurs) : plus besoin de quotas, chacun achète (ou réutilise) un disque externe de la taille de son choix.

Évidemment, il faut les configurer (avec hdparm) pour qu’ils s’arrêtent de tourner quand ils ne sont pas utilisés. Et il faut prévoir un peu plus de place physique (pour mettre les boîtiers), et si besoin un switch USB (alimenté).

Configuration d’un nouvel utilisateur

L’idée est d’avoir un conteneur par utilisateur. Il faut donc lancer une nouvelle instance Docker pour chaque utilisateur : sur un port dédié, et avec des points de montage dédiés (au moins pour les données et le ~/.ssh).

Sur la machine cliente, on génère une clé SSH (ssh-keygen), et on copie le contenu de ~/.ssh/id_rsa.pub dans le fichier authorized_keys correspondant côté serveur.

Puis on configure backintime sur la (ou les) machine(s) à sauvegarder, en utilisant le user backintime, le chemin /donnees-backintime, la clé privée correspondant à la clé publique qu’on vient de configurer côté serveur, et mettre un mot de passe de chiffrement.

Pour la planification, je conseille d’utiliser anacron, à la fréquence qui vous convient.

Il faut ensuite configurer les dossiers/fichiers à inclure ou exclure (attention à avoir un volume raisonnable par rapport à votre bande passante : commencez petit, pour en rajouter ensuite), et les options de purge (je conseille d’activer la « suppression intelligente » pour définir combien de versions conserver). Pour le reste, se référer à la doc de Backintime.

Performances

Sur mes petits serveurs Olinuxino A20, la vitesse de transfert plafonne entre 5 et 9 Mo/s (en montant comme en descendant), et occupe presque complètement les 2 CPUs du serveur.

Étant donné que l’Ethernet du Olinuxino A20 n’est qu’à 100Mbps, on ne pourra pas aller bien au-delà de toutes façons. Le Olinuxino A64 (que je viens de recevoir) est en Gigabit et a un CPU quadri-core plus véloce : il tourne autour de 20 Mo/s.

Optimisations si les temps de transfert sont trop longs

Si votre bande passante est faible et/ou si vous avez beaucoup de données à sauvegarder/restaurer, passer par le réseau peut parfois être très long.

Pour gagner un peu de temps, il peut y avoir quelques astuces :

Initialisation des données hors ligne

La première fois qu’on lance la sauvegarde, ça peut être très long.

Si le volume à sauvegarder est important mais bouge peu, et qu’on a une bande passante limitée, il est probablement possible d’initialiser la sauvegarde côté serveur sans passer par Internet.

Je n’ai pas encore eu l’occasion de le tester, mais sur le principe, il devrait suffire de faire une sauvegarde sur un disque local (en mode « Local encrypted » : https://backintime.readthedocs.io/en/latest/settings.html#local-encrypted), puis de copier ces données sur le serveur.

Les sauvegardes suivantes étant incrémentales, seules les fichiers ajoutés ou modifiés seront transférés.

Restauration hors ligne

Là aussi, on peut a priori copier le répertoire de sauvegarde du serveur sur un disque externe, et le lire en mode « Local encrypted » pour que la restauration aille plus vite.

Pistes d’amélioration

C’est une première version fonctionnelle, mais qui mériterait d’être améliorée.

J’aimerais que le chiffrement soit mieux blindé. Backintime utilise actuellement encfs, qui est probablement faillible. Il semblerait qu’une implémentation avec gocryptfs soit en cours de test : https://github.com/bit-team/backintime/issues/644 mais ça n’avance pas beaucoup.

Quand Backintime est configuré avec anacron, il ne donne pas beaucoup d’informations à l’utilisateur : il n’affiche pas d’icône dans Ubuntu 18.04 (cf https://github.com/bit-team/backintime/issues/916), et ne donne pas de moyen simple d’interrompre une sauvegarde en cours.

La sécurisation du conteneur est probablement encore améliorable. Quelques pistes à creuser :

  • mettre fail2ban pour détecter certaines tentatives d’attaque
  • mettre des systèmes d’alerte si l’utilisateur essaie de devenir root, ou s’il fait d’autres choses louches
  • limiter la consommation de mémoire, d’I/O disque, d’espace disque dans le /home
  • enlever des « capabilities » Docker?

2 réflexions sur « Sauvegardes via Internet, automatisées et auto-hébergées avec Backintime et Docker »

Répondre à olivier Annuler la réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *