Ich habe einige Tage damit verbracht, ein Offsite-Backup von Webhosts bei Uberspace auf ein Synology-NAS zu implementieren. Dieser Text liefert das Ergebnis inklusive einer detaillierten Anleitung, die sich im Kern auch auf andere Quell- und Zielsysteme anwenden lässt.

Meine Ziele waren dabei folgende:

  1. Das Backup soll täglich laufen und jeweils vorgehalten werden, bis ich selber aufräume.
  2. Ich will nicht jeden Tag alles neu ziehen.
  3. Die täglichen Backups sollen nicht immer wieder den vollen Platz belegen, also bedarf es einer Deduplizierung in irgendeiner Form.
  4. Es soll zuverlässig funktionieren und nicht ständig meine Aufmerksamkeit einfordern, vor allem aber will ich selber in frühestens 2 Jahren wieder tieferen Kontakt dazu haben müssen und dann mit Linux-Standardskills wieder flott gedanklich reinkommen können. Weniger Komplexität ist also Trumpf.
  5. Der Backup-User soll auf dem Livesystem nichts kompromittieren können, also nur lesen und zwar in einem festgelegten Verzeichnis, sonst nichts.
  6. Varianten davon mit einem kleineren Satz an verfügbaren Daten sollen auch Dritten zur Verfügung gestellt werden können. Etwa damit Kunden ihre Daten ziehen können, aber nicht die Sourcen der Applikation und die Konfiguration.
  7. Bestimmte Verzeichnisse (vor allem Caches) sollen ausgenommen werden können. Das ist eine Funktion, die zum Beispiel beim Client-Backup vom Synology Active Backup for Business fehlt, für mich völlig unverständlich.
  8. Die Lösung soll möglichst portabel sein, also vorzugsweise auf jeder Kiste laufen, die SSH und rsync kombiniert bekommt. Oder was auch immer, jedenfalls etablierte und breit verfügbare Standards. Vor allem muss sie aber ohne Softwareinstallation auskommen, die man auf Shared-Hostingsystemen eben nicht (mal eben so) machen kann. Mindestens soll sie auf einem Synology-NAS als geplante Aufgabe laufen und Uberspace und einen Rootserver sichern.

Das klingt doch spontan nach rsync als Basislösung und in der Tat kommt man damit schnell zum Ziel, wenn man die Ziele 5 und 6 ignoriert. Denn alles andere bringt rsync von Haus aus mit. Wollen wir alle Ziele erreichen, ist das gar nicht so viel komplizierter (gut für Ziel 4), wenn man weiß wie!

Vorüberlegungen

Eilige dürfen gerne direkt zur Anleitung weiterscrollen. Für die anderen hole ich hier mal etwas aus.

rsync kann mit Hardlinks arbeiten, also ein vorheriges Backup als Basis nehmen und alle unveränderten Dateien einfach als Hardlinks darauf anlegen, die praktisch keinen Platz benötigen. Das ist je nach Ausgestaltung eine Form von differenziellem oder inkrementellem Backup und funktioniert auf allen Dateisystemen, die Hardlinks unterstützen (zum Beispiel auch NTFS). Das Synology-NAS kann auf Dateisystemebene zwar auf Basis von Btrfs ebenfalls eine Deduplizierung durchführen oder auch Snapshots als schönen Partytrick, was dem Ziel ebenfalls gerecht würde, aber der Portabilität wegen möchte ich bei den Hardlinks bleiben. Zudem ist das für mich übersichtlicher, weil es einfach so aussieht, als gäbe es täglich ein Vollbackup.

Bei allen Varianten der Deduplizierung muss man sich vergegenwärtigen, dass die Daten tatsächlich nur einmal im Dateisystem vorhanden sind. Ist eine Datei also einmal irgendwie kaputt (Bit Rot aka Datenverschlechterung oder anfangs falsch gezogen), betrifft das alle Backups. Das ist sehr schlecht! Um das einigermaßen in den Griff zu bekommen wird allenthalben dringendst dazu geraten, ein Dateisystem einzusetzen, das Bit Rot mit Prüfsummen erkennen und (bei Redundanz im Speicherpool) auch selbstständig beheben kann. Btrfs kann das zum Beispiel und sollte auf einem Synology-NAS auch genutzt werden, zudem mit Redundanz bei den Platten. Das geht leider nur bei neueren und besseren Synology-Geräten, also Augen auf beim Kauf.

Daher nicht vergessen, beim Einrichten des Shares auch "Daten-Prüfsumme für erweiterte Dateiintegrität aktivieren" einzuschalten oder wie auch immer das auf dem Speichersystem der Wahl heißt. Darunter steht in DSM 7 "Datei-Selbstreparatur und Datenbereinigung stehen zur Verfügung, um die Datenintegrität sicherzustellen" und genau das wollen wir haben. Redundanz im Speicherpool sollte ohnehin gesetzt sein, also irgendeine Technik, die den Ausfall mindestens einer Platte verkraften kann.

Einschänkung von Rechten auf dem Livesystem

Unser Backupsystem sollte natürlich auf dem Livesystem nichts verändern dürfen und auch nur Dinge zu Gesicht bekommen, die wir für das Backup vorsehen. Das rückt spätestens bei Ziel 6 in den Vordergrund, wenn wir Dritten eigene Backups beschränkt auf bestimmte Daten ermöglichen wollen.

