Backups mit Restic und Raspberry Pi

Backups mit Restic und Raspberry Pi

Leserkommentare zum letzten Artikel (Backups mit Borg und Raspberry Pi) haben mich dazu gebracht, das Setup noch etwas zu verfeinern. Dazu habe ich aus Geschwindigkeitsgründen Borg über Bord geworfen und durch Restic sowie den dazugehörigen REST-Server ersetzt. Außerdem kommt zum automatischen Einhängen der Backup-Festplatte nun systemd anstelle von autofs zum Einsatz.

REST-Server (Raspberry Pi)

Der REST-Server ist eine schnelle Alternative zu SSH als Übertragungsprotokoll und muss auf dem Raspberry Pi als Serverkomponente von Restic installiert werden. Im Heimnetz können wir auch erst mal auf eine TLS-Verschlüsselung verzichten – das Backup selbst ist ja immerhin schon verschlüsselt.

Damit der Dienst nicht mit root-Rechten laufen muss, erstellen wir einen User restic, der sich nicht anmelden können muss:

# useradd -s /usr/sbin/nologin restic

Glücklicherweise gibt es vom REST-Server auch Binaries für die ARM Architektur des Pi. Wir laden also das Binary herunter, entpacken es und kopieren es nach /usr/local/bin/:

# wget https://github.com/restic/rest-server/releases/download/v0.12.0/rest-server_0.12.0_linux_arm64.tar.gz
# tar xzf rest-server_0.12.0_linux_arm64.tar.gz
# mv rest-server_0.12.0_linux_arm64/rest-server /usr/local/bin/

Damit der REST-Server automatisch gestartet wird und mit den Berechtigungen des erstellten restic-Users läuft, erstellen wir eine systemd-Datei und plazieren sie unter /etc/systemd/system/rest-server.service. Die Datei hat folgenden Inhalt:

[Unit]
Description=Rest Server
After=syslog.target
After=network.target

# if you want to use socket activation, make sure to require the socket here
#Requires=rest-server.socket

[Service]
Type=simple
# You may prefer to use a different user or group on your system.
User=restic
Group=restic
ExecStart=/usr/local/bin/rest-server --path /srv/restic --no-auth
Restart=always
RestartSec=5

# The following options are available (in systemd v247) to restrict the
# actions of the rest-server.

# As a whole, the purpose of these are to provide an additional layer of
# security by mitigating any unknown security vulnerabilities which may exist
# in rest-server or in the libraries, tools and operating system components
# which it relies upon.

# IMPORTANT!
# The following line must be customised to your individual requirements.
ReadWritePaths=/srv/restic

# Makes created files group-readable, but inaccessible by others
UMask=027

# If your system doesn't support all of the features below (e.g. because of
# the use of an older version of systemd), you may wish to comment-out
# some of the lines below as appropriate.
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=true
PrivateUsers=true
ProtectSystem=strict
ProtectHome=yes
ProtectClock=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectHostname=true
RemoveIPC=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictSUIDSGID=true
RestrictRealtime=true
# if your service crashes with "code=killed, status=31/SYS", you probably tried to run linux_i386 (32bit) binary on a amd64 host
SystemCallArchitectures=native
SystemCallFilter=@system-service

# Additionally, you may wish to use some of the systemd options documented in
# systemd.resource-control(5) to limit the CPU, memory, file-system I/O and
# network I/O that the rest-server is permitted to consume according to the
# individual requirements of your installation.
#CPUQuota=25%
#MemoryHigh=bytes
#MemoryMax=bytes
#MemorySwapMax=bytes
#TasksMax=N
#IOReadBandwidthMax=device bytes
#IOWriteBandwidthMax=device bytes
#IOReadIOPSMax=device IOPS, IOWriteIOPSMax=device IOPS
#IPAccounting=true
#IPAddressAllow=

[Install]
WantedBy=multi-user.target

Wie man sieht, habe ich hier --path /srv/restic und ReadWritePaths=/srv/restic angegeben. Das ist der Pfad zum Restic-Repository bzw. zu der Festplatte, die wir – wie im letzten Artikel – automatisch dann mounten, wenn darauf zugegriffen wird. Anstelle von autofs machen wir das dieses Mal aber mit systemd-Bordmitteln. Dazu Bedarf es nur eines Eintrages in /etc/fstab:

LABEL="Restic" /srv/restic  ext4  defaults,noatime,x-systemd.automount,x-systemd.idle-timeout=10 0 0

Das implizert natürlich, dass die Festplatte eine ext4-Partition mit dem Label „Restic“ hat (etwa: mkfs.ext4 -L Restic -m 2 /dev/sda1 – Vorsicht, Daten gehen verloren!). Außerdem muss der restic-User darauf Lese-/Schreibrechte haben.

Jetzt müssen systemd noch die Änderungen mitgeteilt und die Automountfunktion gestartet werden. Außerdem wird der REST-Server aktiviert und gestartet:

# systemctl daemon-reload
# systemctl start srv-restic.mount
# systemctl enable --now rest-server

