Server-Backups mit Borg: So sichere ich meine Server

“Kein Backup - kein Mitleid” - ein Motto, das auch ich mir mittlerweile zu eigen mache. Nach einigen Pannen mit fälschlicherweise unwiderruflich gelöschten Dateien musste ich vor einigen Jahren auf schmerzhafte Art und Weise lernen, dass regelmäßige Backups wichtiger Daten nicht ein nettes Feature sind, sondern die Pflicht eines verantwortungsvollen Admins. Das schließt nicht nur Desktop-Rechner ein, sondern vor allem auch Server, die zentrale Dienste bereitstellen, ohne die jeder einzelne User möglicherweise nicht arbeiten kann.

Im Laufe der Jahre habe ich verschiedenste Backup-Lösungen auf meinen Server eingesetzt - Meine aktuelle Lösung gefällt mir aber am besten, daher will ich sie euch in diesem Beitrag kurz vorstellen.

Kernkomponente meines Backup-Setups ist das Backup-Tool “Borg”, welches sich unter Linux-Admins immer größerer Beliebtheit erfreut - und das völlig zu recht. Das Tool ist einfach zu bedienen und ermöglicht im Client-Server Modell sehr effiziente Sicherungen. Sowohl beim Datentransport zum Backup-Storage als auch bezüglich des verbrauchten Speichers.

Borg Installation

Borg gibt es als fertige Pakete für Debian, Ubuntu, und Arch Linux. Neben der Installation über den Paketmanager gibt es auch noch die Installation via pip, oder manuell über ein einzelnes Binary, welches schon alle Abhängigkeiten enthält. Es ist vorteilhaft, auf Backupserver und dem zu sichernden Server (im folgenden einfach “Server” genannt) dieselbe Version von Borg zu nutzen, um Probleme durch eventuelle Inkompatibilitäten zu vermeiden. Die verschiedenen Installationsarten sind im Borg Guide ausführlich erklärt - ich werde daher nicht darauf eingehen.

Konfiguration des Backupservers

Als Backupserver verwende ich meinen NAS-Eigenbau Zuhause. Der Server läuft durchgängig, und eignet sich nicht nur hervorragend als Archiv- und Backupserver für die heimischen Geräte, sondern auch für meine Server. Dank 100 MBit/s / 40 MBit/s DSL und des intelligenten Delta-Datenabgleichs sind auch größere Datenmengen relativ zügig Zuhause gesichert - und nebenbei ist damit auch noch ein sog. “Off-Site-Backup” gemacht, also ein Backup, welches geographisch getrennt von den Produktivservern gelagert wird.

Auf dem Backupserver habe ich einen eigenen Benutzer serverbackup angelegt, dessen Home-Verzeichnis auf einer eigenen, verschlüsselten Partition liegt. So wird verhindert, dass Serverbackups (unbeabsichtigt) ausufern und große Teile des verfügbaren Speichers belegen. Meine Server melden sich jeweils als serverbackup-User an, um sich via SSH zu verbinden und die zu sichernden Daten über eine SFTP-Verbindung zu übertragen. Nach Anlegen des neuen Nutzers auf dem Backupserver wird also auf jedem der anderen Server für den jeweiligen root-Nutzer ein SSH-Key generiert, falls noch nicht vorhanden:

ssh-keygen -a 100 -t ed25519

Die Public Keys aller Server werden gesammelt und auf dem Backupserver in die ~/.ssh/authorized_keys Datei eingetragen, etwa so:

ssh-ed25519 AAAAB3NzaC1yc2EAAAADAQABAAABAQDO67aeTjQwIuuqgxEVasVH root@server1
ssh-ed25519 N3Vb3Nfdgdf1yc2gfAAJen4mJQgfhLgefgh4NhbenHGDsdf62mNe root@server2

Tipp: Am schnellsten lassen sich die Public Keys übertragen, indem man auf jedem Server den gesamten Key im Terminal ausgeben lässt:

cat ~/.ssh/id_ed25519.pub

… und den Inhalt dann auf dem Backupserver z.B. im nano Editor via Copy & Paste einfügt.

Eine Verbindung zum Borg-Backupserver wäre damit eigentlich schon möglich. Doch nehmen wir mal an, einer der Server wird kompromittiert: Ein Angreifer hätte dann ggf. Zugriff auf den Private Key des Servers und damit auch auf den serverbackup-User des Backupservers. Nichts würde ihn daran hindern, weiteren Schaden anzurichten und z.B. alle Backups auf dem Backupserver zu löschen, bevor der gekarperte Server missbraucht oder zerstört wird. Es gilt also zu verhindern, dass einer der Server über die SSH-Verbindung etwas anderes tut, als Backups zu übertragen. Daher wird die gerade bearbeitete authorized_keys Datei wie folgt erweitert:

command="borg serve --restrict-to-path /home/serverbackup/backups/server1 --append-only" ssh-ed25519 AAAAB3NzaC1yc2EAAAADAQABAAABAQDO67aeTjQwIuuqgxEVasVH root@server1
command="borg serve --restrict-to-path /home/serverbackup/backups/server2 --append-only" ssh-ed25519 N3Vb3Nfdgdf1yc2gfAAJen4mJQgfhLgefgh4NhbenHGDsdf62mNe root@server2

Das vorangestellte command=" [...] " sorgt dafür, dass via SSH nur noch die gewünschten Borg-Kommandos ausgeführt werden können - und zwar nur “hinzufügend” (--append-only). Außerdem wird das Verzeichnis festgelegt, innerhalb dessen sich der verbundene Server aufhalten darf (--restrict-to-path). Amok laufende Server stellen dann keine Gefahr mehr für die Backups dar.

Die angegebenen Verzeichnisse für die Backups der Server im serverbackup-Homeverzeichnis müssen selbstverständlich noch angelegt werden. Damit Borg die Backupverzeichnisse als solche erkennt, muss noch ein borg init ausgeführt werden:

mkdir -p /home/serverbackup/backups/server1
borg init /home/serverbackup/backups/server1

mkdir /home/serverbackup/backups/server2
borg init /home/serverbackup/backups/server2

Die Eingabe einer Passphrase wird mit 2x [ENTER] übersprungen. Damit ist der Backupserver vorbereitet.

Konfiguration der zu sichernden Server

Widmen wir uns nun den Servern, von denen regelmäßig Sicherungen angelegt werden sollen. Borg sollte hier bereits installiert sein. Die Backups werden vom root-User durchgeführt, da in den meisten Fällen nur er Zugriff auf alle wichtigen Daten hat.

Der Backup-Prozess wird durch einen sog. Cron Job ausgelöst und von einem einfachen Bash-Script ausgeführt, welches Vorbereitungen durchführt und letztendlich Borg nutzt, um das Backup auf Dateiebene zu starten. Um jede Nacht ein Backup um 00:00 Uhr zu starten, kann beispielsweise folgender Cronjob eingetragen werden (Cronjob-Editor öffnen via crontab -e als root.):

@daily /root/backup/backup.sh > /dev/null 2>&1

Mein Backup-Script in /root/backup/backup.sh sieht wie folgt aus:

#!/bin/bash

##
## Save backup to directory named after hostname
##

LOG="backup.log"
HOST=`hostname`
REPOSITORY="ssh://serverbackup@backupserver.tld:22/~/backups/"$HOST

##
## Write output to logfile
##

exec > >(tee -i ${LOG})
exec 2>&1

echo "###### Starting backup on $(date) ######"


##
## Create list of installed software
##

dpkg --get-selections > /root/backup/software.list


##
## Create database dumps
##

echo "Creating database dumps ..."
/bin/bash /root/backup/dbdump.sh


##
## Sync backup data
##

echo "Syncing backup files ..."
borg create -v --stats                                  \
    $REPOSITORY::'{now:%Y-%m-%d_%H:%M}'                 \
    /root/backup                                        \
    /etc                                                \
    /var/www                                            \
    /var/vmail                                          \
    /storage


echo "###### Finished backup on $(date) ######"


##
## Send mail to admin
##

mailx -a "From: "$HOST" Backup <"$HOST"@meinedomain.tld>" -s "Backup | "$HOST admin@meinedomain.tld < $LOG

Folgende Daten werden gesichert:

  • Liste aller installierten Softwarepakete
  • Ein Dump aller Datenbanken, die in dbdump.sh definiert sind (dazu gleich mehr)
  • Dateien aus den angegebenen Verzeichnissen (/etc, /var/www, … )

Sämtliche Ausgaben, die während des Backupprozesses entstehen, werden in eine Log-Datei geschrieben und als E-Mail an den Administrator geschickt. Das funktioniert freilich nur, wenn ein funktionierender Mailserver auf dem System installiert ist.

Datenbanken sichern

Die Sicherung der MySQL-Datenbanken übernimmt ein eigenes Script /root/backup/dbdump.sh:

#!/bin/bash

DBUSER="backup"
DBPASSWD="backupuserpassword"
DBBAKPATH="/root/backup/dbdumps/"

DBS="nextcloud vmail spamassassin"

for DBNAME in $DBS; do echo "Creating backup for database $DBNAME" && mysqldump -u $DBUSER -p$DBPASSWD $DBNAME > $DBBAKPATH"$DBNAME.sql"; done

Damit das Script funktioniert, muss im /root/backup Verzeichnis ein Unterverzeichnis “dbdumps” existieren. Nur die in “DBS” definierten Datenbanken werden vom Backup erfasst. Auf dem MySQL-Server muss ein spezieller Backup-User “backup” mit dem dazugehörigen Passwort angelegt werden. Dieser hat lesenden Zugriff auf alle Datenbanken und Tabellen:

mysql -u root -p
create user 'backup'@'localhost' identified by 'backupuserpassword';
grant SELECT, RELOAD, LOCK TABLES, REPLICATION CLIENT, SHOW VIEW, EVENT, TRIGGER on *.* to 'backup'@'localhost';
quit;

Das war’s auch schon!

Jetzt noch das Script getestet:

chmod u+x /root/backup/backup.sh
./root/backup/backup.sh

… und das Thema “Backup” ist für’s erste erledigt. Im Grunde müssen jetzt noch noch die eintreffenden E-Mails regelmäßig kontrolliert werden.

Alte Backups automatisch entfernen

Halt, da ist noch etwas! Mit der Zeit werden die Backups langsam aber sicher die zugewiesene Partition auf dem Backupserver füllen. Damit das nicht passiert, macht es Sinn, diese nicht ewig aufzuheben, sondern regelmäßig zu löschen.

Für diese Aufgabe habe ich mir auf dem Backupserver ein weiteres Script /home/serverbackup/prune-backup.sh angelegt:

#!/bin/bash

ROOTDIR="/home/serverbackup/backups/"
LOG="prune-backup.log"

# copy all output to logfile
exec > >(tee -i ${LOG})
exec 2>&1

echo "###### Pruning backup for server1 on $(date) ######"

borg prune -v ${ROOTDIR}server1 \
--keep-daily=7 \
--keep-weekly=4 \
--keep-monthly=6

echo "###### Pruning backup for server2 on $(date) ######"

borg prune -v ${ROOTDIR}server2 \
--keep-daily=7 \
--keep-weekly=4 \
--keep-monthly=6


echo "###### Pruning finished ######"

# Send logfile to serveradmin
mailx -r backup@meinserver.tld -s "Backup | Prune" admin@meinserver.tld < $LOG

Auch dieses Script wird von einem Cronjob ausgelöst - und zwar erst, wenn alle Serverbackups durchgeführt sind. Andernfalls kann es zu Konflikten kommen. Ich lasse das Script zum “Stutzen” des Backups daher erst um 2:00 Uhr täglich laufen. Auf dem Backupserver:

su - serverbackup
chmod u+x ~/prune-backup.sh
crontab -e

Neuer Cronjob:

0 2 * * *  ~/prune-backup.sh

Nach einem “prune” bleibt von den Backups nur folgendes übrig:

  • 7 tägliche Backups aus den letzten Tagen
  • 4 wöchentliche Backups aus den letzten 4 Wochen
  • 6 monatliche Backups aus den letzten 6 Monaten

Das heißt: Ich behalte nur so viele Daten, dass ich die täglichen Stände der letzten Woche wiederherstellen kann, oder die wöchentlichen Stände der letzten 4 Wochen, oder die monatlichen Stände des letzten halben Jahres. Länger als 6 Monate wird kein Backup aufbewahrt.

Backups wiederherstellen

… und das sollte geübt sein: Im Notfall soll es schließlich schnell und routiniert funktionieren. Daher hier die notwendigsten Kommandos:

Liste der vorhandenen Backups einsehen

Entweder auf dem Backupserver: (in den meisten Fällen weniger komfortabel)

cd /home/serverbackups/backups/server1
borg list

… oder direkt auf dem betroffenen Server (als root):

borg list ssh://serverbackup@backupserver.tld:22/~/backups/server1

Die Ausgabe kann z.B. so aussehen:

2017-04-07_00:04                     Fri, 2017-04-07 00:01:18
2017-04-08_00:04                     Sat, 2017-04-08 00:00:56

Ein Verzeichnis wiederherstellen

Um eine spezielle Version eines Verzeichnisses oder einer Datei aus dem Backup-Archiv zu extrahieren, wird das borg extract Kommando genutzt. Beispiel: Das Verzeichnis /var/www vom 08.04.2017 soll vom Server aus wiederhergestellt und nach /backup-extracted/www geschrieben werden:

mkdir -p /backup-extracted/www
cd /backup-extracted/www
borg extract ssh://serverbackup@backupserver.tld:22/~/backups/server1::2017-04-08_00:04 var/www
Vorsicht: Der borg extract Befehl extrahiert die Daten in das aktuelle Verzeichnis: Daher erst in das Zielverzeichnis navigieren - dann borg extract absetzen!

Der entsprechende Wiederherstellungsbefehl auf dem Backupserver könnte beispielsweise lauten:

borg extract ~/backups/server1::2017-04-08_00:04 var/www

Je nach Netzwerkverbindung und Systemleistung kann das Wiederherstellen eines Backups einige Zeit in Anspruch nehmen. In den meisten Fällen muss es nämlich zuerst aus mehreren inkrementellen Backups zusammengesetzt werden.

So. Das war’s jetzt aber wirklich.