Anders herum darf das Livesystem natürlich auf keinen Fall Schreibzugriff auf das Backupsystem haben, damit eine Kompromittierung sich nicht auch auf die Backups ausweiten kann. Also jedenfalls nicht über die bereits kompromittierten Daten im Backup hinaus. Immer daran denken: Backups machen wir nicht nur, um einen Ausfall zu kompensieren, sondern eben auch, um im Zweifel auf einen noch sauberen Stand zurückgreifen zu können, wovon wir folglich mehrere über einen gewissen Zeitraum vorhalten müssen. Dazu muss das Livesystem gar nicht aufgemacht werden oder von Ransomware betroffen sein, auch Fehlfunktionen anderer Art sind immer denkbar. Dieser Aspekt wird leider bei ernüchternd vielen Backuplösungen ignoriert, was wir hier anders machen wollen.

Hier kommt auch rsync alleine an seine Grenze. Also klar, wir können das Backup in eine Richtung implementieren, dann ist eine der beiden Anforderungen schon gelöst. Aber eben noch nicht die andere. Erlauben wir dem Backupsystem also einfach rsync über unseren SSH User (von dem es etwa bei Uberspace nur einen gibt, der alles darf), reicht eine dumme Vertauschung der Quell- und Zielparameter, um alle Livedaten zu vernichten. Lacht nicht, ist uns allen schonmal passiert. Ich habe mal, statt ein Datenbankdump mit mysqldump zu ziehen, mich in der Zeile vergriffen und den Befehl für das Einspielen eines älteren Dumps ausgeführt. Ein Moment der Unachtsamkeit und schon ist der Ärger da. Davon abgesehen möchte man sowieso nicht, dass jemand das Backupsystem kompromittiert und darüber die Livedaten angreifen kann. Der Zugriff auf die Backups ist schon unerfreulich genug.

Auftritt SSH forced commands

Wie machen wir das also? Kann man nicht an einen bestimmten SSH-Key Beschränkungen dranbinden? Das wäre jetzt echt praktisch. Hab ich mir gedacht, so ganz naiv. Der Uberspace-Support gab mir dann den Tipp, mich mal mit SSH forced commands zu beschäftigen und sie gaben mir diesen Link dazu. Hey, genau das ist hier die Lösung! Wir führen beim Login mit einem bestimmten SSH-Key, dem wir zudem den interaktiven Login und alle möglichen anderen Dinge verbieten, ein Kommando bzw. ein Script aus, das sich um die gewünschten Beschränkungen kümmert.

Das reicht noch nicht ganz, denn eine Beschränkung auf rsync an sich reicht nicht, weil man damit ja alles mögliche lesen und schreiben kann, worauf der User Rechte besitzt.

Auftritt rrsync

Man kann da natürlich ein Script hinterlegen, das da ausgeführt wird, und darin beliebig am clientseitig ausgeführten Kommando herumprüfen, das dem mitgegeben wird, und alles ablehnen, was man nicht will. Diese Arbeit haben sich die die Entwickler*innen von rsync dankenswerterweise schon gemacht und ein Script namens rrsync zur Verfügung gestellt. Das ruft man als forced command auf und übergibt einen Pfad, in den geschrieben oder aus dem gelesen werden darf. Zudem können bestimmte Funktionen von rsync beschränkt werden bzw. werden standardmäßig verboten. Das löst nun wirklich alle unsere Probleme.

Warum überhaupt Offsite-Backups?

Uberspace und hoffentlich auch die meisten anderen Hostinganbieter machen Backups der Kundendaten und von deren Datenbanken und im seltenen Fall, dass mal etwas schief läuft, haben die Profis dort es sicher besser drauf als ich, die Daten wieder klar zu bekommen. Wozu also eigene Backups zusätzlich, wenn man doch gerade eine Clouddienstleistung bucht, um sich von solchen Operations-Fragen fernhalten zu können? (Einige Hoster zocken einen übrigens komplett ab, wenn man an deren Backups will, das ist echt ein guter Grund, woanders zu hosten.)

Hier habe ich gleich eine ganze Reihe von Einwänden, allen voran: Auch die haben nicht immer alles so im Griff, wie einem das lieb ist und möchte man sich alleine darauf verlassen, wenn es um unwiderbringliche Daten geht? Uberspace zum Beispiel hat als verantwortungsbewusster Hoster und aus gutem Grund in der Dokumentation sehr klar drinstehen, dass man sich tunlichst um eigene Offsite-Backups kümmern sollte.

Cloud (und auch klassisches Hosting fällt für mich in diese Kategorie) bedeutet immer, dass man seine Daten auf Computern Dritter ablegt und denen ist man dann ausgeliefert. Wir brauchen kein schlimmes Szenario wie den aktuellen Angriffskrieg auf die Ukraine oder die damit verbunden Sanktionen gegenüber russischen Kunden westlicher Clouddienste, um zu erkennen, dass deren Backups alleine nicht reichen. Spätestens, als 2021 das eine Rechenzentrum des Anbieters OVH in Straßburg so spektakulär in Flammen aufgegangen und komplett niedergebrannt ist, sollte man sich die Frage stellen, wie zuverlässig die Backups im Rechenzentrum und unter der Kontrolle des Hostinganbieters letztlich sind. Scheiße passiert, das sollten wir immer berücksichtigen. Die Bilder von massiven Zerstörungen in der Ukraine haben wir alle gesehen (warum sollte sowas nicht irgendwann auch Dein Rechenzentrum treffen?), die Angst vor dem russischen Cyberwar ist auch gerade wieder allgegenwärtig geworden und ebenso gehören Ransomware-Gangs seit Jahren zum virtuellen Straßenbild.