Der REST-Server ist nun aktiv lauscht an Port 8000 auf Restic-Kommandos.

Restic (Client)

Um immer die aktuellste Resic-Version zu haben, bietet es sich an, diese ebenfalls als Binary von der Github-Seite herunterzuladen. Ausführbar gemacht wird die Datei dann nach /usr/local/bin/ verschoben:

# wget https://github.com/restic/restic/releases/download/v0.15.2/restic_0.15.2_linux_amd64.bz2
# bunzip2 restic_0.15.2_linux_amd64.bz2
# chmod +x restic_0.15.2_linux_amd64
# mv restic_0.15.2_linux_amd64 /usr/local/bin/restic

Das Backup-Skript aus dem Borg-Artikel muss natürlich etwas angepasst werden. Bei der Gelegenheit wurde es etwas verschlankt und das Passwort ist jetzt nicht mehr direkt enthalten. Vielmehr wurde es in die Datei /root/.restic.pw ausgelagert. Diese gehört root:root und ist mittels chmod 600 /root/.restic.pw auch nur für diesen lesbar. Auf einem Notebook/PC mit verschlüsselter Platte ist das für mich akzeptabel. Das Backup-Skript selbst kann dann beispielsweise so aussehen:

#!/usr/bin/env bash

export RESTIC_REPOSITORY=rest:http://192.168.178.52:8000

# some helpers and error handling:
info() { printf "\n%s %s\n\n" "$( date )" "$*" >&2; }
usage() {
    echo "usage: backup_restic.sh [-t [home | system]] | [-h]"
}

backup_home() {
    export SNAPSHOT_PATH=/.snapshots/restic-home

    btrfs subvolume snapshot -r /home $SNAPSHOT_PATH
    sync

    cd $SNAPSHOT_PATH

    restic backup                \
        --host `hostname`        \
        --exclude-caches         \
        --exclude '*/.cache/*'   \
        --tag home               \
        --one-file-system        \
        --password-file $HOME/.restic.pw \
        --cleanup-cache          \
        .

    cd /tmp

    btrfs subvolume delete $SNAPSHOT_PATH

    restic forget             \
        --prune               \
        --host `hostname`     \
        --tag home            \
        --password-file $HOME/.restic.pw \
        --keep-daily    7     \
        --keep-weekly   2     \
        --keep-monthly  3 

}

backup_system() {
    export SNAPSHOT_PATH=/.snapshots/restic-system

    btrfs subvolume snapshot -r / $SNAPSHOT_PATH
    sync

    cd $SNAPSHOT_PATH

    # Backup the home directories into an archive named after
    # the machine this script is currently running on:
    restic backup                \
        --exclude-caches         \
        --exclude 'root/.cache'  \
        --exclude 'lost+found'   \
        --tag system             \
        --one-file-system        \
        --password-file $HOME/.restic.pw \
        --cleanup-cache          \
        . /boot /boot/efi

    cd /tmp

    btrfs subvolume delete /.snapshots/restic-system

    restic forget             \
        --prune               \
        --host `hostname`     \
        --tag system          \
        --password-file $HOME/.restic.pw \
        --keep-daily    7     \
        --keep-weekly   2     \
        --keep-monthly  3 
}

## MAIN
bkuptype=""
while [ "$1" != "" ]; do
    case $1 in
        -t | --type )    shift
		         bkuptype=$1
                         ;;
        -h | --help )    usage
                         exit
                         ;;
        * )              usage
                         exit 1
    esac
    shift
done

if [ "$bkuptype" == "home" ]; then
    backup_home
elif [ "$bkuptype" == "system" ]; then
    backup_system
else
    usage
fi

Das Repository muss vorher natürlich einmalig initialisiert werden. Das können wir gut händisch auf dem Client-Rechner erledigen:

# restic -r rest:http://mypiserver:8000 -p /root/.restic.pw init

Eine Bemerkung noch zu RESTIC_REPOSITORY. Hier habe ich bewusst die IPv4-Adresse des Raspberry Pi angegeben. Zunächst hatte ich hier den Hostnamen angegeben. Die Verbindung für ein (umfangreiches) Backup wurde dann über die IPv6-Adresse aufgebaut. Über Nacht scheint sich diese jedoch geändert zu haben1 und der Backupvorgang konnte nicht abgeschlossen werden. Das war zwar kein großes Problem, weil eine erneute Ausführung an der unterbrochenen Stelle einfach weiter gemacht hat, aber unschön war das auf jeden Fall. Wenn man hier einfach die statische IPv4-Adresse angibt, umgeht man dieses Problem.

Übrigens kann man bei Restic problemlos ein und dasselbe Repository für mehrere Rechner verwenden. Restics Deduplizierung funktioniert dann auch über Rechnergrenzen hinweg. Ein Feature, das Borg meines Wissens nach so nicht bietet.

Für weitere Restic-Befehle sei auf die umfangreiche Dokumentation des Projekts verwiesen.


  1. So jedenfalls meine Vermutung. Das Borg-Backup hatte übrigens das gleiche Problem. ↩︎