<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>IT-Security on Kabelbruch</title><link>https://kabelbruch.de/tags/it-security/</link><description>Recent content in IT-Security on Kabelbruch</description><image><title>Kabelbruch</title><url>https://kabelbruch.de/og-image.png</url><link>https://kabelbruch.de/og-image.png</link></image><generator>Hugo</generator><language>de-DE</language><lastBuildDate>Sat, 31 Oct 2026 11:45:00 +0100</lastBuildDate><atom:link href="https://kabelbruch.de/tags/it-security/index.xml" rel="self" type="application/rss+xml"/><item><title>VPS-Backup im Pull-Prinzip: Der Server darf nicht nach Hause telefonieren</title><link>https://kabelbruch.de/posts/vps-backup-pull-prinzip-borg/</link><pubDate>Sat, 31 Oct 2026 11:45:00 +0100</pubDate><guid>https://kabelbruch.de/posts/vps-backup-pull-prinzip-borg/</guid><description>VPS-Backup im Pull-Prinzip: tar und systemd auf dem Server, rrsync mit Nur-Lese-Zugriff, Borg-Verschlüsselung im Homelab – inklusive Restore-Test.</description><content:encoded><![CDATA[<p>Mein VPS wird jetzt jede Nacht gesichert – und zwar so, dass er selbst keinen Weg ins Homelab bekommt. Das Homelab holt sich die Daten ab, nicht umgekehrt.</p>
<h2 id="worum-gehts">Worum geht&rsquo;s?</h2>
<p>Die Website selbst ist mir eigentlich egal, wenn der VPS mal umfällt. Die baut die Pipeline aus dem Git-Repo in ein paar Sekunden neu. Was mir <strong>nicht</strong> egal ist: die ganze Handarbeit in <code>/etc</code>. Caddy-Konfiguration mit Security-Headern und CSP-Hashes, SSH-Härtung, ufw, logrotate, die eingeschränkten Benutzer für Deploy und Log-Abruf. Das alles noch mal aus dem Kopf (oder aus meinen Notizen) nachzubauen, wäre ein Abend, auf den ich keine Lust habe.</p>
<p>Also: ein Backup. Aber eins, das zu meinem Grundsatz für diesen Server passt.</p>
<h3 id="pull-statt-push">Pull statt Push</h3>
<p>Die meisten Backup-Setups arbeiten nach dem <strong>Push-Prinzip</strong>: Der Server, der gesichert wird, verbindet sich zum Backup-Ziel und schiebt seine Daten dorthin. Für einen VPS im Internet heißt das aber: Er bräuchte Zugangsdaten und eine offene Tür in mein Homelab. Wird der VPS kompromittiert, hat der Angreifer diese Tür gleich mit – im schlimmsten Fall inklusive Schreibrechten auf meine Backups.</p>
<p>Beim <strong>Pull-Prinzip</strong> läuft es andersherum: Der VPS legt das Backup nur lokal ab. Eine VM in meinem Homelab holt es nachts ab – und darf dabei ausschließlich <strong>lesen</strong>, und das auch nur in genau einem Verzeichnis. Der VPS weiß nicht einmal, dass es mein Homelab gibt.</p>
<p>Genau so habe ich es schon beim Abholen der Zugriffsprotokolle für die Besucherstatistik gemacht. Das Muster hat sich bewährt, also bekommt das Backup denselben Aufbau.</p>
<h2 id="was-gesichert-wird--und-was-nicht">Was gesichert wird – und was nicht</h2>
<table>
	<thead>
			<tr>
					<th>Gesichert</th>
					<th>Nicht gesichert</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/etc</code> komplett (inkl. SSH-Host-Keys)</td>
					<td><code>/var/www</code> – baut die Pipeline neu</td>
			</tr>
			<tr>
					<td><code>authorized_keys</code> aller Benutzer</td>
					<td><code>/var/lib/caddy</code> – Zertifikate holt Caddy neu</td>
			</tr>
			<tr>
					<td>Paketliste (<code>dpkg --get-selections</code>)</td>
					<td><code>/var/log</code> – siehe unten</td>
			</tr>
			<tr>
					<td>manuell installierte Pakete (<code>apt-mark showmanual</code>)</td>
					<td></td>
			</tr>
	</tbody>