Zudem möchte man, wenn man SaaS für Dritte anbietet, denen eigene Backups ihrer Daten erlauben. Sollte man jedenfalls wollen, wenn man im B2B-Bereich unterwegs ist und Kunden nicht in unnötige Abhängigkeiten und Vertrauenssituationen stürzen möchte. Andersherum: Ich empfehle dringend, sich auf geschäftskritische SaaS-Lösungen nur einzulassen, wenn man wenigstens an die eigenen hinterlegten Daten ohne Diskussion dran kommt, um eigene Offsite-Backups machen zu können.

Die Architektur

Das alles vorausgeschickt, habe ich mich also für folgende Backup-Architektur entschieden:

  1. Das Livesystem wird so präpariert, dass beim Login über SSH mit einem bestimmten SSH-Key ein Script ausgeführt wird, das rrsync benutzt und so eine nur lesende Synchronisation per rsync aus einem Ordner unserer Wahl erlaubt. In diesen Ordner legt dieses Script auch vorher noch einen aktuellen Datenbankdump, denn die Datenbank wollen wir natürlich auch sichern.
  2. Wir richten unser Backupsystem so ein, dass es sich mit einem SSH-Key am Livesystem anmeldet und über rsync täglich einen neuen Ordner über Hardlinks anlegt und damit nur die veränderten Daten zieht. Oder nichts, wenn es nichts neues gibt, dann ist man schnell durch. Hier brauchen wir ein Script, das den neuesten Backup-Ordner findet und rsync mit dem --link-dest Parameter als Basis für die Erstellung der Hardlinks anreicht.

Damit erfüllen wir alle Ziele mit einer Einschränkung: Es gibt unter Windows kein natives rsync und meine Versuche mit cwrsync verliefen nicht besonders zufriedenstellend, jedenfalls was die Nutzung meines normalen SSH-Keys anging an einem Synology-NAS als Gegenstelle. Kriegt man sicher hin, aber nicht ohne Gefummel und Gefummel wollen wir bei Backuplösungen nicht haben. Linux und andere unixoide Systeme mit nativem SSH und rsync sollten hingegen kein Problem darstellen. Randbemerkung: Vergesst PuTTY und nutzt die native OpenSSH-Implementierung in Windows 10 und 11. Man will nie wieder zurück.


Eine Anleitung für eine beispielhafte Implementierung mit Uberspace und einem Synology-NAS als Backupziel

Diese Anleitung lässt sich mit leichten Adaptierungen auch für beliebige andere Systeme nutzen, da Uberspace sehr nah am typischen Linux-System arbeitet und Synology seinerseits ebenfalls. Hier sind im aktuellen DSM 7 nur wenige Klimmzüge zu machen, sonst verhält sich alles wie auf anderen Linux-Systemen auch. Ein Raspberry Pi mit irgendeinem Linux drauf tut es also ebenfalls (wobei zuverlässiges Storage da ein Fragezeichen darstellt) oder ein Server mit irgendeiner NAS-Distribution wie TrueNAS (ehemals FreeNAS), OpenMediaVault, Unraid, you name it.

Andere Hoster haben möglicherweise keinen SSH-Zugriff oder erlauben keine Manipulation an dessen Konfiguration oder erst gar keinen Login mit einem SSH-Key oder es gibt dann kein rsync. In dem Fall habt Ihr hoffentlich nicht allzu viel Zeit mit diesem Artikel verbracht oder nutzt die Gelegenheit, mal mit dem Hoster zu sprechen oder einen besseren zu suchen. Aus meiner Sicht ist das eine Conditio sine qua non für Webhosting mit einem Mindestmaß an professionellem Anspruch, aber das sehen viele große Hoster ganz anders. Unter anderem deshalb hoste ich bei Uberspace. Don't get me started on this. Wenn Ihr einen Rootserver zu sichern habt, sollte das alles kein Problem sein, falls einem nicht Plesk oder ähnliches einen Strich durch die Rechnung macht.

Ich setze voraus, dass man grundsätzlich weiß, wie man sich mit SSH irgendwo einloggt und auch sonst nicht vor Angst erstarrt oder in Tränen ausbricht, wenn man ein Terminal benutzt. Terminaleingaben markiere ich mit einem vorangestellten $, damit diese als solche zu erkennen sind, das also natürlich nicht mitkopieren! Diese Anleitung soll anderen vor allem Recherchearbeit ersparen und einen konsistenten Weg aufzeigen, wie man sowas lösen kann. Ich habe mir das selber alles aus zig verschiedenen Quellen zusammengesucht und so lange herumprobiert, bis ich einigermaßen verstanden habe, was da passiert und wie es passiert. Wenn Ihr Verantwortung für irgendwelche Daten tragt, solltet Ihr das auch tun. Also nicht einfach Copy&Paste machen und dann hoffen, dass das schon irgendwie gut geht!

Schritt 1: Vorbereitung des Backupsystems bis zum SSH-Login über einen SSH-Key auf dem Livesystem

Hier wollen wir erst einmal so weit kommen, dass die Eingabe von ssh USERNAME@LIVESYSTEM im Terminal des Backupsystems zu einem Login mit SSH-Key (also ohne Passwort) am Livesystem führt. Wer das schon am laufen hat, kann den Schritt überspringen.

Nehmen wir als Hostnamen unseres NAS-Systems jetzt mal meinnas an, damit die Anleitung funktioniert.

So bekommt man das auf einem Synology-NAS mit DSM 7 recht flott hin:

  1. Wir legen ein Konto für die Backups im DSM an und geben diesem Schreibzugriff auf einen freigegebenen Ordner, auf den vorzugsweise sonst niemand Zugriff hat. Wir nennen das Konto und die Freigabe jetzt mal uberspace-backup. Der eigentliche Speicherpfad lautet dann zum Beispiel /volume1/uberspace-backup, den werden wir nachher brauchen.

    Bei der Freigabe setzen wir die Haken bei "Verbergen sie diesen gemeinsamen Ordner unter "Netzwerkumgebung"" und "Unterordner und Dateien vor Benutzern ohne Berechtigung ausblenden" und deaktivieren den Papierkorb.

    Das Volume, auf dem die Freigabe erstellt wird, sollte mit Btrfs formatiert sein und auf einem Speicherpool mit mindestens der Stufe *Mit Datenschutz für 1-Laufwerke-Fehlertoleranz* liegen. Das geht alles auch auf einer einzelnen Platte mit ext4, aber so richtig schlau finde ich das nicht, siehe meine Vorüberlegungen.

    Verschlüsseung der Freigabe kann man machen, interessiert uns hier aber nicht. Im nächsten Schritt aktivieren wir aber unbedingt "Daten-Prüfsumme für erweiterte Dateiintegrität aktivieren" (siehe oben zum Thema Bit Rot) und wenn wir wollen auch *Dateikomprimierung aktivieren*, das ist die Deduplizierung auf Dateisystemebene, die wir wegen der Hardlink-Lösung von rsync aber nicht allzu sehr beanspruchen werden.

    Unser Konto bekommt hier Schreibrechte, alle anderen nur mit gutem Grund, auch nicht die Admingruppe. Die können sich die Rechte zwar selber wieder erteilen, aber wir wollen unnötige Fehlerquellen vermeiden.

  2. Jetzt aktivieren wir unter "Benutzer und Gruppe" im Tab erweitert ganz unten "Benutzer-Home-Dienst aktivieren", damit alle Konten ein Homeverzeichnis bekommen, wo die SSH-Schlüssel abgelegt werden. Leider gilt das für alle Nutzer, damit müssen wir leben.

  3. Unter "Terminal & SNMP" aktivieren wir "SSH-Dienst aktivieren", damit wir uns mit Konten aus der Gruppe Administratoren über SSH einloggen können. Aus dem Grund werden wir auch jetzt temporär unser neues Konto in die Gruppe der Administratoren aufnehmen. Für den späteren Betrieb nehmen wir das zurück.

  4. Wir haben jetzt ein Konto, passenden Storage und einen Terminal-Zugang über SSH. Darüber loggen wir uns jetzt auch ein. Auf modernen Systemen machen wir dazu ein Terminal auf unserem lokalen Rechner auf und verbinden uns auf unser Backupsystem, unter Windows 10 muss man das native OpenSSH-Feature erst nachinstallieren, wozu es im Netz zahlreiche Anleitungen gibt, notfalls kann man auch wie früher PuTTY nehmen:

    $ ssh uberspace-backup@meinnas
    

    Wir werden beim ersten Verbinden nach einer Prüfung des Fingerprints gefragt und dann auf jeden Fall nach unserem Passwort, das ist das Passwort, das wir für das Konto vergeben und hoffentlich notiert haben. Danach sollten wir einen Prompt in der Form uberspace-backup@meinnas:~$ sehen. Wir befinden uns also im Homeverzeichnis unseres Kontos, ein $ ls -la sollte uns ein leeres Verzeichnis anzeigen, das unserem Konto gehört. Mit $ pwd können wir uns den Pfad anzeigen lassen, der lautet in unserer Anleitung /var/services/homes/uberspace-backup, auf anderen System oft auch einfach /home/uberspace-backup.

  5. Wir müssen jetzt einen SSH-Key erzeugen (oder einen bestehenden verwenden) und die richtigen Rechte zuweisen. Ein

    $ ssh-keygen -t ed25519 -C "uberspace-backup on meinnas"
    

    erzeugt ein solches Schlüsselpaar im Verzeichnis ~/.ssh, den abgefragten Dateinamen können wir bei id_ed25519 belassen, das Passwort lassen wir leer: Wer hier Zugriff auf den Schlüssel erlangt, kriegt auch eine irgendwie hinterlegte Passphrase heraus und wir haben ein ganz anderes Problem. Der String hinter dem -C ist ein Kommentar, mit dem wir unseren Schlüssel später besser identifizieren können, eine konkrete Bedeutung hat er nicht. Da wir 2022 haben, möchten wir hier unbedingt einen ed25519-Schlüssel haben und keinen RSA-Schlüssel, wie man ihn in vielen Anleitungen sieht. Davon abgesehen, dass das neue Verfahren allgemein als sicherer angenommen wird, ist vor allem der öffentliche Schlüssel viel kürzer und damit handlicher: Niemand scrollt gerne seitwärts in einem Editorfenster in einem Terminal.

    Danach müssen wir mit

    $ chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_ed25519
    

    recht restriktive Rechte für unser .ssh-Verzeichnis und unseren privaten Schlüssel darin einstellen, sonst weigert sich OpenSSH zu Recht, den zu verwenden.

  6. Der öffentliche Teil des Schlüssels muss nun unserem Uberspace-Konto oder wo auch immer unser Livesystem liegt, bekannt gemacht werden. Dazu loggen wir uns im Fall von Uberspace über SSH dort ein und fügen den öffentlichen Schlüssel ans Ende der Datei ~/.ssh/authorized_keys ein, die später noch eine wichtige Rolle spielen wird (weswegen wir nicht den praktischen Shortcut mit ssh-copy-id benutzen).

    Auf dem Backupsystem können wir uns unseren öffentlichen Teil des Schlüssels mit einem

    $ cat ~/.ssh/id_ed25519.pub
    

    anzeigen lassen. Das ist nur eine relativ kurze Zeile, die mit ssh-ed25519 beginnt, gefolgt vom eigentlichen öffentlichen Schlüssel, gefolgt von unserem Kommentar, den wir bei der Erstellung angegeben haben. Hier sieht man auch sofort, warum der so hilfreich ist. Die eine Zeile kopieren wir uns in die Zwischenablage, weil wir sie gleich brauchen werden.

    Auf dem Livesystem bearbeiten wir die Datei mit den akzeptierten Schlüsseln, ich verwende nano dafür, weil ich aus vim ohne Google nie wieder herausfinde:

    $ nano ~/.ssh/authorized_keys
    

    Dort fügen wir am Ende unsere Zeile mit dem öffentlichen Schlüssel des Backupsystems ein, speichern mit Strg+o und schließen den Editor mit Strg-x. Mit dem Livesystem sind wir jetzt erstmal fertig.

  7. Spannung: Wir loggen uns jetzt erstmalig vom Backupsystem aus mit dem neuen Schlüssel ohne Passwort am Livesystem ein:

    $ ssh USERNAME@LIVESYSTEM
    

    Also wie man das sonst auch vom lokalen Rechner aus tun würde. USERNAME ist der Name des Uberspaces, LIVESYSTEM unser Host, also vermutlich irgendeinstern.uberspace.de.

    Wie gewohnt, sollte uns der Fingerprint vom Livesystem angezeigt werden, den wir akzeptieren und danach sollten wir ohne Passworteingabe den Prompt des Livesystems sehen, also sowas wie [USERNAME@HOST ~]$.

    Wenn wir hier angekommen sind, sind wir schon da. Mit

    $ exit
    

    Verlassen wir die SSH-Verbindung auf das Livesystem und kehren zum Backupsystem zurück.

  8. Nun gönnen wir uns etwas, machen Pipi und brühen uns ein neues Heißgetränk auf. Ein bisschen Schulterklopfen ist auch drin. Wenn wir Lust haben, können wir jetzt schonmal mit scp oder rsync herumexperimentieren und staunen, dass das einfach so geht. Bitte nicht zur Begrüßung Quelle und Ziel verwechseln und damit das Livesystem platt machen, das zu verhindern ist erst Aufgabe des übernächsten Schritts!