</table>
<p>Die Logs lasse ich bewusst weg. In meiner Datenschutzerklärung steht, dass Zugriffsprotokolle nach 14 Tagen gelöscht werden. Ein Backup mit 30 Tagen Vorhaltung, in dem die Logs drinstecken, würde diese Aussage still und heimlich aushebeln.</p>
<p>Die SSH-Host-Keys im Archiv sind der Grund, warum das Backup im Homelab <strong>verschlüsselt</strong> liegen soll. Wer die privaten Host-Keys hat, kann sich gegenüber SSH-Clients als mein Server ausgeben.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li><strong>VPS:</strong> Debian 13, rsync mit <code>rrsync</code> (liegt bei Debian im rsync-Paket unter <code>/usr/bin/rrsync</code>), systemd</li>
<li><strong>Homelab:</strong> eine Debian-13-VM (bei mir die Docker-VM), rsync 3.5.0, <strong>BorgBackup 1.4.0</strong> aus den Debian-Paketen</li>
<li>Die VM selbst wird täglich per Proxmox Backup Server gesichert – das Borg-Repo wandert also automatisch mit</li>
</ul>
<p>Ein Wort zu Borg 2: Gibt es, ist aber zum Zeitpunkt dieses Artikels noch Beta und laut den Entwicklern nicht für den Produktivbetrieb gedacht. Ich bleibe bei 1.4.</p>
<h2 id="umsetzung">Umsetzung</h2>
<h3 id="schritt-1-ein-benutzer-der-nur-lesen-darf">Schritt 1: Ein Benutzer, der nur lesen darf</h3>
<p>Auf dem VPS bekommt der Abruf einen eigenen Systembenutzer und ein Zielverzeichnis, in das nur root schreiben darf:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo useradd --system --user-group --create-home --home-dir /home/backuppull --shell /bin/sh backuppull
</span></span><span class="line"><span class="cl">sudo install -d -m <span class="m">0750</span> -o root -g backuppull /var/backups/vps
</span></span><span class="line"><span class="cl">sudo install -d -m <span class="m">0700</span> -o backuppull -g backuppull /home/backuppull/.ssh
</span></span></code></pre></div><p>Die Shell muss eine echte sein, kein <code>nologin</code>: sshd startet den erzwungenen Befehl (dazu gleich mehr) über die Login-Shell des Benutzers.</p>
<h3 id="schritt-2-pull-key-mit-maulkorb">Schritt 2: Pull-Key mit Maulkorb</h3>
<p>Auf der Homelab-VM erzeuge ich einen eigenen SSH-Key nur für diesen Zweck. Ohne Passphrase, weil er nachts ohne mich laufen muss:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ssh-keygen -t ed25519 -N <span class="s1">&#39;&#39;</span> -C <span class="s1">&#39;homelab vps-backuppull&#39;</span> -f /root/.ssh/vps_backuppull_ed25519
</span></span></code></pre></div><p>Ein Key ohne Passphrase klingt erst mal nach schlechter Idee. Entscheidend ist, was man mit ihm <strong>darf</strong> – und das regle ich auf dem VPS in der <code>authorized_keys</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">restrict,command=&#34;/usr/bin/rrsync -ro /var/backups/vps&#34; ssh-ed25519 AAAA... homelab vps-backuppull
</span></span></code></pre></div><ul>
<li><code>restrict</code> schaltet alles ab, was SSH sonst noch kann: Port-Forwarding, Agent-Forwarding, X11, ein Terminal.</li>
<li><code>command=</code> ist ein <strong>Forced Command</strong>: Egal, was der Client ausführen will, sshd startet stattdessen diesen Befehl.</li>
<li><code>rrsync -ro /var/backups/vps</code> ist ein Wrapper für rsync, der den Zugriff auf genau dieses Verzeichnis begrenzt – und mit <code>-ro</code> nur lesend.</li>
</ul>
<p>Selbst wenn jemand den Key aus meinem Homelab klaut, kann er damit nur das abholen, was ohnehin für das Homelab bestimmt ist.</p>
<h3 id="schritt-3-host-key-nicht-blind-vertrauen">Schritt 3: Host-Key nicht blind vertrauen</h3>
<p>Damit das Homelab sicher sein kann, dass es wirklich mit meinem VPS spricht, braucht es dessen Host-Key. Der bequeme Weg wäre <code>ssh-keyscan</code> – aber dann vertraue ich dem, was in dem Moment antwortet. Stattdessen lese ich den Key direkt auf dem VPS aus …</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat /etc/ssh/ssh_host_ed25519_key.pub
</span></span></code></pre></div><p>… und lege ihn auf der Homelab-VM in einer eigenen <code>known_hosts</code>-Datei nur für diesen Abruf ab:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;example.com ssh-ed25519 AAAA...&#39;</span> <span class="p">|</span> sudo tee /root/.ssh/known_hosts_vps &gt;/dev/null
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">0600</span> /root/.ssh/known_hosts_vps
</span></span></code></pre></div><p>Zusammen mit <code>StrictHostKeyChecking=yes</code> bricht die Verbindung ab, sobald sich am anderen Ende ein anderer Key meldet.</p>
<h3 id="schritt-4-testen--auch-das-was-nicht-gehen-soll">Schritt 4: Testen – auch das, was nicht gehen soll</h3>
<p>Bevor irgendein Skript läuft, teste ich beide Richtungen. Lesen muss gehen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">SSH</span><span class="o">=</span><span class="s2">&#34;ssh -i /root/.ssh/vps_backuppull_ed25519 -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts_vps -o StrictHostKeyChecking=yes&#34;</span>
</span></span><span class="line"><span class="cl">sudo rsync -e <span class="s2">&#34;</span><span class="nv">$SSH</span><span class="s2">&#34;</span> backuppull@example.com:./
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">drwxr-x---          4,096 2026/10/31 11:29:50 .
</span></span></code></pre></div><p>Schreiben muss scheitern:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo touch /tmp/rrsync-test
</span></span><span class="line"><span class="cl">sudo rsync -e <span class="s2">&#34;</span><span class="nv">$SSH</span><span class="s2">&#34;</span> /tmp/rrsync-test backuppull@example.com:./
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/usr/bin/rrsync error: sending to read-only server is not allowed
</span></span></code></pre></div><p>Genau so soll es sein. Ein Test, der nur prüft, ob etwas funktioniert, ist bei Security-Themen nur die halbe Miete.</p>
<h3 id="schritt-5-das-backup-skript-auf-dem-vps">Schritt 5: Das Backup-Skript auf dem VPS</h3>
<p>Das Skript packt alles in ein <strong>unkomprimiertes</strong> tar. Warum unkomprimiert, kommt gleich bei Borg. Wichtig ist der Trick mit der <code>.part</code>-Datei: Das Archiv bekommt erst seinen richtigen Namen, wenn es fertig ist. So kann der Abruf nie ein halb geschriebenes Archiv erwischen.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># /usr/local/sbin/vps-backup.sh</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">027</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DEST</span><span class="o">=</span>/var/backups/vps
</span></span><span class="line"><span class="cl"><span class="nv">KEEP_DAYS</span><span class="o">=</span><span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="nv">NAME</span><span class="o">=</span><span class="s2">&#34;vps-</span><span class="k">$(</span>date +%Y-%m-%d_%H%M<span class="k">)</span><span class="s2">.tar&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PART</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$DEST</span><span class="s2">/.</span><span class="si">${</span><span class="nv">NAME</span><span class="si">}</span><span class="s2">.part&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">TMP</span><span class="o">=</span><span class="k">$(</span>mktemp -d<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;rm -rf &#34;$TMP&#34; &#34;$PART&#34;&#39;</span> EXIT
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">dpkg --get-selections &gt; <span class="s2">&#34;</span><span class="nv">$TMP</span><span class="s2">/dpkg-selections.txt&#34;</span>
</span></span><span class="line"><span class="cl">apt-mark showmanual    &gt; <span class="s2">&#34;</span><span class="nv">$TMP</span><span class="s2">/apt-manual.txt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">FILES</span><span class="o">=(</span>etc<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> u in thomas deploy logpull backuppull<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">h</span><span class="o">=</span><span class="k">$(</span>getent passwd <span class="s2">&#34;</span><span class="nv">$u</span><span class="s2">&#34;</span> <span class="p">|</span> cut -d: -f6<span class="k">)</span> <span class="o">||</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$h</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> -f <span class="s2">&#34;</span><span class="nv">$h</span><span class="s2">/.ssh/authorized_keys&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        <span class="nv">FILES</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">h</span><span class="p">#/</span><span class="si">}</span><span class="s2">/.ssh/authorized_keys&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">tar --create --file <span class="s2">&#34;</span><span class="nv">$PART</span><span class="s2">&#34;</span> -C / <span class="s2">&#34;</span><span class="si">${</span><span class="nv">FILES</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> -C <span class="s2">&#34;</span><span class="nv">$TMP</span><span class="s2">&#34;</span> dpkg-selections.txt apt-manual.txt
</span></span><span class="line"><span class="cl">chgrp backuppull <span class="s2">&#34;</span><span class="nv">$PART</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">0640</span> <span class="s2">&#34;</span><span class="nv">$PART</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">mv <span class="s2">&#34;</span><span class="nv">$PART</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$DEST</span><span class="s2">/</span><span class="nv">$NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">find <span class="s2">&#34;</span><span class="nv">$DEST</span><span class="s2">&#34;</span> -maxdepth <span class="m">1</span> -name <span class="s1">&#39;vps-*.tar&#39;</span> -mtime +<span class="k">$((</span>KEEP_DAYS <span class="o">-</span> <span class="m">1</span><span class="k">))</span> -delete
</span></span></code></pre></div><p>Die Home-Verzeichnisse hole ich per <code>getent</code> statt sie fest einzutragen. Ein Archiv ist bei mir rund 2,7 MB groß, auf dem VPS liegen immer die letzten drei Tage.</p>
<h3 id="schritt-6-systemd-timer-mit-härtung">Schritt 6: systemd-Timer mit Härtung</h3>
<p>Statt Cron nutze ich einen systemd-Timer – und gebe dem Dienst gleich ein paar Fesseln mit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/vps-backup.service</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">VPS-Konfigurationsbackup nach /var/backups/vps</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/sbin/vps-backup.sh</span>
</span></span><span class="line"><span class="cl"><span class="na">Nice</span><span class="o">=</span><span class="s">10</span>
</span></span><span class="line"><span class="cl"><span class="na">IOSchedulingClass</span><span class="o">=</span><span class="s">idle</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectSystem</span><span class="o">=</span><span class="s">strict</span>
</span></span><span class="line"><span class="cl"><span class="na">ReadWritePaths</span><span class="o">=</span><span class="s">/var/backups/vps</span>
</span></span><span class="line"><span class="cl"><span class="na">ProtectHome</span><span class="o">=</span><span class="s">read-only</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateTmp</span><span class="o">=</span><span class="s">yes</span>
</span></span><span class="line"><span class="cl"><span class="na">NoNewPrivileges</span><span class="o">=</span><span class="s">yes</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/vps-backup.timer</span>
</span></span><span class="line"><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Täglicher VPS-Konfigurationsbackup um 02:30</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Timer]</span>
</span></span><span class="line"><span class="cl"><span class="na">OnCalendar</span><span class="o">=</span><span class="s">*-*-* 02:30:00 Europe/Berlin</span>
</span></span><span class="line"><span class="cl"><span class="na">RandomizedDelaySec</span><span class="o">=</span><span class="s">5min</span>
</span></span><span class="line"><span class="cl"><span class="na">Persistent</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Install]</span>
</span></span><span class="line"><span class="cl"><span class="na">WantedBy</span><span class="o">=</span><span class="s">timers.target</span>
</span></span></code></pre></div><ul>
<li><code>ProtectSystem=strict</code> macht das komplette Dateisystem für den Dienst schreibgeschützt – bis auf das, was in <code>ReadWritePaths</code> steht.</li>
<li><code>ProtectHome=read-only</code> reicht, weil das Skript die <code>authorized_keys</code> nur lesen muss.</li>
<li>Die Zeitzone direkt in <code>OnCalendar</code> sorgt dafür, dass der Job sicher vor dem Abruf um 03:00 läuft – auch wenn der Server mal auf UTC stehen sollte.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemd-analyze verify /etc/systemd/system/vps-backup.service /etc/systemd/system/vps-backup.timer
</span></span><span class="line"><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="cl">sudo systemctl <span class="nb">enable</span> --now vps-backup.timer
</span></span><span class="line"><span class="cl">sudo systemctl start vps-backup.service
</span></span></code></pre></div><p>Der Testlauf über systemd ist wichtig: Erst da merkt man, ob die Härtung dem Skript etwas wegnimmt, das es braucht.</p>
<h3 id="schritt-7-borg-im-homelab">Schritt 7: Borg im Homelab</h3>
<p><strong>BorgBackup</strong> ist ein Backup-Programm, das Daten in kleine Blöcke zerlegt, doppelte Blöcke nur einmal speichert (<strong>Deduplizierung</strong>), komprimiert und verschlüsselt. Ich nutze es nur auf der Homelab-Seite.</p>
<p>Die Passphrase liegt für die nächtlichen Läufe in einer Datei, die nur root lesen kann. Ganz wichtig: Sie wird nur erzeugt, wenn es die Datei noch nicht gibt. Überschreibt man sie nach dem Anlegen des Repos versehentlich, ist das Backup verschlüsselt – und zwar für immer.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo install -d -m <span class="m">0700</span> /root/.config/borg
</span></span><span class="line"><span class="cl">sudo sh -c <span class="s1">&#39;umask 077; f=/root/.config/borg/vps.passphrase; [ -s &#34;$f&#34; ] || head -c 32 /dev/urandom | base64 &gt; &#34;$f&#34;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo -H env <span class="nv">BORG_PASSCOMMAND</span><span class="o">=</span><span class="s2">&#34;cat /root/.config/borg/vps.passphrase&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  borg init --encryption<span class="o">=</span>repokey-blake2 /opt/vps-backup/borg
</span></span></code></pre></div><p>Bei <code>repokey</code> liegt der (verschlüsselte) Schlüssel im Repository selbst, die Passphrase entsperrt ihn. Passphrase <strong>und</strong> einen Key-Export habe ich zusätzlich in meinen Passwortmanager gelegt:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -H borg key <span class="nb">export</span> /opt/vps-backup/borg
</span></span></code></pre></div><p>Den brauche ich nur, wenn Homelab-VM <strong>und</strong> Proxmox Backup Server gleichzeitig weg sind. Aber genau für diesen Fall macht man Backups.</p>
<h3 id="schritt-8-abholen-und-einlagern">Schritt 8: Abholen und einlagern</h3>
<p>Das Skript auf der Homelab-VM holt die Archive per rsync ab, legt jedes neue tar als eigenes Borg-Archiv ab und räumt danach auf:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># /usr/local/sbin/vps-backup-pull.sh</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">077</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HOME</span><span class="o">=</span>/root
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">BASE</span><span class="o">=</span>/opt/vps-backup
</span></span><span class="line"><span class="cl"><span class="nv">INCOMING</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BASE</span><span class="s2">/incoming&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">DONE_LIST</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BASE</span><span class="s2">/archived.list&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">BORG_REPO</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$BASE</span><span class="s2">/borg&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">BORG_PASSCOMMAND</span><span class="o">=</span><span class="s2">&#34;cat /root/.config/borg/vps.passphrase&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SSH</span><span class="o">=</span><span class="s2">&#34;ssh -i /root/.ssh/vps_backuppull_ed25519 -o IdentitiesOnly=yes -o UserKnownHostsFile=/root/.ssh/known_hosts_vps -o StrictHostKeyChecking=yes -o BatchMode=yes -o ConnectTimeout=30&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> 9&gt;<span class="s2">&#34;</span><span class="nv">$BASE</span><span class="s2">/.lock&#34;</span>
</span></span><span class="line"><span class="cl">flock -n <span class="m">9</span> <span class="o">||</span> <span class="o">{</span> <span class="nb">echo</span> <span class="s2">&#34;Läuft bereits, Abbruch.&#34;</span> &gt;<span class="p">&amp;</span>2<span class="p">;</span> <span class="nb">exit</span> 1<span class="p">;</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">touch <span class="s2">&#34;</span><span class="nv">$DONE_LIST</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Abholen: incoming spiegelt die letzten 3 Tage vom VPS</span>
</span></span><span class="line"><span class="cl">rsync -rt --delete --exclude<span class="o">=</span><span class="s1">&#39;.*&#39;</span> -e <span class="s2">&#34;</span><span class="nv">$SSH</span><span class="s2">&#34;</span> backuppull@example.com:./ <span class="s2">&#34;</span><span class="nv">$INCOMING</span><span class="s2">/&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Neue Tar-Dateien in Borg übernehmen</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$INCOMING</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">shopt</span> -s nullglob
</span></span><span class="line"><span class="cl"><span class="k">for</span> f in vps-*.tar<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">f</span><span class="p">%.tar</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    grep -qxF <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$DONE_LIST</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
</span></span><span class="line"><span class="cl">    borg create --stats --compression zstd,6 --timestamp <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span> <span class="s2">&#34;::</span><span class="nv">$name</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$f</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$name</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$DONE_LIST</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Vorhaltung: 30 Tage</span>
</span></span><span class="line"><span class="cl">borg prune --list --keep-daily <span class="m">30</span> --glob-archives <span class="s1">&#39;vps-*&#39;</span>
</span></span><span class="line"><span class="cl">borg compact
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Plausibilität: jüngstes Tar darf nicht älter als 26 Stunden sein</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! find <span class="s2">&#34;</span><span class="nv">$INCOMING</span><span class="s2">&#34;</span> -maxdepth <span class="m">1</span> -name <span class="s1">&#39;vps-*.tar&#39;</span> -mmin -1560 <span class="p">|</span> grep -q .<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;FEHLER: kein VPS-Backup jünger als 26 Stunden&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>Ein paar Details, die mir wichtig waren:</p>
<ul>
<li><code>--exclude='.*'</code> sorgt dafür, dass <code>.part</code>-Reste nie im Homelab landen.</li>
<li><code>--timestamp &quot;$f&quot;</code> setzt als Archivzeit die Erstellungszeit auf dem VPS, nicht die Uhrzeit des Abrufs.</li>
<li>Die <code>archived.list</code> merkt sich, was schon in Borg liegt. Ohne sie würde <code>prune</code> ein Archiv löschen, das am nächsten Tag noch im Spiegel liegt – und das Skript würde es fröhlich wieder anlegen.</li>
<li><code>export HOME=/root</code> steht drin, damit Borg unter systemd denselben Cache findet wie bei meinen Aufrufen von Hand.</li>
<li>Der Plausibilitätscheck am Ende lässt den Dienst fehlschlagen, wenn der VPS nichts Neues geliefert hat. Daran hänge ich später eine Benachrichtigung.</li>
</ul>
<p>Dazu kommen ein Service und ein Timer nach demselben Muster wie auf dem VPS – diesmal um 03:00 und mit <code>ReadWritePaths=/opt/vps-backup /root/.cache/borg /root/.config/borg</code>, weil Borg in seinen Cache und sein Security-Verzeichnis schreiben muss.</p>
<h3 id="schritt-9-der-restore-test">Schritt 9: Der Restore-Test</h3>
<p>Ein Backup, das man nie zurückgespielt hat, ist eine Hoffnung, kein Backup. Also hole ich das tar aus Borg zurück – ohne es auf die Platte zu schreiben – und vergleiche die Prüfsumme mit dem Original:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -H env <span class="nv">BORG_PASSCOMMAND</span><span class="o">=</span><span class="s2">&#34;cat /root/.config/borg/vps.passphrase&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  borg extract --stdout /opt/vps-backup/borg::vps-2026-10-31_1134 vps-2026-10-31_1134.tar <span class="p">|</span> sha256sum
</span></span><span class="line"><span class="cl">sudo sha256sum /opt/vps-backup/incoming/vps-2026-10-31_1134.tar
</span></span></code></pre></div><p>Beide Werte identisch. Dazu noch einmal alles entschlüsseln und prüfen lassen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -H env <span class="nv">BORG_PASSCOMMAND</span><span class="o">=</span><span class="s2">&#34;cat /root/.config/borg/vps.passphrase&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  borg check --verify-data /opt/vps-backup/borg <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;CHECK OK&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">CHECK OK
</span></span></code></pre></div><h2 id="stolperfallen--learnings">Stolperfallen &amp; Learnings</h2>
<p><strong>Borg auf dem VPS? Lieber nicht.</strong> Meine erste Idee war, Borg direkt auf dem VPS laufen zu lassen. Borg ist aber auf Push ausgelegt: Der Client verbindet sich zum Repository. Damit wäre ich wieder bei der Tür ins Homelab. Es gibt zwar einen dokumentierten Pull-Modus über einen SSH-Reverse-Tunnel, aber für ein paar Megabyte <code>/etc</code> war mir das zu viel Bastelei. Die Aufteilung „tar auf dem VPS, Borg im Homelab&quot; ist einfacher und behält das Pull-Prinzip.</p>
<p><strong>Die Deduplizierung greift kaum.</strong> Ich hatte erwartet, dass das zweite Archiv am selben Tag praktisch nichts kostet – der Inhalt war ja derselbe. Stattdessen: wieder rund 450 kB. Der Grund: Borg zerlegt Daten standardmäßig in Blöcke von etwa 2 MiB. Ein 2,7-MB-tar besteht damit aus gerade mal vier Blöcken, und ein einziges geändertes Byte (etwa ein Zeitstempel im tar-Header) macht einen ganzen Block neu. Bei 30 Tagen sind das rund 13 MB – kein Drama. Wer es sauberer will, kann mit <code>--chunker-params</code> kleinere Blöcke einstellen.</p>
<p><strong><code>prune</code> hat nicht gelöscht, was ich erwartet hatte.</strong> Zwei Archive am selben Tag, <code>--keep-daily 30</code> – ich dachte, das ältere fliegt raus. Borg 1.4 behält aber zusätzlich das älteste Archiv (<code>daily[oldest]</code>), solange es weniger Tage gibt, als man behalten will. Ist so gewollt und erledigt sich nach 30 Tagen von selbst.</p>
<p><strong><code>os-release</code> ist leer?</strong> Beim Restore-Test wollte ich mir <code>etc/os-release</code> aus dem wiederhergestellten tar anzeigen lassen – und bekam gar nichts. Kein Fehler im Backup: Unter Debian ist <code>/etc/os-release</code> nur ein Symlink, und im tar steckt deshalb auch nur der Link. Mit <code>etc/debian_version</code> kam dann brav <code>13.7</code> zurück.</p>
<p><strong>Logs sehen nur Auserwählte.</strong> <code>journalctl -u …</code> zeigte als normaler Benutzer schlicht „No entries&quot;. Ohne Mitgliedschaft in der Gruppe <code>systemd-journal</code> (oder <code>sudo</code>) sieht man die System-Logs nicht. Klingt banal, hat mich aber kurz irritiert.</p>
<h2 id="fazit">Fazit</h2>
<p>Der VPS sichert sich jetzt jede Nacht selbst, das Homelab holt das Ergebnis ab und legt es verschlüsselt in Borg – und über den Proxmox Backup Server landet das Ganze noch ein zweites Mal woanders. Der VPS hat dabei keinen einzigen Zugang ins Homelab, und der Abruf-Key kann nichts außer lesen. Das fühlt sich deutlich besser an als „irgendwann mach ich mal ein Backup&quot;.</p>
<h2 id="was-als-nächstes-kommt">Was als Nächstes kommt</h2>
<ul>
<li><strong>Benachrichtigung bei Fehlern:</strong> Beide Dienste beenden sich mit einem Fehlercode, wenn etwas schiefgeht. Das soll mir künftig jemand sagen, statt dass ich es zufällig bemerke.</li>
<li><strong>Der richtige Restore-Test:</strong> Eine Prüfsumme ist gut, ein frischer Server ist besser. Ich will das Archiv auf einer Test-VM auspacken, die Pakete aus der Paketliste nachinstallieren und schauen, ob danach wirklich alles wieder so dasteht wie vorher.</li>
</ul>
]]></content:encoded></item></channel></rss>