Wir haben also einen funktionierenden SSH-Login mit SSH-Key vom Backupsystem aus auf das Livesystem und einen Pfad, wo unsere Backups gelagert werden sollen, in unserem Beispiel war das: /volume1/uberspace-backup. Viele Backup-Lösungen enden hier und bauen sich ein Script, um mit rsync Dateien über diese SSH-Verbindung zu ziehen. Wenn uns Ziel 5 und 6 nicht so wichtig sind, reicht das auch vollkommen aus. Beginnen wir also mit genau so einem Beispiel-Script für Uberspace-Spaces:

#!/bin/bash

backup_uberspace()
{
    TODAY=$(date +%Y-%m-%d)
    echo ""
    echo "##########"
    echo "Backup ${UBERSPACEUSER}@${UBERSPACESERVER} $TODAY"
    echo ""

    # The source path to backup. Can be local or remote.
    SOURCE=${UBERSPACEUSER}@${UBERSPACESERVER}:/var/www/virtual/$UBERSPACEUSER/

    # Where to store the differential backups
    DESTBASE=/volume1/uberspace-backup/${UBERSPACEUSER}/

    # create our destination path, if it does not exist
    if [ ! -d "$DESTBASE" ]
    then
        mkdir -p $DESTBASE
    fi

    # Where to store today's backup
    DEST="${DESTBASE}$TODAY"

    # Where to find the last backup that is not from today
    # don't be confused by the stupid regex: find doesn't
    # know about quantifiers
    # other folders than our date-based ones are ignored
    LASTDEST=`find ${DESTBASE}* -type d -prune -regex ".*/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" -not -name "${TODAY}" | tail -n 1`
    if [ ! -z "$LASTDEST" ]
    then
        # set a found last backup as base for our hardlinks
        LINKOPT="--link-dest $LASTDEST/"
    else
        # no last backup found, so just create a new one with no links
        LINKOPT=
    fi

    # Run the rsync
    rsync -rltv $LINKOPT $ADDITIONALPARAMS "$SOURCE" "$DEST"
}

UBERSPACEUSER=uspuser
UBERSPACESERVER=einstern.uberspace.de

# here you can set some paths (relative to the location of
# the SOURCE), that will be excluded from the backup, the
# examples are good for a typical Symfony-application
# I don't know why, but I could not find a working way to
# encapsulate the given exclude paths within '' oder "",
# so we hope that there is no whitespace involved
ADDITIONALPARAMS="--exclude var/cache/* --exclude vendor/*"

backup_uberspace


UBERSPACEUSER=otherusp
UBERSPACESERVER=andererstern.uberspace.de
# nothing to exclude here, so reset ADDITIONALPARAMS!
ADDITIONALPARAMS=""

backup_uberspace

Was haben wir hier? Erstmal ab Zeile 3 eine Funktion, die wir später für verschiedene Spaces aufrufen können, was wir unten im Script beispielhaft für zwei Spaces sehen. Die Kommentare sollten uns bereits helfen, aber da Shell-Scripting nicht mein Lieblingsthema ist, gehe ich auch nicht allzu tief darauf ein. Ihr wollt und könnt hier sicher ohnehin etwas eigenes und viel besseres implementieren. Was mein Script tut ist folgendes:

Für jeden zu sichernden Space gibt es in unserem Backup-Pfad einen eigenen Ordner, der nach dem Usernamen des Spaces benannt wird. Darin wird für jeden Tag, an dem die Sicherung läuft, ein Unterordner mit dem aktuellen Tagesdatum angelegt. Dann wird geguckt, ob es einen älteren Tagesordner gibt, damit dieser für rsync als --link-dest übergeben werden kann. Dieser dient also als Basis für die Hardlinks, die rsync verwendet. Ansonsten werden noch eventuell vorhandene ADDITIONALPARAMS übergeben, in unserem Beispiel sind das im ersten Space exkludierte Verzeichnisse für cache und vendor, die wir im Backup nicht brauchen. Für den zweiten Space bleiben die einfach leer.

Als Schalter für rsync habe ich hier -rltv gesetzt, also rekursives Kopieren, Symlinks werden als solche übernommen, die Dateizeiten werden übernommen und wir kriegen eine wortreiche Ausgabe zu sehen. Nutzer, Gruppen und andere Finessen interessieren mich bei einem solchen Backup nicht, hier sollte man die Schalter also entsprechend den eigenen Bedürfnissen einstellen, wenn man das anders haben will. Also mal in Ruhe die Schalter in der Dokumentation durchgehen und schauen, was man gebrauchen kann.

In der Funktion sollten wir bei der Zuweisung der Variable DESTBASE unseren tatsächlichen Backup-Pfad nutzen, den wir uns oben notiert haben. Das ${UBERSPACEUSER}/ am Ende lassen wir natürlich stehen.

Unter der Funktion sehen wir zwei Blöcke, wo jeweils die Variablen UBERSPACEUSER, UBERSPACESERVER und ADDITIONALPARAMS neu gesetzt werden, um danach die backup_uberspace-Funktion aufzurufen, die diese Variablen nutzt. Sieht für Entwickler wie mich wenig ausgefuchst aus, funktioniert aber. Shell ist wie gesagt nicht mein Lieblingsthema.

Das Script können wir nach unseren Wünschen anpassen und dann irgendwo auf dem Backupsystem ablegen, wo unser Backup-Konto Zugriff darauf hat (ich habe es im Homeverzeichnis liegen). Hier erweist sich die App Text-Editor auf dem Synology-NAS, die man aus dem Paketzentrum installieren kann, als ausgesprochen hilfreich.

Wenn wir das Script auf dem Backupsystem haben, wollen wir es erst mal ausprobieren und an den Details schrauben, denn es gibt immer etwas zu schrauben! Dazu einfach in der SSH-Verbindung zum Backupsystem das Script aufrufen. Nehmen wir an es liegt in unserem Home-Verzeichnis und heißt rsync_backup_from_uberspace.sh, dann sollte es mit einem

$ ~/rsync_backup_from_uberspace.sh

aufrufbar sein. Möglicherweise muss es mit einem $ chmod +x ~/rsync_backup_from_uberspace.sh erst ausführbar gemacht werden, damit das klappt. Wir sollten nun die Ausgabe sehen und wenn wir alles richtig gemacht haben, sollten jetzt alle Dateien aus unserem Webroot vom Livesystem in unseren Tagesordnung auf dem Backup-Speicher landen. Falls nicht, muss noch etwas Feinschliff folgen. Hatte ich erwähnt, dass die Text-Editor-App hier echt hilfreich ist?

Wenn wir zufrieden sind, können wir unser Script im Aufgabenplaner der Diskstation einrichten: Als Benutzer wählen wir natürlich unser neues Backup-Konto, der Zeitplan sollte zweckmäßigerweise täglich oder nach Wunsch auch seltener sein und der Befehl lautet in unserem Beispiel einfach: /var/services/homes/uberspace-backup/rsync_backup_from_uberspace.sh

Das war es an der Stelle schon. Im Aufgabenplaner können wir das Backup nochmal testweise ausführen und unter Aktion auch die Ausgabe einsehen. Protipp: Das ausführende Konto braucht Schreibrechte auf den Ordner, den man bei den Aufgabenplater-Einstellungen für die Logs gesetzt hat. Ohne Schreibrechte dort bleiben die Ausgaben leer und es gehen auch keine E-Mails raus.

Wenn uns das schon reicht, sind wir jetzt durch. Allerdings fehlt hier noch das Datenbank-Backup. Anfangs hatte ich dazu in dem Script noch einen zweiten Part drin, der die Dumps aus /mysql_backups von Uberspace zieht, aber mit der gleich vorgestellten Lösung wurde das obsolet, daher reden wir hier nicht weiter drüber. Alternativ kann man auch einen Cronjob auf dem Livesystem machen, der täglich einen Dump in den Webroot legt (natürlich nicht in den public-Teil davon!).

Ach ja, wenn wir das Script an einem Tag mehrmals ausführen, werden in der Zwischenzeit gelöschte Dateien aus dem aktuellen Backup nicht entfernt. Den entsprechenden --delete-Schalter von rsync habe ich weggelassen, der würde genau das tun. Da ich die Backups ohnehin nur einmal täglich laufen lasse, brauche ich den schlicht nicht und weniger ist mehr bei solchen gefährlichen Dingen. Durch die täglich neuen Ordner schleppen wir übrigens keine Altlasten mit uns herum: Die Hardlinks werden nur für aktuell vorhandene Dateien gesetzt.

Schritt 3: Absicherung über SSH Forced Command und rrsync

Atmen wir einmal kurz durch, denn wir haben bereits ein funktionierendes Backup, das täglich einen Snapshot der Webroots unserer Spaces anlegt. Das ist ein Grund zum Feiern.

Der letzte Schritt ist ein kurzer und spielt sich fast ausschließlich auf den Livesystemen ab. Für jedes zu sichernde Livesystem wiederholen wir die folgenden Dinge:

Uberspace hat pro Konto ein eigenes bin-Verzeichnis, das nutzen wir jetzt. Also auf dem Livesystem über SSH einloggen und folgende Kommandos eingeben (für andere Hoster ggf. entsprechend anpassen):

$ cd ~/bin
$ wget https://ftp.samba.org/pub/unpacked/rsync/support/rrsync
$ chmod +x rrsync
$ nano rrsync-backup-webroot.sh

Wir landen jetzt im nano-Editor und fügen die folgenden sieben Zeilen ein, die erfreulicherweise für jeden Space gleich sind:

#!/bin/sh

DATE=$(date +%Y-%m-%d-%H%M)
WEBROOT=/var/www/virtual/$USER
mysqldump --all-databases | gzip > $WEBROOT/$USER-$DATE.sql.gz
$HOME/bin/rrsync -ro $WEBROOT
nohup rm $WEBROOT/$USER-$DATE.sql.gz

Was passiert hier? Das ist ein kurzes Script, das einen Dump der Datenbanken des Kontos im Webroot mit Zeitstempel ablegt. Danach wird unser gerade heruntergeladenes Script rrsync gestartet und unser Webroot als read-only angegeben. Wenn das durch ist, wird der Dump wieder gelöscht, damit sich nicht ewig viele Dumps ansammeln. Dieses Script speichern wir mit Strg-o, verlassen den Editor mit Strg-x und machen es ausführbar:

$ chmod +x rrsync-backup-webroot.sh

Jetzt folgt der eigentliche Trick: Wir stellen unserem vorhin hinterlegten Schlüssel unser soeben erstelltes Script als forced command voran. Dazu bearbeiten wir wieder die ~/.ssh/authorized_keys:

$ nano ~/.ssh/authorized_keys

Dort fügen wir vor dem ssh-ed25519 unseres Backup-Keys von vorhin folgende schönen Dinge ein, an den Anfang der gleichen Zeile und getrennt mit einem Leerzeichen:

command="~/bin/rrsync-backup-webroot.sh",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding

Wie gehabt speichern wir mit Strg-o und verlassen den Editor mit Strg-x. Das führt nun beim SSH-Login mit diesem Key unser Script aus, das wiederum mit rrsync dafür sorgt, dass da nur ein rsync mit . als Quellordner erlaubt ist, wofür dann der im Script mit dem -ro-Schalter angegebene Pfad genutzt wird. Die anderen no-Schalter sorgen dafür, dass auch nichts anderes mit dem Key gemacht werden kann, was SSH alles noch so kann. Das forced command sollte das zwar ohnehin verhindern, aber explizit verbieten schadet auch nicht. Wenn wir den Dump ohnehin nicht brauchen oder anders erstellen, können wir uns das Script übrigens auch sparen und das rrsync-Kommando direkt hier übergeben: command="~/bin/rrsync -ro /var/www/virtual/$USER"

Das ist schon die ganze Magie!

Wer mitgedacht hat, weiß, was jetzt noch zu tun bleibt: In unserem Backup-Script auf dem Backup-System müssen wir die Pfadangabe hinter dem : bei SOURCE= durch einen . ersetzen. Also wird aus

SOURCE=${UBERSPACEUSER}@${UBERSPACESERVER}:/var/www/virtual/$UBERSPACEUSER/

jetzt ein

SOURCE=${UBERSPACEUSER}@${UBERSPACESERVER}:.

Denn rrsync sorgt dafür, dass man im richtigen Quellverzeichnis landet und zwar nur dort. Also speichern wir unser Backup-Script und probieren es aus (auf der Shell oder im Aufgabenplaner). Es sollte auf Anhieb wie gehabt weiterlaufen, aber nun einen Dump der Datenbank beinhalten.

Wenn alles funktioniert, können wir unser Konto uberspace-backup wieder aus der Administratoren-Gruppe auf dem NAS entfernen, denn einen SSH-Zugriff auf die Shell brauchen wir jetzt erst wieder, wenn wir erneut herumprobieren müssen. Dabei nicht vergessen, dass unser Konto Schreibrechte im für Logs im Aufgabenplaner vorgesehenen Ordner braucht, sonst bleiben die Logs leer.


Finale Gedanken

Das meiste hiervon klappt auch auf anderen System, das Ziel der Portabilität sollte also erreicht sein, vermutlich lässt sich das auch unter macOS so machen. Vielleicht mag das ja mal jemand ausprobieren. Auf Linux-Systemen sollte das überall weitgehend identisch klappen, Besonderheiten beim Storage und der Kontenverwaltung natürlich nicht berücksichtigt. Ob Windows als Backupsystem funktioniert, ist mir nicht ganz klar, mit einem WSL (das is das eingebaute Linux) vermutlich schon, aber das ist ja auch eher Gefummel.

Wenn es um Ziel 6 geht, also um das Zurverfügungstellen von nur bestimmten Dateien, habe ich schon eine Idee, wie man das am besten lösen kann: Statt das Webroot-Verzeichnis bei rrsync zu erlauben, legen wir uns ein gesondertes Verzeichnis in ~ an, wo wir die gewünschten Dateien und Verzeichnisse über Symlinks reinmappen. Statt des -l Schalters von rsync, der Symlinks als solche kopiert, nutzen wir dann den -L-Schalter oder besser verständlich --copy-links, dadurch werden die Links aufgelöst und wir bekommen die verlinkten Dateien und Verzeichnisse geliefert. Mit den Symlinks können wir bequem bestimmen, welche Daten zur Verfügung stehen. Ausprobiert habe ich das noch nicht, bei Gelegenheit schreibe ich dann mal ein Update.

Insgesamt halte ich das für eine erfreulich unkomplexe Lösung, weil sie sehr nah an den absoluten Basics (also SSH und rsync) bleibt, die seit Jahrzehnten millionenfach im täglichen Dauereinsatz und entsprechend stabil und erprobt sind. Wir verzichten auch auf eigene selbstgebastelte Spielereien und Gefummel, die die Stabilität in Zweifel ziehen würden. Unsere Ziele sind jedenfalls erreicht und wir haben wieder etwas über Linux gelernt. Genau deswegen mag ich Uberspace so gerne: Man lernt immer wieder etwas neues, was man auch anderswo anwenden kann. Ich werde mit dem System auch einen eigenen Rootserver sichern, von dem bislang und seit vielen Jahren bereits täglich mit SCP ein Komplettbackup der Hostingdateien und einiger Settings auf ein Synology-NAS im gleichen Netzwerk (aber anderem Gebäude) gezogen wird. Hier lebe ich bislang damit, dass eine Kompromittierung meines NAS-Systems auch das Livesystem kompromittieren würde, wobei das NAS-System in einem VLAN ohne Zugriff von Außen viel besser abgesichert ist, als das Livesystem auf einer VM in einer privaten Cloud. Die neue Lösung kommt mir hier dennoch wesentlich smarter vor.

Bitte um Feedback

Ich hoffe, dass ich keinen Schritt vergessen habe oder Dinge auf dem NAS schon eingerichtet waren, die standardmäßig nicht eingerichtet sind. Wer Feedback dazu hat, darf sich gerne bei mir melden über Twitter oder per E-Mail oder auch persönlich. Feedback zum eigentlichen Script oder "xy ist eine ganz dumme Idee, mach das besser so:" ist auch stets willkommen. Ansonsten hoffe ich, dass ich jemandem weiterhelfen konnte und dass dieser Artikel hilft, dass man etwas weniger Infos zusammensuchen muss, wenn man seine Hostingdateien sicher und bequem sichern möchte.

Zum Abschluss noch ein paar Hostingtipps für Projekte ab mittlerem Wichtigkeitsniveau:

  • Betreibt keine Rootserver, wenn Ihr nicht sehr genau wisst, was Ihr da tut. Wirklich, lasst das!
  • Hostet nicht bei Hostern, die Euch den Zugriff auf Backups nur gegen halsabschneiderische Summen erlauben.
  • Hostet nicht bei Hostern, die Euch in sinnvoll bepreisten Paketen keinen SSH-Zugriff erlauben oder keinen SSH-Zugriff über Keys oder kein rsync anbieten. Das geht sehr wohl alles auch bei Shared-Hosting.
  • Hostet nicht bei Hostern, bei denen zusätzliche FTP-Accounts (für abweichende Unterordner) nur ohne Verschlüsselung funktionieren. WTF?
  • Hostet nicht bei Hostern, die kein Let's Encrypt anbieten, weil sie Euch für jede (Sub-)Domain gesondert mehrere Euro pro Monat und Zertifikat berechnen wollen und großzügigerweise ein(!) Inklusivzertifikat ins 10-Euro-Paket legen.

Noch keine Kommentare

Die Kommentarfunktion wurde vom Besitzer dieses Blogs in diesem Eintrag deaktiviert.