<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Security on Schallberts Blog</title><link>https://blog.schallbert.de/tags/security/</link><description>Recent content in Security on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.schallbert.de/tags/security/index.xml" rel="self" type="application/rss+xml"/><item><title>Scheitern von SSH-Verbindungen</title><link>https://blog.schallbert.de/ssh-typical-issues/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/ssh-typical-issues/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-ssh-errors-thumb.avif"&#10; class="post-cover"&#10; alt="Image: A list of different SSH error messages. Some very common, but a few have rare causes."&#10; title="Scheitern von SSH-Verbindungen" /&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-ssh-icon.svg" alt="Image: terminal logo image from Aegis simple image pack, framing the topic of this post." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;In diesem kurzen Artikel geht es um Fehler beim Aufbau einer SSH-Verbindung. Egal ob andere Rechner, Server oder Dienste wie Versionsverwaltung - die Fehlermeldungen und zugehörige Lösungen sind meist gleich. Weiterlesen lohnt sich allerdings nur, wenn gängige Hilfeseiten wie &lt;a href="https://docs.github.com/en/authentication/troubleshooting-ssh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Githubs troubleshooting-ssh&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nicht die erwünschte Lösung brachte.&lt;/p&gt;&#10;&lt;h2 id="problem-no-route-to-host"&gt;Problem: &amp;ldquo;No Route To Host&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Die Konsolenausgabe zeigt (im lokalen Netz):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh 192.168.0.27&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host 192.168.0.27 port 22: No route to host&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oder (in einem entfernten Netz)&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh 1.2.3.4&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host 1.2.3.4 port 22: Network is unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ursache ist sehr wahrscheinlich eine falsch eingegebene IP oder eine fehlgeschlagene DNS-Namensauflösung. Im lokalen Netz ist ein realistischer Grund, dass der Teilnehmer abgeschaltet ist und der Router ihn daher nicht erreichen kann. Abhilfe schafft meist eine Korrektur der Zieladresse, Einschalten des Ziels oder Update der Routing-Tabelle.&lt;/p&gt;&#10;&lt;h2 id="problem-connection-refused"&gt;Problem: Connection Refused&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git push gitea&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh: connect to host git.schallbert.de port 22: Connection refused&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fatal: Could not read from remote repository.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier kann nicht mal eine Verbindung zum Ziel hergestellt werden. Man ist also &amp;ldquo;vor&amp;rdquo; der Authentisierung schon gescheitert. Meiner Erfahrung nach werden solche Fehler aus drei Gründen erzeugt:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Host ist noch nicht bereit. Er bootet beispielsweise gerade (Netzwerkkarte ist an) aber der SSH-Dienst ist noch nicht hochgefahren. Hier hilft meist: abwarten.&lt;/li&gt;&#10;&lt;li&gt;Eine Firewall, ein Rate Limiter, oder Dienste wie &lt;em&gt;fail2ban&lt;/em&gt; weisen die Verbindung ab (&lt;code&gt;REJECT&lt;/code&gt;). Die Gründe hierfür sind meist zu viele erfolglose Anmeldeversuche oder eine fehlgeleitete Bot-Erkennung. In seltenen Fällen kann es aber auch sein, dass die eigene IP-Adresse auf einer &amp;ldquo;deny-list&amp;rdquo; auftaucht, die von den Zielen eingelesen wurde. Auch hier empfiehlt sich: warten.&lt;/li&gt;&#10;&lt;li&gt;Der angesprochene Dienst erwartet einen anderen Port als den SSH-Standard &lt;code&gt;Port 22&lt;/code&gt;. Somit stimmen User/Ziel-Adresse/Port nicht überein.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Das Port-Problem lässt sich leicht lösen.&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-06-12-gitea-server-configuration-ssh-port.avif" alt="Image: Gitea showing SSH port IDs under /admin/config/Server_Configuration/SSH_Configuration, Port:222, Listen Port: 22" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Herausfinden des &amp;ldquo;Listen Port&amp;rdquo; auf dem Host (Server Configuration / SSH). Auf dem Bild ist dargestellt, wie die Einstellung auf meiner Gitea-Instanz aussieht.&#10;Alternativ genügt meist ein Blick in die Konfigurationsdatei des entsprechenden Services.&lt;/li&gt;&#10;&lt;li&gt;Die SSH-Konfiguration unter &lt;code&gt;~./ssh/config&lt;/code&gt; mit einer konkreten Port-Nummer versehen. Beispiel:&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Host gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname git.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Port &lt;span style="color:#ae81ff"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="problem-too-many-authentication-failures"&gt;Problem: Too Many Authentication Failures&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git pull&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Received disconnect from &amp;lt;ipAddress&amp;gt; port &amp;lt;portId&amp;gt;: Too many authentication failures&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wir haben nun also eine Verbindung zum Server 😁, aber der mag uns nicht reinlassen 😨. Immerhin wird mitgeteilt, warum: Er meint, wir hätten zu viele Schlüssel an seiner Haustür ausprobiert.&lt;/p&gt;&#10;&lt;h3 id="server-anmeldelimit-prüfen"&gt;Server-Anmeldelimit prüfen&lt;/h3&gt;&#10;&lt;p&gt;Dieser Fehler tritt auf, wenn das Server-Limit der Anmeldeversuche gerissen wird. Der Wert beträgt beim Service &lt;code&gt;sshd&lt;/code&gt; beispielsweise standardmäßig &lt;a href="https://www.baeldung.com/linux/too-many-authentication-failures-error" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MaxAuthTries = 6&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Hat man viele Schlüssel für mehrere Verbindungen abgespeichert oder verwendet wie ich neuerdings &lt;a href="https://blog.schallbert.de/hardware-security-keys-yubikey/"&gt;Hardware Security Keys&lt;/a&gt; und legt Zweitschlüssel an (also &lt;code&gt;ssh-keys.count*2&lt;/code&gt;), wird dieser Wert schnell überschritten.&lt;/p&gt;&#10;&lt;p&gt;Fies bei diesem Fehler ist außerdem, dass er nur bei bestimmten Services &amp;ldquo;unten&amp;rdquo; in des Agents Schlüsselbund auftritt: unter den ersten sechs dürfen ja fünf Schlüssel falsch sein. Zudem schützt einen nicht, wenn man die Schlüssel in der &lt;code&gt;~./ssh/config&lt;/code&gt; korrekt hinterlegt hat. Der Agent nimmt stets alle Schlüssel zum Probieren mit, wenn man es ihm nicht explizit verbietet.&lt;/p&gt;&#10;&lt;p&gt;Das Verbot erteilen wir per &lt;code&gt;IdentitiesOnly yes&lt;/code&gt;. Hiermit wird dem Agent klar gesagt, dass er nur die für diesen Host explizit angegebenen Identities (Also Schlüsseldateien oder Nutzername/Passwort) verwenden soll.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname git.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Port &lt;span style="color:#ae81ff"&gt;222&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User &amp;lt;user&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreferredAuthentications publickey&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile &amp;lt;path_to_private_key1&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Identityfile &amp;lt;path_to_private_key2&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentitiesOnly yes&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="git-config-überprüfen"&gt;Git-Config überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Diesen Fehler bekommt man auch, wenn die Git-Konfiguration des Repository unter &lt;code&gt;&amp;lt;reponame&amp;gt;/.git/config&lt;/code&gt; nicht mit dem in der SSH-Konfiguration &lt;code&gt;~/.ssh/config&lt;/code&gt; hinterlegten Datensatz für Server und Repository übereinstimmt. Ändert man die SSH-Konfiguration, so sind alle auf diesem Server bereitgehaltenen Repo-Konfigurationen ebenfalls zu ändern.&lt;/p&gt;&#10;&lt;p&gt;Beispiel:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# &amp;lt;reponame&amp;gt;/.git/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;remote &lt;span style="color:#e6db74"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ssh://git@git.schallbert.de:222/schallbert/&amp;lt;reponame&amp;gt;.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; +refs/heads/*:refs/remotes/origin/*&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;branch &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;remote &lt;span style="color:#f92672"&gt;=&lt;/span&gt; origin&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;merge &lt;span style="color:#f92672"&gt;=&lt;/span&gt; refs/heads/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;vscode-merge-base &lt;span style="color:#f92672"&gt;=&lt;/span&gt; origin/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In diesem Falle wird die &lt;code&gt;gitserver&lt;/code&gt;-Konfiguration gar nicht verwendet, sondern sich direkt einwählt. Dies führt dazu, dass der Agent kein &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; Flag mitgeliefert bekommt und er Schlüssel durchprobiert, bis der Server abwinkt. Lösung:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;remote &lt;span style="color:#e6db74"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; url &lt;span style="color:#f92672"&gt;=&lt;/span&gt; git@gitserver:schallbert/&amp;lt;reponame&amp;gt;.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;fetch &lt;span style="color:#f92672"&gt;=&lt;/span&gt; +refs/heads/*:refs/remotes/origin/*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="problem-permission-denied-publickeypassword"&gt;Problem: Permission Denied (publickey,password)&lt;/h2&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh -T git@gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git@git.schallbert.de: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey,password&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dieses Problem kann leider viele verschiedene Ursachen haben. Ein paar räumt Githubs &lt;a href="https://docs.github.com/en/authentication/troubleshooting-ssh/error-permission-denied-publickey" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bereits aus dem Weg, darunter:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; verwendet&lt;/li&gt;&#10;&lt;li&gt;Falscher Server&lt;/li&gt;&#10;&lt;li&gt;nicht den &lt;code&gt;git&lt;/code&gt;-user verwendet&lt;/li&gt;&#10;&lt;li&gt;Falschen Schlüssel verwendet (client)&lt;/li&gt;&#10;&lt;li&gt;Schlüssel nicht hinterlegt (server)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Die Ausgabe bei falschem Username sieht so aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ ssh -T mit@gitserver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mit@git.schallbert.de: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eine weitere Ursache kann eine fehlende Abstimmung zwischen der Konfigurationsdatei &lt;code&gt;~./ssh/config&lt;/code&gt; und dem im &lt;em&gt;git client&lt;/em&gt; hinterlegten Remote-Adresse liegen. Sind diese nicht deckungsgleich, wird eine eventuell bestehende Konfiguration gar nicht erst benutzt.&lt;/p&gt;&#10;&lt;p&gt;Eine Überprüfung kann man mit &lt;code&gt;git remote -v&lt;/code&gt; durchführen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote -v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;ssh://git@git.schallbert.de:222/schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;fetch&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;ssh://git@git.schallbert.de:222/schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;push&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Korrekt wäre mit der verbesserten Konfigurationsdatei (Ports eindeutig gesetzt, Identity Files definiert und auf die Angegebenen beschränkt):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Update link to server&amp;#39;s git repository, use host alias from config file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote update origin git@gitserver:schallbert/blog.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Now check if the update was effective&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;article&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git remote -v&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;git@gitserver:schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;fetch&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;origin&#9;git@gitserver:schallbert/blog.git &lt;span style="color:#f92672"&gt;(&lt;/span&gt;push&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="problem-agent-refused-operation"&gt;Problem: Agent Refused Operation&lt;/h3&gt;&#10;&lt;p&gt;Konsolenausgabe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ blog git:&lt;span style="color:#f92672"&gt;(&lt;/span&gt;local-setup&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ✗ git push --set-upstream origin local-setup &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sign_and_send_pubkey: signing failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ED25519-SK &amp;lt;path-to-private-key&amp;gt; from agent: agent refused operation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diesen Fehler sehe ich erst, seitdem ich &lt;a href="https://blog.schallbert.de/hardware-security-key-add-ssh/"&gt;Hardware Security Keys für ssh verwende&lt;/a&gt;. Aus meiner Sicht kann er zwei Ursachen haben. Beide sind zum Glück leicht zu beheben:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Der Agent kennt den Schlüssel nicht. Der Befehl &lt;code&gt;ssh-add&lt;/code&gt; löst das Problem. Prüfung per &lt;code&gt;ssh-add -l&lt;/code&gt; | grep &amp;ldquo;&amp;lt;your-key&amp;rsquo;s-comment&amp;gt;&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Der angeforderte Schlüssel ist nicht da. Kann bei HSK und HSM passieren, falls sie nicht eingesteckt sind, nicht richtig auf dem NFC-Lesegerät platziert sind oder das HSM im Netzwerk nicht verfügbar ist. Oder wenn manuell im Verzeichnis &lt;code&gt;~./ssh/config&lt;/code&gt; gearbeitet wurde und der Schlüssel nun umbenannt oder gelöscht ist.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Hardware Security Key: SSH einrichten</title><link>https://blog.schallbert.de/hardware-security-key-add-ssh/</link><pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hardware-security-key-add-ssh/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-randomart_thumb.avif"&#10; class="post-cover"&#10; alt="Image: An SSH public key randomart image as visual fingerprint for humans to quickly see if public keys match"&#10; title="Hardware Security Key: SSH einrichten" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;In meinem Post über &lt;a href="https://blog.schallbert.de/hardware-security-keys-yubikey/"&gt;Hardware Security Keys&lt;/a&gt; schrieb ich, dass ich möglichst viele meiner Verbindungen passwortlos mit meinem neuen HSK absichern möchte. Ich verwende &lt;em&gt;SSH&lt;/em&gt;-Verbindungen für verschiedene Dienste und Zwecke: Ich greife auf meinen &lt;a href="https://en.wikipedia.org/wiki/Virtual_private_server" target="_blank" rel="noopener noreferrer" class="external-link"&gt;VPS&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zu, benutze Dienste wie Github, Gitea sowie meine eigene &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/"&gt;Gitea-Instanz&lt;/a&gt; und lasse die Erzeugung von Backups im lokalen Netz über &lt;a href="https://linuxize.com/post/how-to-use-scp-command-to-securely-transfer-files/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;scp&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; laufen, welches das SSH-Protokoll verwendet.&lt;/p&gt;&#10;&lt;h2 id="ssh-zugriff-für-hardware-security-keys-einrichten"&gt;SSH-Zugriff für Hardware Security Keys einrichten&lt;/h2&gt;&#10;&lt;p&gt;HSKs wie mein Yubikey können private Schlüssel für SecureShell-Zugriffe abspeichern. Somit muss man sie nicht mehr lokal auf jedem Endgerät ablegen (das bleibt aber konfigurierbar, siehe unten). Zudem sind sie gegenüber der Verwahrung auf einem Rechner besser gegen unbefugte Zugriffe geschützt.&lt;/p&gt;&#10;&lt;p&gt;Also probiere ich aus, meinen Github-SSH-Zugriff auf HSK umzustellen und folge &lt;a href="https://www.yubico.com/blog/github-now-supports-ssh-security-keys/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubicos Anleitung für passwordlose Authentisierung für Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Das Verfahren zur Erzeugung von SSH-Schlüsseln nach FIDO unterscheidet sich für verschiedene Schlüsselhersteller nicht. Ich verlinke ebenfalls die &lt;a href="https://docs.nitrokey.com/nitrokeys/features/fido2/ssh" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey-Anleitung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als Referenz.&lt;/p&gt;&#10;&lt;h2 id="was-so-nicht-funktioniert"&gt;Was so nicht funktioniert&lt;/h2&gt;&#10;&lt;p&gt;Doch mit den Empfehlungen von Yubico selbst komme ich auf meinem Betriebssystem und der Standardinstallation von &lt;code&gt;openssh &lt;/code&gt;nicht zum Ziel. Das aus der Anleitung zusammengebaute Kommando lautet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-keygen -t ed25519-sk -O resident -O verify-required -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -O user&lt;span style="color:#f92672"&gt;=&lt;/span&gt;schallbert -C &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach Erstellung muss man den öffentliche Schlüssel (Dateiendung &lt;code&gt;.pub&lt;/code&gt;) in der Web-Anwendung des jeweiligen Dienstes im Menüpunkt SSH-Zugang hinterlegen.&lt;/p&gt;&#10;&lt;h3 id="bedeutung-des-kommandos-und-der-wichtigsten-optionen"&gt;Bedeutung des Kommandos und der wichtigsten Optionen&lt;/h3&gt;&#10;&lt;p&gt;Dieser Befehl&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; sagt der Anwendung &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ssh-keygen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, dass sie mir einen Schlüssel des Typs &lt;code&gt;-t ed25519-sk&lt;/code&gt; erzeugen soll. Die Endung &lt;code&gt;-sk&lt;/code&gt; sagt aus, dass der Schlüssel auf einem Security Key - in meinem Fall dem Yubikey - gespeichert werden soll.&lt;/p&gt;&#10;&lt;p&gt;Um das Flag &lt;code&gt;-O resident&lt;/code&gt; zu verstehen muss man wissen, dass FIDO Authenticator-Schlüssel wie mit &lt;code&gt;-sk&lt;/code&gt; erzeugt aus zwei Teilen bestehen: Ein &lt;code&gt;key handle&lt;/code&gt; Teil, welcher auf der Festplatte des Rechners verbleibt und der private Schlüssel selbst, welcher &lt;a href="https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html#FIDO_AUTHENTICATOR" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nur auf dem HSK existiert&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Nur zusammen können sie Authentisierungsanfragen korrekt beantworten. Das Flag selbst weist jedoch an, beide Schlüsselteile auf den Yubikey zu schreiben. Das erleichtert die Verwendung mehrerer Geräte mit demselben Schlüssel: Der &lt;code&gt;key handle&lt;/code&gt;-Teil kann vom Yubikey ausgelesen und auf anderen Endgeräten abgelegt werden. Auf der anderen Seite jedoch genügt nun das Verlorengehen des Yubikey allein, um möglichen Angreifern SSH-Zugriff zu geben.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-f filename&lt;/code&gt; legt Namen für die Schlüsseldateien fest. Macht besonders dann Sinn, wenn man mehrere SSH-Verbindungen oder HSM verwalten muss. Ich habe mir eine Konvention erstellt, um nicht durcheinander zu kommen. Diese orientiert sich stark an der Schreibweise des Schlüssel-Auslesens von einem HSK (&lt;code&gt;ssh-keygen -K&lt;/code&gt;).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-f &amp;lt;keyType&amp;gt;_&amp;lt;protocol&amp;gt;:&amp;lt;service&amp;gt;_&amp;lt;user&amp;gt;_&amp;lt;device&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als konkretes Beispiel würde mein Dateiname hier wie folgt lauten:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;⚠️ Obacht bei Nutzung von Windows: Dort ist der Doppelpunkt &lt;code&gt;:&lt;/code&gt; nicht als Zeichen in Dateinamen erlaubt. Er wird durch den Unterstrich &lt;code&gt;_&lt;/code&gt; ersetzt, was in der &lt;code&gt;config&lt;/code&gt; entsprechend abgebildet werden muss (&lt;code&gt;strg+h&lt;/code&gt; -&amp;gt; &lt;code&gt;:&lt;/code&gt; ersetzen mit &lt;code&gt;_&lt;/code&gt;)&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-C schallbert@github.com&lt;/code&gt; fügt der Schlüsseldatei selbst einen Kommentar hinzu, zum Beispiel eine Kontaktadresse. Somit bekommt der Schlüssel ein Label, was die Handhabung unter mehreren vereinfacht.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O application=ssh:github&lt;/code&gt; dient nur der sauberen Auflistung im Authenticator-Programm und sorgt dafür, dass ich Schlüssel nicht miteinander verwechseln kann. Ansonsten steht dort nämlich nur &lt;code&gt;ssh:keygen&lt;/code&gt; und ich kann nicht sehen, für welche Services ich Schlüssel hinterlegt habe. Außerdem hilfreich bei der Verwendung von &lt;code&gt;-resident&lt;/code&gt;, da bei der Extraktion der Schlüssel mit &lt;code&gt;ssh-keygen -K&lt;/code&gt; Unterscheidbarkeit gegeben bleibt.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O user=schallbert&lt;/code&gt; hat für mich keinen praktischen Nutzen, da SSH dieses Flag nicht interessiert und der Yubikey Authenticator meinen Nutzernamen anders als erwartet nicht anzeigt.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O no-touch-required&lt;/code&gt; vermeidet, dass der Hardware Security Key bei jeder Verwendung des Schlüssels berührt werden muss. Nützlich bei sehr häufiger Verwendung, allerdings Sicherheitsverlust durch Automatisierbarkeit.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;-O verify-required&lt;/code&gt; sagt aus, dass die PIN des HSM abzufragen ist, bevor der Schlüssel genutzt werden kann. Eigentlich als zusätzliche Sicherheit gedacht, bekomme ich damit in meiner Shell jedoch Probleme. Und zwar wird die PIN einmal abgefragt, erzeugt aber vom &lt;code&gt;ssh-agent&lt;/code&gt; nicht jedes mal eine Abfrage in meiner Konsole. Sobald ich in einer anderen Session versuche mich per SSH zu verbinden, erhalte ich eine Fehlermeldung &lt;code&gt;agent refused operation&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh -T git@github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sign_and_send_pubkey: signing failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; ED25519-SK &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt; from agent: agent refused operation&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@opnsense: Permission denied &lt;span style="color:#f92672"&gt;(&lt;/span&gt;publickey&lt;span style="color:#f92672"&gt;)&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="woran-hat-es-gelegen"&gt;Woran hat es gelegen?&lt;/h2&gt;&#10;&lt;p&gt;Nach langer, frustrierender Fehlersuche (Was mache ich falsch? Funktioniert es mit dem Zweitschlüssel? Warum geht es beim ersten mal, danach aber nicht mehr? Was ist, wenn ich die Schlüssel erneut erstelle? Was passiert an anderen Rechnern? Was meint &amp;ldquo;KI&amp;rdquo; dazu? - Letzteres kostete bestimmt eine Stunde und ließ mich komplett auf der Stelle treten) probiere ich es mit einem anderen Kommando. Das funktioniert auf Anhieb.&lt;/p&gt;&#10;&lt;h2 id="was-hingegen-funktioniert"&gt;Was hingegen funktioniert&lt;/h2&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-keygen -t ed25519-sk -O application&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh:github -C &lt;span style="color:#e6db74"&gt;&amp;#34;schallbert@github.com&amp;#34;&lt;/span&gt; -f ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Lasse ich &lt;code&gt;verify-required&lt;/code&gt; weg, kann ich mich erfolgreich anmelden und der SSH-agent läuft sauber durch.&lt;/p&gt;&#10;&lt;h3 id="was-verify-required-benötigt"&gt;Was &lt;code&gt;verify-required&lt;/code&gt; benötigt&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://superuser.com/questions/1804740/yubikey-ssh-authentication-fails-with-signing-failed-for-ecdsa-sk" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dieser Post im Forum superuser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; bringt für mein Fehlerbild Licht ins Dunkel. Die Terminal-Session mit der ich das SSH-Schlüsselpaar erstelle speichert die per &lt;code&gt;verify-required&lt;/code&gt; Flag erzwungene Pin-Eingabe zwischen. Daher funktioniert der Login erst einmal. Logge ich mich später oder in einem anderen Fenster ein, wie ich es oben getan habe um z.B. &lt;code&gt;git pull&lt;/code&gt; in einem meiner Repos zu prüfen, fragt der SSH-Agent meine PIN nicht ab.&lt;/p&gt;&#10;&lt;p&gt;Das liegt wohl daran, dass der SSH-Agent selbst keine Prompts an das anfragende Programm ausgeben kann. Abhilfe schafft ein Untermodul von Openssh: &lt;a href="https://packages.fedoraproject.org/pkgs/openssh/openssh-askpass/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;openssh-askpass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dieses kann man dem Agent übergeben, welcher dann die PIN-Abfrage über &lt;em&gt;askpass&lt;/em&gt; an den User übermittelt.&lt;/p&gt;&#10;&lt;h3 id="installation-von-ssh-askpass"&gt;Installation von ssh-askpass&lt;/h3&gt;&#10;&lt;p&gt;Mein Betriebssystem hatte &lt;em&gt;ssh-askpass&lt;/em&gt; nicht installiert, daher hole ich das hier nach. Es verwendet den Paketmanager &lt;code&gt;DNF&lt;/code&gt; &amp;ldquo;Dandified YUM&amp;rdquo; statt &lt;em&gt;apt&lt;/em&gt; oder anderer gängiger Manager. Dabei ist die Namensgebung meiner Meinung nach nicht 100% einheitlich, spreche ich den ssh-Programme doch im Terminal mit &lt;em&gt;ssh-agent&lt;/em&gt; an, muss jedoch &lt;em&gt;openssh-agent&lt;/em&gt; tippen für die Installation.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# verify-required needs openssh subpackage through Fedora&amp;#39;s package manager&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# sudo dnf install openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Installing:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Complete!&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun will ich dem Agent mitteilen, dass er &lt;em&gt;ssh-askpass&lt;/em&gt; zum Erfragen der PIN ansprechen kann. Dafür mus ich wissen, in welchem Pfad sich das Submodul befindet.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# which openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/which: no openssh-askpass &lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Komisch, aber ich hatte es doch installiert! Frage ich mal den Paketmanager:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# dnf repoquery --list openssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/libexec/openssh/ssh-askpass&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aha, openssh versteckt sich im Ordner &lt;code&gt;libexec&lt;/code&gt;. Na dann kann ich das Modul jetzt dem Agent bekanntgeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s; SSH_ASKPASS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/libexec/openssh/ssh-askpass&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Agent pid &lt;span style="color:#ae81ff"&gt;16747&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-ssh-askpass-prompt.avif" alt="Image: Testing the ssh connection to Github with ssh -T and getting prompted for HSK device pin" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich prüfe die Verbindung zu Github und bekomme wie gewünscht einen Dialog angezeigt, der meine PIN abfragt. Anschließend muss ich den HSK noch berühren und bekomme dann die Erfolgsmeldung. Damit das alles auch in einem neuen Terminal und nach Neustart des Rechners funktioniert, füge ich eine Referenz auf den Schlüssel in meine Konfigurationsdatei ein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ~/.ssh/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host github&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; User git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Hostname github.com&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PreferredAuthentications publickey&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IdentityFile ~/.ssh/ed25519-sk_ssh:github_schallbert_yubikey-main&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Um auf Nummer sicher zu gehen, den Agenten neustarten und neue Terminalsessions erstellen. Sollten Zugangsdaten im Cache gewesen sein, ist das hiermit zurückgesetzt. Anschließend erneut die SSH-Verbindung testen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schallbert@machine:~# eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -k&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; eval &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;ssh-agent -s&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="nervig-gnome-popup-allow-inhibiting-shortcuts"&gt;Nervig: Gnome-Popup &amp;ldquo;Allow inhibiting shortcuts&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-gnome-inhibit-shortcuts-prompt.avif" alt="Image: Gnome popup *Allow inhibiting shortcuts*" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Einziger Wehrmutstropfen ist, dass ich jetzt beim Verbinden mit SSH ein zusätzliches Popup bekomme. Das wird durch die grafische Nutzeroberfläche &lt;em&gt;gnome&lt;/em&gt; ausgelöst. Leider finde ich keine Option, die zugehörige Einstellung permanent speichern. Somit wird es bei jeder Verbindung mit &lt;code&gt;verify-required&lt;/code&gt; angezeigt. Bei Gnome wird ist das Problem bereits &lt;a href="https://gitlab.gnome.org/GNOME/gnome-connections/-/work_items/85" target="_blank" rel="noopener noreferrer" class="external-link"&gt;für andere Anwendungen bekannt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, anscheinend aber noch nicht abgestellt.&lt;/p&gt;&#10;&lt;h2 id="übertragung-auf-windows-systeme"&gt;Übertragung auf Windows-Systeme&lt;/h2&gt;&#10;&lt;p&gt;Ich habe meine Schlüssel ja mit der Option &lt;code&gt;-O resident&lt;/code&gt; erzeugt. Da liegt es nahe, die Extraktion des &lt;code&gt;key-handle&lt;/code&gt; mal auf einem komplett anderen System zu testen: &lt;em&gt;Windows&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ich öffne die Kommandozeile und tippe&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@windows-machine:~# ssh-keygen -K&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Enter PIN &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; authenticator:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You may need to touch your authenticator to authorize key download.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Provider &lt;span style="color:#e6db74"&gt;&amp;#34;internal&amp;#34;&lt;/span&gt; returned failure -1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unable to load resident keys: invalid format&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Invalid format? Im Netz steht öfters geschrieben, dass die Schlüssel manchmal am Ende noch ein &lt;a href="https://stackoverflow.com/questions/47859437/windows-10-openssh-key-invalid-format#68593742" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LineFeed-Zeichen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; benötigen um richtig erkannt zu werden. Ohne Download vom HSK kann ich sie aber nicht modifizieren. Also übertrage ich öffentlichen Schlüssel und key-handle manuell auf den Rechner und probiere testweise das &lt;code&gt;git pull&lt;/code&gt;-Kommando aus. Und siehe da: Es klappt!&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-windows-fido-key-ssh-pin-prompt.avif" alt="Image: FIDO ssh connection test on a Windows machine: OS prompts for PIN entry. It works!" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Möchte man also die Schlüssel mit verschiedenen Betriebssystemfamilien verwenden, nutzt die Option &lt;code&gt;-resident&lt;/code&gt; anscheinend nichts. Der Befehl &lt;code&gt;ssh-keygen -K&lt;/code&gt; ist nicht granular steuerbar: Er lädt alle auf dem HSK befindlichen resident keys herunter.&lt;/p&gt;&#10;&lt;h2 id="vps-vorsicht-bei-schlüssel-ersatz"&gt;VPS: Vorsicht bei Schlüssel-Ersatz&lt;/h2&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-05-03-hetzner-key-add-irrelevant-for-existing-servers.avif" alt="Image: Hetzner telling me that new SSH keys will not propagate to existing resources (VPS). Important to know!" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Noch ein Hinweis aus eigener Erfahrung: Hat man bereits SSH-Schlüssel hinterlegt bei einem Dienstanbieter, der selber Ressourcen bereitstellt (In meinem Fall einen Server als VPS) so werden neue SSH-Schlüssel meist nicht über das Web-Frontend an diese Ressource weitergereicht.&lt;/p&gt;&#10;&lt;p&gt;Die &amp;ldquo;alten&amp;rdquo; Schlüssel werden folglich weiterhin benötigt und die Neuen nicht verwendet, obwohl sie in der Web-App abgelegt sind!&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Die neuen Schlüssel müssen dem Ziel manuell bekanntgemacht werden.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Das führe ich der &lt;a href="https://community.hetzner.com/tutorials/howto-ssh-key/de#schritt-4---ssh-key-auf-server-hinterlegen" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung von meinem Hoster Hetzner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; folgend mit dem Befehl &lt;code&gt;ssh-copy-id&lt;/code&gt; aus.&lt;/p&gt;&#10;&lt;h3 id="neue-schlüssel-manuell-auf-vps-ablegen"&gt;Neue Schlüssel manuell auf VPS ablegen&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-copy-id -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dabei ist &lt;code&gt;server&lt;/code&gt; der SSH-Alias für meinen VPS. Ich verbinde mittels des bekannten, &amp;ldquo;alten&amp;rdquo; Schlüssels lasse den &amp;ldquo;Neuen&amp;rdquo; ablegen. Doch irgend etwas funktioniert nicht wie erwartet:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt;, to filter out any that are already installed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#9;&#9;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; you think this is a mistake, you may want to use -f option&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="schlüssel-erfolgreich-übertragen"&gt;Schlüssel erfolgreich übertragen?&lt;/h3&gt;&#10;&lt;p&gt;Ich logge mich also auf meinem Server ein und prüfe, ob der neue Schlüssel tatsächlich in der &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; Datei auftaucht. Tut er nicht. Also führe ich das obige Kommando erneut aus, diesmal mit aktivierter &lt;code&gt;-f (Force)&lt;/code&gt; Option. Und tatsächlich, die Übertragung klappt!&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# ssh-copy-id -f -i ./ed25519-sk_ssh:server_schallbert_yubikey-main.pub server &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Number of key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; added: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Now try logging into the machine, with: &lt;span style="color:#e6db74"&gt;&amp;#34;ssh -i &amp;lt;identityFile&amp;gt; &amp;#39;server&amp;#39;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;and check to make sure that only the key&lt;span style="color:#f92672"&gt;(&lt;/span&gt;s&lt;span style="color:#f92672"&gt;)&lt;/span&gt; you wanted were added.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Testen wie empfohlen nun mit dem neuen Schlüssel einloggen und&amp;hellip; Läuft! Erst jetzt können die alten Schlüssel sicher gelöscht werden: &lt;code&gt;shred -u &amp;lt;path/to/private/key&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;h3 id="auf-direktem-wege-ohne-ssh-copy-id"&gt;Auf direktem Wege ohne ssh-copy-id&lt;/h3&gt;&#10;&lt;p&gt;Will man den neuen Schlüssel ohne zusätzliche Werkzeuge bekannt machen, so kann man den Inhalt der öffentlichen Schlüsseldatei &lt;code&gt;&amp;lt;key-filename&amp;gt;.pub&lt;/code&gt; auch direkt selbst in eine neue Zeile von &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; schreiben. Dafür benötigt man lediglich eine offene SSH-Verbindung zum Ziel.&lt;/p&gt;&#10;&lt;h2 id="überlegungen-zu-komfort-und-bedienbarkeit"&gt;Überlegungen zu Komfort und Bedienbarkeit&lt;/h2&gt;&#10;&lt;p&gt;Wenn ich mir meine SSH-Verbindungen so ansehe, dann teilen sie sich grob in zwei Kategorien:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Admin-Zugänge&lt;/strong&gt; (VPS, NAS, Raspberry Pi) und Signaturen&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Protokoll-Zugänge&lt;/strong&gt; (Git etc.)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Erstere erfordern aus meiner Sicht ein höheres Sicherheitsniveau. Zudem nutze ich sie nicht so häufig wie Versionsverwaltungen. Daher empfehle ich für die erste Kategorie &lt;code&gt;verify-required&lt;/code&gt; und würde dies bei der Zweiten weglassen. Mit der Option &lt;code&gt;no-touch-required&lt;/code&gt; würde ich ebenso verfahren: falls alle paar Minuten ein Commit hochgeladen werden soll, werden ständige Berührungen des Schlüssels schnell irritierend.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Alle Optionen bekommt man bei Aufruf des Handbuchs per &lt;code&gt;man ssh-keygen&lt;/code&gt; angezeigt.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Hardware Security Keys</title><link>https://blog.schallbert.de/hardware-security-keys-yubikey/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/hardware-security-keys-yubikey/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubikey_thumb.avif"&#10; class="post-cover"&#10; alt="Image: Browser (Firefox) login process for passkey usage: PIN, then touch"&#10; title="Hardware Security Keys" /&gt;&#10;&lt;h2 id="begriffsabgrenzung-tpm-und-hsk"&gt;Begriffsabgrenzung: TPM und HSK&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Akronym&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Bezeichnung&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th style="text-align: center"&gt;Kurzbeschreibung&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;TPM&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Trusted Platform Module&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Fest verbaut. Schützt das Gerät selbst&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;HSK&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Hardware Security Key&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td style="text-align: center"&gt;Mobil. Schützt Konten, Zertifikate, private Schlüssel&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Das &lt;a href="https://de.wikipedia.org/wiki/Trusted_Platform_Module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Trusted Platform Module&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist ein Sicherheitschip, der kryptografische Schlüssel sicher speichert und Funktionen wie Secure Boot, BitLocker-Verschlüsselung und Geräteidentität bereitstellt. Er ist fest auf der Hauptplatine eines Computers oder Smartphones verlötet.&lt;/p&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-this-is-a-yubikey.avif" alt="Image: A Hardware Security Key with USB-C connector called YubiKey5 NFC." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ein &lt;a href="https://en.wikipedia.org/wiki/Hardware_security_module" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hardware Security Key &lt;em&gt;HSK&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Hardware-Sicherheitsschlüssel hingegen ist meist mobil und kann mit verschiedenen Geräten verbunden werden. Es kann Schlüssel nicht nur speichern, sondern meist auch kryptografische Operationen (Schlüsselgenerierung, Signatur, Verschlüsselung) ausführen. HSKs sind hochsicher und manipulationsgeschützt konstruiert. Sie können extern zertifiziert bzw. auditiert sein, sodass auch hochsensible Vorgänge wie Zahlungstransaktionen ausgeführt werden können.&lt;/p&gt;&#10;&lt;h2 id="was-bisher-geschah"&gt;Was bisher geschah&lt;/h2&gt;&#10;&lt;p&gt;Ich hörte einen Podcast vom Bundesamt für Sicherheit in der Informationstechnik (BSI) zum Thema &lt;a href="https://www.bsi.bund.de/SharedDocs/Audio/DE/BSI/Update_verfuegbar_Folge40_2024_02_29.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&amp;ldquo;Passkey statt Passwort&amp;rdquo;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Den Empfehlungen folgend habe ich mir ein mobiles Hardware-Sicherheitsmodul (HSM) gekauft, um in Zukunft &lt;a href="https://en.wikipedia.org/wiki/WebAuthn" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Passkeys&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für meine Dienste zu verwenden. Schon lange bin ich nicht mehr glücklich mit meiner fragmentierten Geheimnisverwaltung aus Zetteln, Browser-Plugins, offline-Passwortmanagern wie &lt;a href="https://en.wikipedia.org/wiki/KeePass" target="_blank" rel="noopener noreferrer" class="external-link"&gt;KeePass&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, auf Festplatte abgelegten &lt;a href="https://en.wikipedia.org/wiki/Secure_Shell" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSH&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Schlüsseln, Backup-Keys und &lt;a href="https://askleo.com/how-should-i-keep-recovery-codes-secure/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Notfallcodes&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; diverser Anbieter.&lt;/p&gt;&#10;&lt;p&gt;Ich möchte endlich mal Ordnung in die Landschaft bringen, ohne mich an einen kommerziellen Anbieter so fest zu binden, dass ich von ihm abhängig werde.&lt;/p&gt;&#10;&lt;h2 id="zielvorstellung"&gt;Zielvorstellung&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Keine Passwörter oder Login-Daten mehr merken müssen&lt;/li&gt;&#10;&lt;li&gt;Möglichst einfache, dabei sichere Verwaltung von Passwörtern aufbauen&lt;/li&gt;&#10;&lt;li&gt;Ordnung in den Accountwust bringen&lt;/li&gt;&#10;&lt;li&gt;Digitalen Nachlass regeln&lt;/li&gt;&#10;&lt;li&gt;Anderen ermöglichen, es mir gleichzutun&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Welche Erwartung ich jetzt schon einmal nehmen kann: Ganz ohne Passwörter oder Pins geht es auch mit dem HSK nicht. Ein Yubikey ist kein Passwortmanager. Er kann zwar Zertifikate, Passkeys und Secrets für OTPs halten, gibt diese Geheimnisse dann aber nicht mehr preis. Mit seiner Hilfe kann man signieren (Authentizität beweisen), Verschlüsseln und abgeleitete Schlüssel erstellen.&lt;/p&gt;&#10;&lt;p&gt;Vielmehr lässt sich die Passwortverwaltung mit Hilfe eines Hardware-Sicherheitsschlüssels aber zentralisieren und vereinfachen, da gängige Passwortmanager z.B. Passkeys unterstützen und so kein &amp;ldquo;Masterpasswort&amp;rdquo; mehr nötig ist.&lt;/p&gt;&#10;&lt;h3 id="yubikey"&gt;Yubikey&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-yubico-authenticator.avif" alt="Image: Screenshot of Yubico Authenticator&amp;#39;s menu options, showing `Home`, `Accounts`, `Passkeys`, `Certificates`, and `Slots`" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Als HSK nutze ich zwei &lt;a href="https://www.yubico.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Yubikey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Schlüsselanhänger. Einen zum immer dabeihaben und einen als Zweitschlüssel, welcher an einem sicheren Ort verwahrt wird. Es gibt noch andere Anbieter mobiler Hardware Security Keys wie &lt;a href="https://www.nitrokey.com/de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Nitrokey&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die kannte ich vor Verfassen dieses Artikels noch nicht. Das ist bedauerlich, ist ihr Produkt doch quelloffen und damit idealer Testkandidat für mein Blog 😥.&lt;/p&gt;&#10;&lt;h3 id="yubico-authenticator"&gt;Yubico Authenticator&lt;/h3&gt;&#10;&lt;p&gt;Das GUI-Programm &amp;ldquo;Yubikey Authenticator&amp;rdquo; ist nützlich zum Einrichten, Belegen der Funktionstaste (z.B. Bestätigung oder Passwortausgabe als Tastatursimulation) und falls man &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Zwei-Faktor-Authentisierung/zwei-faktor-authentisierung_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;2FA&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Einmalkennwörter z.B. als &lt;a href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;TOTP&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; erzeugen will. Außerdem unterstützt mein Yubikey &lt;a href="https://en.wikipedia.org/wiki/Public_key_infrastructure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;PKI&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, sodass man dort unter anderem auch &lt;a href="https://letsencrypt.org/certificates/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;SSL&lt;/em&gt;-Zertifikate&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und private Schlüssel geschützt ablegen kann.&lt;/p&gt;&#10;&lt;h2 id="konzept-des-hsk"&gt;Konzept des HSK&lt;/h2&gt;&#10;&lt;p&gt;Dabei gehört es zum Konzept, dass private Schlüssel entweder direkt auf dem Hardware-Schlüssel erzeugt werden oder - ohne sie lokal dauerhaft zu speichern - zwar auf einem Endgerät erstellt, aber auf dem Yubikey abgelegt werden. Diese verbleiben auf dem Schlüssel und sind nicht exportierbar. Er unterstützt gängige asymmetrische Verfahren wie &lt;em&gt;RSA&lt;/em&gt; und &lt;em&gt;ECDH&lt;/em&gt;. Für Details siehe z.B. &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02102/BSI-TR-02102.pdf?__blob=publicationFile&amp;amp;v=15" target="_blank" rel="noopener noreferrer" class="external-link"&gt;diese BSI-Publikation über kryptografische Verfahren&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Außerdem wird der Schlüssel die meiste Zeit offline, also ohne Energieversorgung herumliegen und dadurch kaum angreifbar sein. Eine zusätzliche Pin als Schutzmaßnahme bei Verlust des Schlüssels ist ein weiterer Sicherheitsfaktor.&lt;/p&gt;&#10;&lt;h3 id="zweitschlüssel"&gt;Zweitschlüssel&lt;/h3&gt;&#10;&lt;p&gt;Dennoch sollte man sich notieren, welche Zugänge man mit welchem Verfahren schützt, damit die richtigen Accounts im Falle des Verlustes gesperrt werden können. Ein &amp;ldquo;&lt;a href="https://support.yubico.com/s/article/How-to-register-your-spare-key" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Zweitschlüssel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;rdquo; sollte vorhanden und belegt sein, was fast genauso zeitaufwändig ist wie den Hauptschlüssel zu konfigurieren.&lt;/p&gt;&#10;&lt;h3 id="nfc"&gt;NFC&lt;/h3&gt;&#10;&lt;p&gt;Von der &lt;a href="https://en.wikipedia.org/wiki/Near-field_communication" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;em&gt;NFC&lt;/em&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Unterstützung meines Schlüssels hatte ich mir erhofft, Logins noch schneller und leichter hinter mich bringen zu können. Zwei Dinge hier stören mich jedoch: Meinen Laptop kriege ich partout nicht dazu, den Yubikey per NFC zu erkennen. Und auf dem Handy funktioniert die NFC-Erkennung höchstens mittelmäßig. Halte ich den Schlüssel dran, startet die App und bittet mich um Eingabe der Pin. Tue ich dies, muss ich den Schlüssel wieder entfernen und erneut an den Sensor halten &amp;ldquo;Schlüssel nicht bewegen!&amp;rdquo;. Das funktioniert längst nicht jedes mal und wirkt insgesamt ziemlich störanfällig. Schade.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-nfc-device-manager.avif" alt="Image: NFC shown in device manager of my Windows laptop: Drivers installed but not detecting my Yubikey" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="was-mir-beim-umgang-mit-yubikey-fehlt"&gt;Was mir beim Umgang mit Yubikey fehlt&lt;/h2&gt;&#10;&lt;p&gt;Insgesamt vermisse ich ein paar Komfort-Funktionen beim alltäglichen Umgang mit dem Yubikey.&#10;Bei Klick auf die gewünschten Schlüssel oder Accounts passiert nämlich nichts. Und konfigurieren kann ich da anscheinend auch nichts.&lt;/p&gt;&#10;&lt;p&gt;Bei der Verwendung von &lt;strong&gt;Passkeys&lt;/strong&gt; würde ich mir eine Verknüpfung wünschen. Ein Klick auf den entsprechenden Schlüssel im Authenticator sollte die entsprechende App bzw. das Browserfenster mit der Zielwebsite aufrufen. Da ich das HSK zu diesem Zeitpunkt bereits mit der PIN entsperrt habe, kann sogar der Anmeldevorgang nach Berührung des Schlüssels vollautomatisch stattfinden.&lt;/p&gt;&#10;&lt;p&gt;Ähnliches wünsche ich mir beim Umgang mit &lt;strong&gt;Accounts&lt;/strong&gt;. Warum heißt das Modul &amp;ldquo;Accounts&amp;rdquo;, wenn ich meine Konten nicht direkt darüber aufrufen kann? Auch hier muss ich die entsprechenden WebApps etc. selbst öffnen, den Passwortmanager am Start haben und dann noch die 2FA-Codes vom Authenticator herauskopieren. Das geht bestimmt einfacher.&lt;/p&gt;&#10;&lt;h3 id="touch-password-pin-puk-management-key-passphrase"&gt;Touch, password, PIN, PUK, management key, passphrase&lt;/h3&gt;&#10;&lt;p&gt;Je nach Modul gibt es auch bei der Verwendung des Yubikey zumeist eine Mehr-Faktor-Authentisierung. Das unkomplizierteste Verfahren ist eine Anwesenheitserkennung über eine Berührung des Sticks. Damit kann sichergestellt werden, dass eine Person das HSK aktiv bedient und nicht etwa automatisch eingeloggt werden kann. &lt;em&gt;Touch&lt;/em&gt; kann modulübergreifend angewendet werden; zur Detailkonfiguration komme ich &lt;a href="https://blog.schallbert.de/hardware-security-key-add-ssh/"&gt;in meinem Artikel über SSH&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Das &lt;em&gt;Accounts&lt;/em&gt;-Modul kann optional mittels Passwort, das &lt;em&gt;Passkeys&lt;/em&gt;-Modul per &lt;em&gt;PIN&lt;/em&gt; gesichert werden. Manche Anbieter verlangen die Einrichtung einer PIN als zweiten Faktor sogar.&lt;/p&gt;&#10;&lt;p&gt;Das &lt;em&gt;Certificates&lt;/em&gt;-Modul muss mit &lt;a href="https://docs.yubico.com/yesdk/users-manual/application-piv/pin-puk-mgmt-key.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PIN, und PUK und Management Key&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; vor unbefugtem Zugriff gesperrt werden. Private Schlüssel oder Zertifikate können optional zusätzlich mit einer &lt;a href="https://www.ssh.com/academy/ssh/passphrase" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Passphrase&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ausgestattet werden, was die Schlüssel selbst schützt (und aus meiner Sicht für den Yubikey keinen Sinn macht, da die privaten Schlüssel ohnehin nicht auslesbar sind).&lt;/p&gt;&#10;&lt;h2 id="funktionsumfang-und-kryptographische-landschaft"&gt;Funktionsumfang und kryptographische Landschaft&lt;/h2&gt;&#10;&lt;p&gt;Da das HSK einen so großen Sicherheitsgewinn verspricht, möchte ich natürlich möglichst viele seiner Fähigkeiten nutzen. Ich habe jetzt ein paar Wochen mit den verschiedenen Modulen herumgespielt und muss feststellen, dass man sich ganz schön hineindenken und viel lernen muss, um alle gut nutzen zu können. Immerhin versteht man danach ungefähr was die Module tun und bekommt einen guten Überblick zu den Themen Internet-Sicherheit, Verschlüsselungsverfahren und -Protokolle.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Accounts&amp;rdquo; und &amp;ldquo;Passkeys&amp;rdquo; sind für Eingabemasken (Plattformen, Webseiten, Shops) und Apps (Mailer, Passwortmanager, Messenger) einfach einzurichten: Absolut empfehlenswert! - schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Yubikey-Modul&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Beschreibung&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Accounts&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Verwaltung von Konten/Anmeldungen, unterstützt TOTP&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Passkeys&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Passwortloses Anmeldeverfahren durch &lt;a href="https://bitmi.de/it-sicherheitsblog-was-ist-eigentlich-fido2/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;FIDO/WebAuthn&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (Public-Key-Credential)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Certificates&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Speicherung und Nutzung von Zertifikaten; unterstützt &lt;a href="https://www.openpgp.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OpenPGP&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PGP) und &lt;a href="https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR02103/BSI-TR-02103.pdf?__blob=publicationFile&amp;amp;v=2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;X.509&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; (PIV)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Slots&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Zwei programmierbare Slots zur Ausgabe statischer Passworte oder Einmalkennwörter&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Kompliziert und aufwändig wird es meiner Meinung nach mit Public/Private Keys und Zertifikaten. Erzeugungs- und Validierungsverfahren hängen vom Betriebssystem oder darauf verfügbaren Programmen ab und Anleitungen greifen teils zu kurz oder widersprechen sich: Nur für Nerds oder Profis. - schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Gehen wir die Module mal durch.&lt;/p&gt;&#10;&lt;h3 id="2-faktor-authentisierung-yubikey-accounts"&gt;2-Faktor-Authentisierung &amp;ldquo;Yubikey: Accounts&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Der Yubikey kann als Generator für zeitbasierte Einmalkennwörter dienen. Dafür legt man wie in jeder anderen Authenticator-App auch pro Dienst ein Konto an und übergibt einen Sicherheitsschlüssel (Geheimnis, Secret) an den Yubikey. Auf Basis dieses Geheimnisses können bei angestecktem HSK nun Einmalcodes erzeugt werden.&lt;/p&gt;&#10;&lt;p&gt;Da es keine Möglichkeit gibt, diese &amp;ldquo;Secrets&amp;rdquo; vom Yubikey zu exportieren, muss der Vorgang für Zweitschlüssel wiederholt werden. Doch nicht jeder Anbieter sieht die Nutzung mehrerer Authentikatoren vor. Also kann es sinnvoll sein, das Geheimnis in der Zwischenablage&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; zu halten und ihn bei der Konfiguration des/der Zweitschlüssel wiederzuverwenden.&lt;/p&gt;&#10;&lt;h3 id="passkeys-yubikey-passkeys"&gt;Passkeys &amp;ldquo;Yubikey: Passkeys&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Das Passkey-Modul meines Yubikey funktioniert recht gut. Viele Services&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; unterstützen die passwortlose Anmeldung leider noch nicht oder akzeptieren Passkeys nur als zweiten Faktor. Bei anderen Services&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; kann ich das Passwort nicht deaktivieren, sodass ich kaum einen Sicherheitsgewinn sehe oder mir werden Backup-Codes ausgegeben, die im Notfall als zweiter Faktor dienen sollen&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. Immerhin, ich kann durch den Passkey jetzt bei einigen Diensten die Passwort-TOTP-Anmeldung umgehen und bin schneller eingeloggt.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Die wohl größten Vorteile der Technologie für Nutzerinnen und Nutzer liegen darin, dass Sie in Zukunft zum einen keine Passwörter mehr erstellen und verwalten müssen. Zum anderen sind Passkeys immun gegen die breite Masse bekannter Phishing-Angriffe.&amp;rdquo; - &lt;a href="https://www.bsi.bund.de/DE/Themen/Verbraucherinnen-und-Verbraucher/Informationen-und-Empfehlungen/Cyber-Sicherheitsempfehlungen/Accountschutz/Passkeys/passkeys-anmelden-ohne-passwort_node.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;BSI-Website, Oktober 2025&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Die Authenticator-App braucht man nicht zur Konfiguration von Passkeys. Dies läuft komplett im Browser oder der Kommandokonsole ab. Beide müssen das Handling von Passkeys unterstützen.&lt;/p&gt;&#10;&lt;h3 id="keys--certificates-yubikey-certificates"&gt;Keys &amp;amp; Certificates &amp;ldquo;Yubikey: Certificates&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Um hier etwas Licht hineinzubringen, müssen wir uns erst einmal den Unterschied zwischen Schlüsselpaar und Zertifikat im Kontext der Authentisierung vor Augen führen. In beiden Fallen geht es darum, die Identität des Client zu verifizieren. Vor einem Server (z.B. per SSH), einer Institution (z.B. einer Certificate Authority) oder einem anderen Client (z.B. per &lt;em&gt;PGP&lt;/em&gt;). Somit wird für das Ziel feststellbar, dass es sich tatsächlich um den zur Verbindung berechtigten Client handelt.&lt;/p&gt;&#10;&lt;h3 id="publicprivate-key-pair"&gt;Public/Private Key Pair&lt;/h3&gt;&#10;&lt;p&gt;Das reine Schlüsselpaar (also kein Zertifikat) wird üblicherweise dann verwendet, wenn das Ziel der Kommunikation im Besitz desjenigen ist, der auch über den Client verfügt. Dann nämlich kann der Client zum Aufsetzen der Verbindung den öffentlichen Schlüssel auf dem Host platzieren. Dieses Verfahren wird z.B. bei Einrichten von SSH-Verbindungen und für einige APIs wie bei der Search Console verschiedener Suchmaschinenanbieter angewendet.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-04-25-gitea-ssh-keys.avif" alt="Image: Gitea account security settings: SSH public key stored in Gitea Webapp" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Der Host kann dem Client mit dem öffentlichen Schlüssel verschlüsselte Testnachricht schicken, welche nur mit dem zugehörigen privaten Schlüssel wieder in Klartext umgewandelt werden kann. Schickt der Client den korrekten Klartext zum Host zurück, ist für den Host bewiesen, dass der Verbindungspartner authentisch ist.&lt;/p&gt;&#10;&lt;h3 id="certificates--piv"&gt;Certificates / PIV&lt;/h3&gt;&#10;&lt;p&gt;Doch was, wenn der Host nicht unter der Kontrolle des Clients steht? Wie soll dann sichergestellt werden, dass sich weder Client noch Server für jemand anders ausgibt und einen gefälschten öffentlichen Schlüssel hinterlegt? Genau da kommen Zertifikate zum Einsatz.&lt;/p&gt;&#10;&lt;p&gt;Vereinfacht gesagt enthält das Zertifikat nicht nur den öffentlichen Schlüsselteil, sondern zusätzliche Informationen über den Netzteilnehmer und die gewünschte Verbindung. Nun kommt eine vertrauenswürdige, an der Kommunikation zwischen Client und Server unbeteiligte dritte Partei ins Spiel. In der Private Key Infrastructure &lt;em&gt;PKI&lt;/em&gt; nennt man sie Certificate Authority &lt;em&gt;CA&lt;/em&gt;.&lt;/p&gt;&#10;&lt;p&gt;Der Netzteilnehmer sendet nun den öffentlichen Schlüssel samt Metadaten über sich selbst an die CA und stellt einen sogenannten &lt;em&gt;CSR&lt;/em&gt;, einen Certificate Signing Request. Die CA prüft nun, ob der Kanal des Teilnehmers auch wirklich diesem gehört.&lt;/p&gt;&#10;&lt;p&gt;Bei einer E-Mail (Stichwort &lt;a href="https://www.gpg4win.org/doc/en/gpg4win-compendium_8.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PGP/GPG&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) kann die CA beispielsweise einen aus dem Public Key errechneten Hash-Link an die angegebene Mailadresse versenden. Wird der Link geklickt, beweist dies der CA gegenüber, dass die Mailadresse tatsächlich dem Client gehört und sie kann das Zertifikat ausstellen, den CSR also mit ihrem privaten Schlüssel signieren.&lt;/p&gt;&#10;&lt;p&gt;Bei Domains (Stichwort SSL/TLS) kann die CA beim Domain Name Service &lt;em&gt;DNS&lt;/em&gt;-Anbieter prüfen, ob die Angaben zur Anfrage des Servers passen.&lt;/p&gt;&#10;&lt;p&gt;Clients haben die Signaturen gängiger Certificate Authorities eingespeichert (z.B. im Browser). Wird das Zertifikat auf dem Rückweg zwischen Server und Client manipuliert, verändert sich der Hashwert und der Client spuckt eine Zertifikatswarnung aus: &amp;ldquo;invalid certificate&amp;rdquo;.&#10;Jetzt muss man jedoch den Certificate Authorities vertrauen&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; 🙃&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Der Rechner sollte dafür offline sein. Wenn man supersicher sein will, macht man das Anlernen auf einem Live-Betriebssystem. Das bootet üblicherweise von einem USB-Stick und hat nach dem Herunterfahren alles vergessen.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Z.B. Gar nicht: Mailbox, 2FA: Discord und Kraken, zum Zeitpunkt Apr-26. Vielleicht habe ich aber auch nicht die richtigen Einstellungen gefunden?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Z.B. Google, Github und Hetzner, zum Zeitpunkt Apr-26.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;Auch diese Backup-Codes müssen sicher abgelegt werden. Idealerweise wieder offline, unbedingt aber unabhängig vom HSK.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;Es gibt Alternativen zu Institutionen wie CAs, siehe &lt;a href="https://de.wikipedia.org/wiki/Web_of_Trust" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Web of Trust&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. So kann die Verifikation der Identität beispielsweise auch peer-to-peer von unbeteiligten Dritten stattfinden.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Giteas act_runner rootless: Startprobleme</title><link>https://blog.schallbert.de/act-runner-dind-failed-to-start-the-child/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/act-runner-dind-failed-to-start-the-child/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Top page crop of OWASP&amp;#39;&amp;#39;s Docker Security Cheat Sheet [Source, downloaded May-26](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) [License](https://creativecommons.org/licenses/by-sa/4.0/)"&#10; title="Giteas act_runner rootless: Startprobleme" /&gt;&#10;&lt;h2 id="das-fehlerbild"&gt;Das Fehlerbild&lt;/h2&gt;&#10;&lt;p&gt;Mein &lt;a href="https://blog.schallbert.de/gitea-act-runner-dind/"&gt;docker-in-docker (DinD) act_runner&lt;/a&gt; stürzt kurz nach dem Start mit folgender Fehlermeldung ab:&lt;/p&gt;&#10;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-problem.avif" alt="Image: console window with docker logs text output: `\[rootlesskit:parent\] error: failed to start the child: fork/exec /proc/self/exe: operation not permitted`" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="issue-ticket-bei-gitea"&gt;Issue-Ticket bei Gitea&lt;/h2&gt;&#10;&lt;p&gt;In der &lt;a href="https://gitea.com/gitea/act_runner/issues/721" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea-Community (issue #721)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; habe ich ein Ticket für das Problem angelegt. Im Folgenden gehe ich auf eigene Erkenntnisse ein und fasse die Diskussion etwas zusammen.&lt;/p&gt;&#10;&lt;h2 id="problem-mit-kernel-berechtigungen"&gt;Problem mit Kernel-Berechtigungen?&lt;/h2&gt;&#10;&lt;p&gt;Der &lt;code&gt;act_runner&lt;/code&gt; muss bei der Docker-in-Docker Konfiguration selbst einen eigenen &lt;a href="https://docs.docker.com/engine/security/#docker-daemon-attack-surface" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Daemon&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; betreiben. Nur so kann der Runner für die &lt;em&gt;Actions&lt;/em&gt; eigene Container erstellen. Dies ist aus Sicherheitsgründen erst einmal nicht erlaubt.&lt;/p&gt;&#10;&lt;h3 id="warum-container-keine-container-starten-dürfen"&gt;Warum Container keine Container starten dürfen&lt;/h3&gt;&#10;&lt;p&gt;Im Kontext des &lt;em&gt;act_runner&lt;/em&gt; führt eine Action Code aus, der selbst Teil des Repository ist. Wird unbemerkt Schadcode in die Action eingeschleust, z.B. von einem &amp;ldquo;Collaborator&amp;rdquo;, so kann dieser bei einer Docker-Installation ohne DinD über den &lt;a href="https://docs.docker.com/engine/security/rootless/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;unter Umständen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit &lt;strong&gt;root-Rechten&lt;/strong&gt; ausgestatteten Docker-Daemon &lt;code&gt;dockerd&lt;/code&gt; direkt auf das Hostsystem durchgreifen.&lt;/p&gt;&#10;&lt;h3 id="auswirkungen-falls-trotz-dind-etwas-schief-geht"&gt;Auswirkungen, falls trotz DinD etwas schief geht&lt;/h3&gt;&#10;&lt;p&gt;Bei Anwendung des DinD-Konzeptes kann dies bei einem Angriff lediglich zum Zugriff auf den &lt;code&gt;dockerd&lt;/code&gt; innerhalb des &lt;em&gt;act_runner&lt;/em&gt; Containers führen - und das nur, wenn die Action nicht sauber gekapselt ist. Immer noch nicht gut, aber vertretbar: Bei Neustart des Containers ist der Status quo ante wiederhergestellt. Durchgriff auf das Hostsystem besteht allenfalls indirekt dadurch, dass der Container selbst mit sämtlichen Kernel-Features ausgestattet ist.&lt;/p&gt;&#10;&lt;h3 id="einfache-lösung-dind-container-privileged-starten"&gt;Einfache Lösung: DinD-Container &lt;code&gt;privileged&lt;/code&gt; starten&lt;/h3&gt;&#10;&lt;p&gt;Um dem DinD-Container jetzt die Möglichkeit zu geben eigene &lt;em&gt;Actions&lt;/em&gt; aufzusetzen, kann in der Konfiguration &lt;code&gt;privileged: true&lt;/code&gt; gesetzt werden. Damit werden dem Container &lt;strong&gt;alle Kernel-Fähigkeiten&lt;/strong&gt; eingeräumt. Dies ist laut (dem hervorragend gemachten) &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-3-limit-capabilities-grant-only-specific-capabilities-needed-by-a-container" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP Security Cheat Sheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; möglichst zu vermeiden. Sollte &lt;em&gt;act_runner&lt;/em&gt; nun selbst angegriffen werden oder kritische Sicherheitslücken offenbaren, hätte der Eindringling auf diese Weise bereits alle Möglichkeiten, der Kapselung vor dem Host-System zu entgehen.&lt;/p&gt;&#10;&lt;p&gt;Will man den einfachen Weg gehen, sieht die entsprechende &lt;code&gt;docker-compose.yml&lt;/code&gt; wie folgt aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section ACT_RUNNER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;And remember: Do not run containers with the &amp;ndash;privileged flag!!!&amp;rdquo; - OWASP&amp;rsquo;s Docker Security Cheat Sheet, 2026 &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;License&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="sackgasse-nur-unbedingt-benötigte-rechte-vergeben"&gt;Sackgasse: Nur unbedingt benötigte Rechte vergeben&lt;/h3&gt;&#10;&lt;p&gt;Nach kurzer Recherche finde ich einen &lt;a href="https://www.codestudy.net/blog/can-i-run-docker-in-docker-without-using-the-privileged-flag/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Artikel auf CodeStudy.net&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, welcher sich mit dem Thema DinD beschäftigt. Hieraus bastele ich Änderungen in meiner &lt;code&gt;docker-compose.yml&lt;/code&gt; zusammen.&lt;/p&gt;&#10;&lt;p&gt;Meine Vorgehensweise:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Änderungen in &lt;code&gt;docker-compose.yml&lt;/code&gt; vornehmen&lt;/li&gt;&#10;&lt;li&gt;Service neu starten &lt;code&gt;docker compose restart &amp;lt;service&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Logs anschauen &lt;code&gt;docker logs --tail 100 &amp;lt;container-name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Falls gestartet: Programm laufen lassen &lt;code&gt;web-app-&amp;gt;repo-&amp;gt;jobs-&amp;gt;rerun_all-jobs&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Bei Fehlschlag: Wiederholen&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Ich beginne mit wenigen, aber sehr mächtigen Rechten. Nach ein paar Iterationen erhalte ich:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section act_runner DinD-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cap_add&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;SYS_ADMIN&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;security_opt&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#66d9ef"&gt;no&lt;/span&gt;-&lt;span style="color:#ae81ff"&gt;new-privileges:true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apparmor:unconfined &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;systempaths=unconfined&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;writable-cgroups=true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Aber noch immer fehlen mir Rechte, die außerhalb von Kernel-Privilegien und Sicherheitszonen liegen: Der Docker-Daemon möchte auf &lt;code&gt;sysfs&lt;/code&gt; und &lt;code&gt;proc&lt;/code&gt;, also Systemdateiverzeichnisse und -Prozesse zugreifen können.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;[rootlesskit:child ] failed to mount sysfs, falling back to read-only mount: operation not permitted&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das kann ich selbst mit &lt;code&gt;cap_add: ALL&lt;/code&gt; nicht ausgleichen. Zudem wird die Quellenlage im Internet auf dieser technischen Ebene echt dünn (und ich bin kein Docker-Spezialist). Ein &lt;a href="https://zhsj.me/blog/view/dind-without-privileged" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Re-Mount&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; der Dateisysteme könnte hier noch helfen; das erscheint mir aber zu experimentell und fehleranfällig.&lt;/p&gt;&#10;&lt;p&gt;Nach weiteren Stunden Recherche und Trial-and-Error auf meinem Server muss ich &lt;a href="https://github.com/docker-library/docker/issues/546" target="_blank" rel="noopener noreferrer" class="external-link"&gt;@tianon in einer Diskussion auf Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zustimmen: Der Docker Daemon benötigt so viele Rechte und Fähigkeiten, dass man gleich bei &lt;code&gt;privileged: true&lt;/code&gt; bleiben kann und sich nicht mit einer Armada an &lt;code&gt;CAP_ADD&lt;/code&gt; und System-bind-mounts herumschlagen muss.&lt;/p&gt;&#10;&lt;h3 id="zukünftige-lösung-virtualisierung-oder-daemonless"&gt;Zukünftige Lösung: Virtualisierung oder Daemonless&lt;/h3&gt;&#10;&lt;p&gt;Doch es gibt möglicherweise bessere Lösungen: Eigene Container-Laufzeitumgebungen wie &lt;a href="https://github.com/nestybox/sysbox" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Sysbox&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Box hat selbst auf dem Hostsystem keine besonderen Rechte, ähnlich wie in einer virtuellen Maschine scheint sie in ihr laufenden Applikationen aber alle Rechte und Fähigkeiten zur Verfügung stellen zu können.&lt;/p&gt;&#10;&lt;p&gt;Installation und Inbetriebnahme bis hin zu funktionierenden Job-Containern führt für diesen Post zu weit. Daher muss ich vorerst auf andere &lt;a href="https://www.jaburjak.cz/posts/docker-in-docker-unprivileged/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quellen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; verweisen.&lt;/p&gt;&#10;&lt;p&gt;Auch das Umschwenken &lt;a href="https://tiendu.github.io/2025/04/18/dind.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;von Docker auf Podman&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist eine mögliche Lösung: Podman ist Daemonless, rootless und bietet ähnlichen Funktionsumfang wie Docker. Ein Umzug nach Podman wäre für mich ein eigenes Projekt und passt nicht in den Kontext dieses Posts.&lt;/p&gt;&#10;&lt;h2 id="linux-security-modules-seccomp-apparmor-selinux"&gt;Linux Security Modules (seccomp, AppArmor, SELinux)&lt;/h2&gt;&#10;&lt;p&gt;Begriffsklärung: &lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/LSM/index.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;LSM&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ermöglichen verschiedene Sicherheitsprüfungen und -Sperren auf Kernelebene. Durch &amp;ldquo;Mandatory Access Control&amp;rdquo; können Sicherheitserweiterungen wie AppArmor Kernel-Fähigkeiten für einzelne Anwendungen steuern und Zugriffe falls nötig sperren.&lt;/p&gt;&#10;&lt;h3 id="apparmor"&gt;Apparmor&lt;/h3&gt;&#10;&lt;p&gt;Ich betreibe meinen Server (VPS) mit Ubuntu. Dieses Betriebssystem hat in den aktuelleren Versionen &amp;ldquo;user namespace creation restrictions&amp;rdquo; in AppArmor integriert, was das Betreiben von &lt;em&gt;appimages&lt;/em&gt;, &lt;em&gt;WebApps&lt;/em&gt; und eben auch &lt;em&gt;Containers&lt;/em&gt; mit höheren Rechten verhindert.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Unprivileged user namespaces are a feature in the Linux kernel [&amp;hellip;]; it enables unprivileged users to gain administrator (root) permissions within a confined environment [&amp;hellip;]&amp;rdquo; - mbelair, Ubuntu Discourse, as of May-2026, &lt;a href="https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Ubuntu Discourse Website&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Kurzgesagt wurde dieses Werkzeug als &lt;a href="https://de.wikipedia.org/wiki/H%C3%A4rten_%28Computer%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;security-hardening-Maßnahme&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entwickelt, um Angriffsfläche auf dem Host-System bei Ausführung von Programmen zu reduzieren, die höhere Rechte erfordern.&lt;/p&gt;&#10;&lt;h3 id="einfache-lösung-komplett-abstellen"&gt;Einfache Lösung: Komplett abstellen&lt;/h3&gt;&#10;&lt;p&gt;Die Dampfhammermethode stellt das Feature für Benutzernamensräume komplett kalt. Wir teilen AppAmor mit, dass Drittprogramme wie in alten Betriebssystemversionen auch ohne Einschränkungen Kernel-Features oder höhere Rechte nutzen dürfen. Den zugehörigen Befehl habe ich aus dem &lt;a href="https://askubuntu.com/questions/1511854/how-to-permanently-disable-ubuntus-new-apparmor-user-namespace-creation-restric" target="_blank" rel="noopener noreferrer" class="external-link"&gt;askubuntu-Forum&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; entliehen.&lt;/p&gt;&#10;&lt;p&gt;Er schaltet die Einschränkung von Nicht-Admin-Benutzernamensräumen (die Übertragung ins Deutsche tut schon fast weh) für diese Sitzung ab, indem er die Kernelparameter zur Laufzeit umschreibt (&lt;code&gt;-w&lt;/code&gt;).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbertTestsThis@machine:~# sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich halte den Befehl zur Fehlersuche für sehr hilfreich. Möchte man herausfinden, ob das gewünschte Programm wegen AppArmor nicht startet:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Obiges Kommando eingeben&lt;/li&gt;&#10;&lt;li&gt;das Drittprogramm, den Container etc. testen&lt;/li&gt;&#10;&lt;li&gt;durchbooten. Oder den Befehl mit &lt;code&gt;=1&lt;/code&gt; eingeben. Danach ist der Ursprungszustand wiederhergestellt.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Möchte man diese Verwundbarkeit permanent erhalten, so gibt man ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;makeVulnerability@machine:~# echo &lt;span style="color:#e6db74"&gt;&amp;#39;kernel.apparmor_restrict_unprivileged_userns = 0&amp;#39;&lt;/span&gt; | sudo tee /etc/sysctl.d/20-apparmor-donotrestrict.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;makeVulnerability@machine:~# sudo shutdown -r now&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies alleine stellt noch keine Sicherheitslücke dar. Es ist jetzt nur prinzipiell einfacher geworden, eventuelle Schwächen im Kernel auszunutzen und der &amp;ldquo;Sandbox&amp;rdquo; des Containers zu entrinnen.&lt;/p&gt;&#10;&lt;h3 id="richtige-lösung-für-jede-anwendung-justieren"&gt;Richtige Lösung: Für jede Anwendung justieren&lt;/h3&gt;&#10;&lt;p&gt;Es gibt Anwendungen (wie meine DinD-Version von &lt;code&gt;act_runner&lt;/code&gt;), die benötigen zwingend erweiterte Rechte um ordnungsgemäß zu funktionieren. Und nur diese sollten die Möglichkeit erhalten, Nicht-Admin-Benutzernamensräume zu erhalten.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://docs.docker.com/engine/security/apparmor/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker selbst&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hat &lt;em&gt;AppAmor&lt;/em&gt; einen Abschnitt in der Dokumentation gewidmet. Um zur Lösung für den DinD-Runner zu gelangen, benötigt es eine &lt;a href="https://www.spad.uk/posts/rootless-dind-noble/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Transferleistung (danke, @thespad)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Zur Wiederholung nochmal die Fehlermeldung von oben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner | &lt;span style="color:#f92672"&gt;[&lt;/span&gt;rootlesskit:parent&lt;span style="color:#f92672"&gt;]&lt;/span&gt; error: failed to start the child: fork/exec /proc/self/exe: operation not permitted&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gitea-runner | s6-svwait: fatal: some services reported permanent failure or their supervisor died&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Logfile sagt aus, dass der &lt;code&gt;parent&lt;/code&gt; (Docker-Daemon im Container) sein &lt;code&gt;child&lt;/code&gt; (Runner-Container) nicht starten kann, weil er für andere Teilnehmer (&lt;code&gt;fork&lt;/code&gt;) selbst keine Prozesse (&lt;code&gt;/proc/self&lt;/code&gt;) erstellen kann. Wer ist der User dieses Daemons? &lt;code&gt;rootlesskit&lt;/code&gt;.&#10;Genau hier hakt unsere Lösung ein: Wir müssen in der &lt;code&gt;docker-compose.yml&lt;/code&gt; das AppArmor-Profil für &lt;em&gt;rootlesskit&lt;/em&gt; im DinD-Container des act_runner freigeben.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# section act_runner DinD-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;runner&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea/act_runner:latest-dind-rootless&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;gitea-runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;privileged&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;security_opt&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apparmor=rootlesskit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Entsprechend gut sieht jetzt auch das Logfile aus. Fertig!&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2026-03-28-act_runner-dind-failed-to-start-child-solved.avif" alt="Image: act_runner rootless-DinD logfile with --privileged:true and --security_opt:apparmor=rootlesskit, showing a clean startup" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Ich habe das Gefühl, dass jener Post mit &amp;ldquo;KI&amp;rdquo; geschrieben wurde. Wenn es konkret wird und es um &amp;ldquo;critical volume mounts&amp;rdquo; geht, stehen dort nämlich keine echten System-Pfade mehr und der Artikel wird insgesamt so vage, dass ich den Ratschlägen folgend gar nicht bis zum Ziel komme.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>fail2ban: Update zerstört Konfiguration</title><link>https://blog.schallbert.de/fail2ban-error-configuration-bad-value/</link><pubDate>Thu, 23 Oct 2025 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fail2ban-error-configuration-bad-value/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-09-23-docker-chain-thumb.avif"&#10; class="post-cover"&#10; alt="Image: iptables view for docker-user chain"&#10; title="fail2ban: Update zerstört Konfiguration" /&gt;&#10;&lt;p&gt;Alle paar Wochen sehe ich auf meinem Server üblicherweise nach dem Rechten:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Ist bei Docker alles ok? &lt;code&gt;docker ps&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Werden die Backups korrekt erzeugt? &lt;code&gt;borgmatic repo-info&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Funktionieren meine Abwehrmaßnahmen? &lt;code&gt;iptables -n -L&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Laufen meine Daemons zur Update-Automation? &lt;code&gt;htop -F /server-config&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Dieses mal allerdings zeigte &lt;code&gt;iptables&lt;/code&gt; mir gähnende Leere. Das ist ungewöhnlich und deutet darauf hin, dass mit &lt;em&gt;fail2ban&lt;/em&gt; etwas nicht stimmt.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain FORWARD &lt;span style="color:#f92672"&gt;(&lt;/span&gt;policy DROP&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DOCKER-FORWARD all -- 0.0.0.0/0 0.0.0.0/0 &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target port opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Übersetzt lautet die Ausgabe etwa in etwa&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Die &lt;code&gt;FORWARD&lt;/code&gt;-Chain leitet wie von &lt;em&gt;Docker&lt;/em&gt; benötigt an die für Container relevante &lt;code&gt;DOCKER-USER&lt;/code&gt;-Chain weiter. Unter dieser sind allerdings keine Anwendungen gelistet, die Pakete zurückweisen oder deren Weiterleitung verbieten (&lt;code&gt;REJECT/DROP&lt;/code&gt;) können.&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;An dieser Stelle müssten zwei Einträge für &lt;code&gt;caddy-server&lt;/code&gt; und &lt;code&gt;caddy-ratelimit&lt;/code&gt; auftauchen.&lt;/p&gt;&#10;&lt;h2 id="fail2ban-error-failed-during-configuration"&gt;&amp;ldquo;fail2ban ERROR Failed during configuration&amp;rdquo;&lt;/h2&gt;&#10;&lt;p&gt;Also schaue ich zuerst mal bei Docker nach:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker logs -t fail2ban &lt;span style="color:#75715e"&gt;# Logs für den Container mit Namen fail2ban mit Zeitempel ausgeben&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Statt wie üblich nach dem Durchscrollen mit &lt;code&gt;&amp;lt;timestamp&amp;gt; Server ready&lt;/code&gt; belohnt zu werden, bekomme eine nicht enden wollende Flut an Logmeldungen angezeigt. Sie wiederholen sich und zeigen, dass &lt;em&gt;fail2ban&lt;/em&gt; mit &lt;code&gt;Exit with code 255&lt;/code&gt; aussteigt. Einen besseren Überblick liefert mir das Log, wenn ich nur die letzten 100 Zeilen ausgebe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker logs -t --tail &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt; fail2ban &lt;span style="color:#75715e"&gt;# Nur die letzten 100 Zeilen des Logs ausgeben, mit Zeitstempel&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="die-fehlermeldung"&gt;Die Fehlermeldung&lt;/h3&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;&amp;lt;timestamp, ID&amp;gt; ERROR Failed during configuration: Bad value substitution: &#10;option &amp;#39;action&amp;#39; in section &amp;#39;caddy-ratelimit&amp;#39; contains an interpolation key &amp;#39;banaction&amp;#39; which is not a valid option name. &#10;Raw value: &amp;#39;%(action_)s&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die Meldung ist interessant, weil ich genau an dieser Stelle auch schon beim Erstellen im Zuge des Artikels &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#check-jail"&gt;Fail2ban mit Caddy&lt;/a&gt; Probleme mit der &lt;code&gt;jail.local&lt;/code&gt; hatte. Damals musste ich &lt;code&gt;action = iptables-multiport&lt;/code&gt; aus der Konfiguration nehmen, weil die auslösenden IP-Adressen auf der falschen &lt;code&gt;CHAIN&lt;/code&gt; (INPUT) gelandet waren.&lt;/p&gt;&#10;&lt;p&gt;Nun scheint also die default-action auf der &lt;code&gt;DOCKER-USER&lt;/code&gt; chain nicht mehr zu funktionieren. Also gehe ich ins Netz und suche nach ein paar Quellen für eine mögliche Lösung. Ich finde aber weder in der Update-Historie von &lt;em&gt;fail2ban&lt;/em&gt; noch in der von mir benutzten Distribution von &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Hinweise auf Updates, die hier große Änderungen im &lt;code&gt;action.d&lt;/code&gt; Ordner veranlassen würden.&lt;/p&gt;&#10;&lt;h3 id="lösung-per-banaction--iptablestypemultiport"&gt;Lösung per &lt;code&gt;banaction = iptables[type=multiport]&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Also bemühe ich die Websuche und finde einen Kommentar eines &lt;a href="https://github.com/fail2ban/fail2ban/issues/3894" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Maintainers von fail2ban&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;[&amp;hellip;] With action = iptables[type=multiport, protocol=tcp, chain=DOCKER-USER] you overwrote the default action [&amp;hellip;] [Solution:] simply set the chain variable only in the jail [&amp;hellip;]: &lt;code&gt;port = http, https banaction = iptables[type=multiport] chain = DOCKER-USER&lt;/code&gt;&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Die Grundlage dieser Aussage kann man in den &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; nachlesen. Nebenbemerkung: An allen Stellen wird empfohlen, für zukünftige Anwendungen das modernere &lt;em&gt;nftables&lt;/em&gt; statt &lt;em&gt;iptables&lt;/em&gt; zu verwenden. &lt;a href="https://linux-audit.com/networking/nftables/differences-between-iptables-and-nftables-explained/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Hier&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gibt es einen Artikel zum Thema.&lt;/p&gt;&#10;&lt;p&gt;Somit kann ich das bisher in meiner &lt;code&gt;jail.local&lt;/code&gt; vorhandene &lt;code&gt;action&lt;/code&gt;-Feld komplett löschen und stattdessen &lt;code&gt;banaction&lt;/code&gt; verwenden. Sie sieht nun wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# jail.local update Oct-2025&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Server/Client errors: 401 403 404 500&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;caddy-status]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-status&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = &amp;lt;path/to/logfiles&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10min&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 5&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 1d&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;banaction = iptables[type=multiport]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit dieser Konfiguration fährt &lt;em&gt;fail2ban&lt;/em&gt; normal hoch und gibt &lt;code&gt;Server ready&lt;/code&gt; aus.&lt;/p&gt;&#10;&lt;h3 id="überprüfen-der-lösung-mittels-iptables"&gt;Überprüfen der Lösung mittels &lt;em&gt;iptables&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Ich erzeuge mir wie oben beschrieben Einträge in den Test-Logfiles, welche die Aktionen von &lt;code&gt;caddy-status&lt;/code&gt; und &lt;code&gt;caddy-ratelimit&lt;/code&gt; auslösen sollen. Wie erwartet erhalte ich in der &amp;ldquo;blocklist&amp;rdquo;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-status tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Giteas *act_runner* rootless ausführen</title><link>https://blog.schallbert.de/gitea-act-runner-dind/</link><pubDate>Sun, 10 Aug 2025 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-act-runner-dind/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Symbolized Docker containers next to text &amp;#39;&amp;#39;rootless act_runner DinD&amp;#39;&amp;#39;"&#10; title="Giteas *act_runner* rootless ausführen" /&gt;&#10;&lt;p&gt;In diesem Artikel zeige ich, wie ich den &lt;em&gt;act_runner&lt;/em&gt; meiner &lt;em&gt;Gitea&lt;/em&gt;-Instanz von &lt;code&gt;gitea/act_runner&lt;/code&gt; auf &lt;code&gt;gitea/act_runner:latest-dind-rootless&lt;/code&gt; umstelle.&lt;/p&gt;&#10;&lt;p&gt;Warum dieser Aufwand? Weil &lt;code&gt;act_runner&lt;/code&gt; in &lt;em&gt;Docker&lt;/em&gt; läuft und Zugriff auf den Daemon per &lt;code&gt;/var/run/docker.sock&lt;/code&gt; als sogenanntes Volume (gemeint im Sinne von Datenträger) bzw. Bind-Mount benötigt, um zu funktionieren. Der Besitzer dieses Sockets ist &lt;code&gt;root&lt;/code&gt;, wodurch der Container praktisch Vollzugriff auf das Hostsystem erhält.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;RULE #1 - Do not expose the Docker daemon socket (even to the containers)&amp;rdquo; - OWASP / Docker Security Cheat Sheet&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Dies ist hoch riskant, denn es liegt im Prinzip des Runners, von Dritten erzeugten Job-Code auszuführen. Ein offenes Einfallstor. In Verbindung mit direktem Durchgriff auf das Hostsystem kann das im Falle eines erfolgreichen Angriffes einen Totalausfall bzw. die &amp;ldquo;feindliche Übernahme&amp;rdquo; meiner Infrastruktur bedeuten.&lt;/p&gt;&#10;&lt;h2 id="gefahr-durch-dockersock"&gt;Gefahr durch &lt;code&gt;docker.sock&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Ich richte mich in dieser Anleitung nach der Empfehlung von &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#docker-security-cheat-sheet" target="_blank" rel="noopener noreferrer" class="external-link"&gt;OWASP zum Thema Docker-Sicherheit&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und wende deren Regel(n) in der Praxis an. OWASP steht für &amp;ldquo;Open Web Application Security Project&amp;rdquo;. Es ist eine Organisation, die sich für höhere Sicherheit von Webanwendungen einsetzt und dabei Anwender wie mich mit kostenfreien Artikeln, Dokumentation und Technologie unterstützt.&lt;/p&gt;&#10;&lt;p&gt;Die unter &lt;strong&gt;Regel 1&lt;/strong&gt; beschriebenen Maßnahmen lauten:&lt;/p&gt;&#10;&lt;h3 id="dockers-tcp-socket-abgeschaltet-lassen"&gt;Dockers &lt;em&gt;tcp&lt;/em&gt; Socket abgeschaltet lassen&lt;/h3&gt;&#10;&lt;p&gt;Sollte der Zugang zum &lt;em&gt;Docker Daemon&lt;/em&gt; per &lt;em&gt;tcp&lt;/em&gt; eingeschaltet sein, kann sich mit diesem - sofern keine weiteren Vorkehrungen getroffen wurden - über eine ungesicherte Verbindung und ohne Authentisierung verbunden werden. Der Daemon ist dann durch praktisch jeden Internetnutzer zugänglich und damit angreifbar.&lt;/p&gt;&#10;&lt;p&gt;Wie stelle ich also sicher, dass der &lt;em&gt;tcp&lt;/em&gt; Socket abgeschaltet ist?&#10;Die Anleitung hierzu findet sich in den &lt;a href="https://docs.docker.com/engine/daemon/remote-access/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker Docs&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und muss quasi verkehrt herum angewandt werden. Auch dort wird ausdrücklich vor ungeschützter Verfügbarmachung des &lt;em&gt;tcp&lt;/em&gt;-Sockets gewarnt.&lt;/p&gt;&#10;&lt;p&gt;Nach Ausführen aller erforderlichen Schritte lasse ich &lt;a href="https://en.wikipedia.org/wiki/Netstat" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;netstat&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; einmal durchlaufen. Es listet mir offene Sockets, Netzwerkschnittstellen und Routing-Tabellen auf:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# check if Docker Daemon&amp;#39;s &amp;#34;dockerd&amp;#34; tcp socket is exposed&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# netstat -lntp | grep dockerd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# test is a pass if this command does not return anything.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei mir findet sich kein Eintrag mit &lt;code&gt;dockerd&lt;/code&gt;. Mein Server ist an dieser Stelle schon einmal nicht verwundbar.&lt;/p&gt;&#10;&lt;h3 id="den-docker-socket-varrundockersock-nicht-in-andere-container-einbinden"&gt;Den Docker Socket &lt;code&gt;/var/run/docker.sock&lt;/code&gt; nicht in andere Container einbinden&lt;/h3&gt;&#10;&lt;p&gt;Hier wird es schon etwas schwieriger: &lt;em&gt;act_runner&lt;/em&gt; benötigt den Socket, um Job-Container zu erzeugen, zu verwalten und schließlich wieder zu entsorgen. Ohne Zugriff auf den &lt;em&gt;Docker Daemon&lt;/em&gt; über den Socket funktioniert die Build-Pipeline schlicht und einfach nicht - es sei denn, man möchte komplett auf &lt;em&gt;Docker&lt;/em&gt; verzichten und sowohl den Runner als auch die Build-Jobs direkt auf der Host-Maschine laufen lassen. Damit gingen aber viele Nachteile einher: Verlust der Kapselung, keine Portabilität, schlechte Skalierbarkeit, geringere Sicherheit&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;Aber auch für die Option mit &lt;em&gt;Docker&lt;/em&gt; gibt es eine Lösung: &lt;a href="https://gitea.com/gitea/act_runner/src/branch/main/examples/docker-compose#running-act_runner-using-docker-in-docker-dind" target="_blank" rel="noopener noreferrer" class="external-link"&gt;act_runner Docker-in-Docker aufsetzen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Mit diesem Setup erhält &lt;em&gt;act_runner&lt;/em&gt; einen eigenen &lt;em&gt;Docker Daemon&lt;/em&gt;, jedoch mit eingeschränkten Rechten und ohne Zugriff auf den des Host-Systems. Dieser übernimmt dann den Lebenszyklus von Job-Containern, sodass sie komplett unabhängig vom Host-System laufen.&lt;/p&gt;&#10;&lt;h2 id="docker-in-docker"&gt;Docker in Docker&lt;/h2&gt;&#10;&lt;p&gt;Folgendes Schaubild verdeutlicht den Unterschied:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-runner-dind-setup.avif" alt="Image: act_runner with standard configuration versus DinD-rootless. The latter has an isolated Docker Daemon running within the act_runner container." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="act_runner-rootless-starten"&gt;Act_runner &amp;ldquo;rootless&amp;rdquo; starten&lt;/h3&gt;&#10;&lt;p&gt;Also folge ich der Anleitung und kopiere mir eine passende &lt;code&gt;docker-compose.yml&lt;/code&gt; zusammen. Wichtig hier:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Es muss &lt;code&gt;privileged: true&lt;/code&gt; gesetzt werden. Ansonsten kann der Docker Daemon im &lt;em&gt;act_runner&lt;/em&gt;-Container nicht richtig starten, da ihm Kernel-Funktionen fehlen. Somit schmiert der ganze Container wieder und wieder ab, ohne dabei hilfreiche Fehlermeldungen zu erzeugen.&lt;/li&gt;&#10;&lt;li&gt;Die Umgebungsvariable &lt;code&gt;DOCKER_HOST=unix:///var/run/user/1000/docker.sock&lt;/code&gt; muss gesetzt sein. Hier wird der Docker Socket über einen nicht-privilegierten User gesteuert und steht dem Runner für die Verwaltung von Job Containern zur Verfügung.&#10;Der Daemon läuft gekapselt im Container und steht nicht in Verbindung mit der Host-Maschine.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-08-10-gitea-act-runner-dind-rootless.avif" alt="Image: DinD-rootless runner is now working fine" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="volume-verwirrung"&gt;Volume-Verwirrung&lt;/h3&gt;&#10;&lt;p&gt;Prima! Da der Runner nun läuft, lasse ich ihn direkt mal auf einen Job los. Leider schlägt das Bauen bereits nach Sekundenbruchteilen mit dieser Meldung fehl:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Runner step: Set up job&#10;failed to start container: Error response from daemon: error while creating mount source path &amp;#39;&amp;lt;volumeSourceFullPath&amp;gt;&amp;#39;: mkdir &amp;lt;volumeSourcePath&amp;gt;: permission denied&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hier habe ich stundenlang recherchieren müssen und war lange der falschen Überzeugung, dass es an fehlenden Berechtigungen bei Ordnern auf der Host-Maschine liegt. Später erst verstand ich wirklich, dass Docker-in-Docker genau so gemeint ist, wie es heißt: Nicht nur werden Container durch Container erzeugt, sondern innerhalb des Containers läuft ein separater Docker Daemon!&lt;/p&gt;&#10;&lt;p&gt;Damit funktioniert nämlich der klassische Weg nicht mehr, Volumes in Job Containern direkt vom Host-System per &lt;code&gt;-v /a/b:/x/y&lt;/code&gt; verfügbar zu machen.&#10;Stattdessen müssen Volumes nun durchgereicht werden. Beispiel:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;em&gt;host&lt;/em&gt; directory &lt;code&gt;/opt/server/www/blog-artifacts&lt;/code&gt; -&amp;gt; &lt;em&gt;act_runner&lt;/em&gt; volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt; &amp;ndash;&amp;gt; &lt;em&gt;job&lt;/em&gt; container volume &lt;code&gt;/tmp/blog-artifacts&lt;/code&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Das &lt;code&gt;:z&lt;/code&gt; im Volume vom &lt;em&gt;act_runner&lt;/em&gt; wird jetzt wichtig. Es zeigt &lt;em&gt;Docker&lt;/em&gt; gegenüber an, dass dieses Volume zwischen Containern geteilt wird. Diese Volumes müssen nicht nur in der &lt;code&gt;docker-compose.yml&lt;/code&gt; von &lt;em&gt;act_runner&lt;/em&gt; angegeben werden, sondern auch die Job-Skripte im Ordner &lt;code&gt;.gitea/workflows/&lt;/code&gt; sind jetzt entsprechend anzupassen. Hier wird das &lt;code&gt;:z&lt;/code&gt; auf der &amp;ldquo;rechten Seite&amp;rdquo; hingegen nicht benötigt.&lt;/p&gt;&#10;&lt;h3 id="is-not-a-valid-volume"&gt;&amp;ldquo;is not a valid volume&amp;rdquo;&lt;/h3&gt;&#10;&lt;p&gt;Allem Aufwand zum Trotz laufen meine Jobs noch immer nicht durch. Dieses Mal wegen einer Fehlermeldung, die &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#action-volumes"&gt;mir bereits bekannt vorkam&lt;/a&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log*" data-lang="log*"&gt;# Runner step: Set up job&#10;[/tmp/blog-artifacts] is not a valid volume, will be ignored&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Also rein in die &lt;code&gt;config.yml&lt;/code&gt; von &lt;em&gt;act_runner&lt;/em&gt; und die Volume-Namen hinzufügen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /gitea/runner/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;valid_volumes&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/blog-artifacts&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;/tmp/lectures-artifacts&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In dieser Datei kann ich &lt;code&gt;privileged: false&lt;/code&gt; stehen lassen, denn der Job Container selbst benötigt anders als &lt;em&gt;act_runner&lt;/em&gt; keine Kernel-Features.&lt;/p&gt;&#10;&lt;h2 id="berechtigungen-korrekt-setzen"&gt;Berechtigungen korrekt setzen&lt;/h2&gt;&#10;&lt;p&gt;Nun erhalte ich erneut &lt;code&gt;Permission Denied&lt;/code&gt; Fehlermeldungen beim Ausführen meiner Jobs, wenn auch nicht direkt im ersten Schritt der Aktionen. Da ich die Volume-Pfade jetzt bis ins Kleinste geprüft habe, kann es eigentlich nur noch an den Ordnerberechtigungen auf der Host-Maschine liegen.&lt;/p&gt;&#10;&lt;p&gt;Damit die vom Job-Container erzeugten Artefakte durch die Volumes auf meinem Host abgelegt werden können, muss ich das zu schreibende Verzeichnis und alle Unterordner &lt;code&gt;-R&lt;/code&gt; an den vorher definierten, nicht-privilegierten User &lt;code&gt;ID=1000&lt;/code&gt; übergeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# chown -R 1000:1000 /target/path/to/artifact/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Endlich funkioniert alles reibungslos und ich habe der (unwahrscheinlichen, aber möglichen) Übernahme meines Host-Systems durch bösartige Job-Container einen Riegel vorgeschoben.&lt;/p&gt;&#10;</description></item><item><title>Rate limiter für Gitea</title><link>https://blog.schallbert.de/gitea-rate-limiter/</link><pubDate>Fri, 25 Jul 2025 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-rate-limiter/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-25-gitea-rate-limiter-thumb.avif"&#10; class="post-cover"&#10; alt="Image: Gitea&amp;#39;&amp;#39;s logo, a cup of tea, protected by many arrows symbolizing GET requests"&#10; title="Rate limiter für Gitea" /&gt;&#10;&lt;p&gt;Hier halte ich eine Schritt-für-Schritt Anleitung fest, die das Problem meiner &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;&lt;em&gt;Gitea&lt;/em&gt;-Abstürze&lt;/a&gt; lösen soll. Die Erfahrung aus der Erstanwendung eines Rate Limiters &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;für meinen Blog&lt;/a&gt; lasse ich hier einfließen mit dem Ziel, dass die Anwendung robuster läuft und gegen ungerichtete Denial of Service Attacken geschützt wird.&lt;/p&gt;&#10;&lt;h2 id="schritt-1-log-quelle-für-rate-limiter-festlegen"&gt;Schritt 1: Log-Quelle für rate limiter festlegen&lt;/h2&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; erzeugt natürlich selbst Logs, theoretisch bis herunter auf Zugriffsebene durch externe Clients. Doch schaffte ich es in der Vergangenheit nicht, &lt;a href="https://blog.schallbert.de/server-protection/#was-noch-nicht-funktioniert-gitea--fail2ban"&gt;&lt;em&gt;Giteas&lt;/em&gt; Access-Logs aus Docker auszuleiten&lt;/a&gt;. Daher geben wir jetzt dem als Reverse Proxy angeflanschten &lt;em&gt;Caddyserver&lt;/em&gt; die Anweisung, Logs stellvertretend zu erstellen. In ihnen tauchen dann Zugriffe auf das Webinterface von &lt;em&gt;Gitea&lt;/em&gt; auf.&lt;/p&gt;&#10;&lt;p&gt;In der &lt;code&gt;Caddyfile&lt;/code&gt; sieht das Log-Modul total unspektakulär aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="internen-traffic-vom-log-ausschließen"&gt;Internen Traffic vom Log ausschließen&lt;/h3&gt;&#10;&lt;p&gt;Nun schaue ich die Logs mal an und sehe Einträge, die ich gar nicht haben will. So erzeugt &lt;em&gt;act_runner&lt;/em&gt; zum Beispiel alle zwei Sekunden einen &lt;code&gt;Fetch Task POST&lt;/code&gt; nach &lt;em&gt;Gitea&lt;/em&gt; und alle zehn Sekunden einen &lt;code&gt;GET&lt;/code&gt; Request mit demselben Ziel als &lt;a href="https://blog.schallbert.de/fix-gitea-runner/"&gt;Health check&lt;/a&gt;. Diese brauchen für mich im Log gar nicht erst aufzutauchen. Mein erster Gedanke hier war, einfach Anfragen von internen IP-Adressen nicht zu loggen. Da &lt;em&gt;Caddy&lt;/em&gt; allerdings als Reverse Proxy fungiert, laufen ausnahmslos alle IP-Adressen &amp;ldquo;intern&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Also muss ich &lt;em&gt;act_runner&lt;/em&gt; spezifische Logs anders erkennen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable logging for fail2ban, don&amp;#39;t log for runner&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/api/&lt;/span&gt;actions&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output file &lt;span style="color:#e6db74"&gt;/log/&lt;/span&gt;gitea&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;log_skip&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Direktive weist &lt;em&gt;caddy&lt;/em&gt; nun an, keine Logs für Zugriffe auf &lt;code&gt;/api/actions*&lt;/code&gt; mehr anzulegen.&lt;/p&gt;&#10;&lt;h3 id="reverse-proxy-remote-ips-anzeigen"&gt;Reverse-Proxy: Remote IPs anzeigen&lt;/h3&gt;&#10;&lt;p&gt;Dennoch habe ich noch immer ein Problem: Wenn im Log alle externen Anfragen auf einer internen IP-Adresse hereinkommen, wie soll ich &amp;ldquo;böse&amp;rdquo; Anfragen dann wegblocken? Eine Websuche &lt;a href="https://caddy.community/t/how-to-get-a-true-remote-ip-behind-caddy-reverse-proxy/22348/2" target="_blank" rel="noopener noreferrer" class="external-link"&gt;zeigt, dass dies für Reverse Proxy in Docker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ein üblicher Stolperstein ist. Leider helfen mir viele Lösungen nicht, da sie andere Serversoftware wie &lt;em&gt;nginx&lt;/em&gt; verwenden oder andere Services hinter ihren Proxies laufen haben als &lt;em&gt;Gitea.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;Doch tatsächlich ist es ganz einfach: Nur eine Zeile muss an der richtigen Stelle in die &lt;code&gt;Caddyfile&lt;/code&gt; eingefügt werden und schon kommen die remote IP-Adressen unverfälscht rein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git&lt;span style="color:#f92672"&gt;.&lt;/span&gt;schallbert&lt;span style="color:#f92672"&gt;.&lt;/span&gt;de {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reverse_proxy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#e6db74"&gt;http&lt;/span&gt;:&lt;span style="color:#e6db74"&gt;//&lt;/span&gt;&lt;span style="color:#e6db74"&gt;gitea&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;3000&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trusted_proxies &lt;span style="color:#ae81ff"&gt;172&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Docker-internal netwock traffic runs with these IPs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...] &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die Direktive &lt;a href="https://caddyserver.com/docs/caddyfile/options#trusted-proxies" target="_blank" rel="noopener noreferrer" class="external-link"&gt;&lt;code&gt;trusted_proxies&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; teilt &lt;em&gt;Caddy&lt;/em&gt; mit, dass dem von &lt;em&gt;Docker&lt;/em&gt; bereitgestelltem Netzwerk vertraut werden kann. Somit wird die eigentliche Quell-IP angezeigt statt der internen Schnittstelladresse des Containers. Genau das brauche ich, um die Adressen später mit &lt;em&gt;fail2ban&lt;/em&gt; analysieren zu können.&lt;/p&gt;&#10;&lt;h2 id="schritt-2-anwendungsfall-bestimmen"&gt;Schritt 2: Anwendungsfall bestimmen&lt;/h2&gt;&#10;&lt;p&gt;Schauen wir uns an, wie viele HTTP &lt;code&gt;200 ok&lt;/code&gt; Anfragen im Grenzfall zwischen Normalnutzung und &amp;ldquo;abuse&amp;rdquo; kommen. Dazu surfe ich auf &lt;em&gt;Gitea&lt;/em&gt; herum und klicke einen Haufen Dinge an, wie ich das als Mensch sonst nie in der Geschwindigkeit tun würde. Danach mache ich eine Auswertung der Logs.&lt;/p&gt;&#10;&lt;p&gt;Damit haben wir erste Richtwerte für den Rate Limiter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# rate limiter tests&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="schritt-3-fail2ban-konfigurieren"&gt;Schritt 3: Fail2ban konfigurieren&lt;/h2&gt;&#10;&lt;p&gt;Anschließend konfigurieren wir Filter und Jail-Datei von &lt;code&gt;fail2ban&lt;/code&gt; so, dass sie die oben definierten Logs analysieren.&lt;/p&gt;&#10;&lt;h3 id="filter"&gt;Filter&lt;/h3&gt;&#10;&lt;p&gt;Ich verwende hier schlicht denselben Filter für meinen Rate-Limiter aus dem &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#ordner-filterd"&gt;vorherigen Artikel&lt;/a&gt; wieder. Uns interessieren nur erfolgreiche Anfragen, die wir innerhalb eines Zeitfensters zählen.&lt;/p&gt;&#10;&lt;h3 id="jail"&gt;Jail&lt;/h3&gt;&#10;&lt;p&gt;Ich verwende die Werte aus den rate limiter Tests 1:1 hier in der Jail-Datei weiter. Als Filter verweise ich auf &lt;code&gt;caddy-ratelimit&lt;/code&gt; (Link oben).&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/config/fail2ban/jail.local&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;gitea-ratelimit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-ratelimit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = /var/log/caddy2/gitea/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit einem Neustart von &lt;em&gt;fail2ban&lt;/em&gt; schalte ich den Schutz scharf.&lt;/p&gt;&#10;&lt;h2 id="schritt-4-rate-limiter-testen"&gt;Schritt 4: Rate limiter testen&lt;/h2&gt;&#10;&lt;p&gt;Hier gehe ich genauso vor wie in meinem Artikel &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/#test-rate-limiter"&gt;fail2ban with caddy&lt;/a&gt; und komme zum selben Ergebnis. Es funktionert!&lt;/p&gt;&#10;&lt;h2 id="-update-oktober-2025"&gt;⚠️ Update Oktober 2025&lt;/h2&gt;&#10;&lt;p&gt;Wird &lt;em&gt;fail2ban&lt;/em&gt; auf Services in Docker-Containern angewendet, kann mit der oben genannten Konfiguration des &lt;em&gt;jails&lt;/em&gt; Gein Fehler auftreten. Die Behebung habe ich im folgenden Artikel dokumentiert: &lt;a href="https://blog.schallbert.de/fail2ban-error-configuration-bad-value/"&gt;&lt;em&gt;fail2ban&lt;/em&gt;-Fehler &amp;ldquo;Bad value substitution&amp;rdquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt;&lt;/p&gt;&#10;</description></item><item><title>Rate limiter mit Caddy und fail2ban</title><link>https://blog.schallbert.de/fail2ban-with-caddy/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/fail2ban-with-caddy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit-thumb.avif"&#10; class="post-cover"&#10; alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog"&#10; title="Rate limiter mit Caddy und fail2ban" /&gt;&#10;&lt;p&gt;Hier beschreibe ich, wie man das meist zur &lt;a href="https://blog.schallbert.de/server-protection/"&gt;Abwehr unerlaubter Zugriffsversuche&lt;/a&gt; eingesetze Werkzeug &lt;em&gt;fail2ban&lt;/em&gt; auch zur Begrenzung der Anzahl von erfolgreichen Zugriffen innerhalb eines definierten Zeitfensters verwenden kann. Dies nennt man &amp;ldquo;rate limiting&amp;rdquo; und soll denial of service Attacken vereiteln, bei denen &lt;a href="https://blog.schallbert.de/gitea-out-of-memory/"&gt;mein Server überlastet&lt;/a&gt; wird.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-fail2ban-ratelimit.avif" alt="Image: block drawing of how caddy interacts with fail2ban to rate limit accesses to my blog" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="caddy-logs-einschalten"&gt;Caddy-Logs einschalten&lt;/h2&gt;&#10;&lt;p&gt;Zuerst müssen wir den Logger des Webservers einschalten. Bei Caddy geht das mit wenigen Zeilen Code, die ich meiner &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#caddy"&gt;bestehenden Konfiguration&lt;/a&gt; hinzufüge:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;blog.schallbert.de {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Define webserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;root * /www/blog&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;encode gzip&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;file_server&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Enable logging for fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;log {&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;output file /log/blog/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ab jetzt werden im angegebenen Pfad Logs angelegt, die ich mit einer &lt;a href="https://blog.schallbert.de/logrotate-mistake/"&gt;&lt;em&gt;logrotate&lt;/em&gt;-Konfiguration&lt;/a&gt; verwalte.&lt;/p&gt;&#10;&lt;h2 id="fail2ban-als-rate-limiter-konfigurieren"&gt;Fail2ban als Rate Limiter konfigurieren&lt;/h2&gt;&#10;&lt;p&gt;Nochmal kurz zur Wiederholung: &lt;em&gt;fail2ban&lt;/em&gt; greift auf die Paketfilter-Regeln der Maschine zu und modifiziert sozusagen ihre Firewall, um dynamische Angriffe abzuwehren. Damit dies funktioniert, müssen &lt;em&gt;fail2ban&lt;/em&gt; Logdateien zur Verfügung gestellt werden, die eine Host-Applikation wie beispielsweise ein Webserver erzeugt und IP-Adressen der Client-Rechner enthalten.&lt;/p&gt;&#10;&lt;h3 id="vorüberlegung-http-200-ok-als-filter"&gt;Vorüberlegung: &amp;ldquo;http 200 OK&amp;rdquo; als Filter?&lt;/h3&gt;&#10;&lt;p&gt;Auch normale, zulässige Zugriffe &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200" target="_blank" rel="noopener noreferrer" class="external-link"&gt;200 OK&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; müssen für meinen Rate Limiter zählen.&lt;/p&gt;&#10;&lt;p&gt;Um ein Gefühl zu bekommen wie der Logger Zugriffe aufzeichnet, werfe ich einen Blick in die Logs. Ich lasse mir die Anzahl Einträge zurückgeben, die einen Status &lt;code&gt;200 OK&lt;/code&gt; enthalten. Dazu führe ich mit &lt;em&gt;grep&lt;/em&gt; eine Suche durch und lasse mir die Anzahl Treffer zeilenweise per &lt;code&gt;wc -l&lt;/code&gt; ausrechnen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;847&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gut, es hat heute also bereits 847 Zugriffe gegeben, die &lt;code&gt;200 OK&lt;/code&gt; zurückgemeldet bekommen haben. Nun rufe ich selbst mal die Landing page meiner Blogseite auf und lasse mir dann die Artikelübersicht anzeigen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;858&lt;/span&gt; // after calling blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rschallbert@machine:/var/log/caddy2/blog# grep -o &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#34;status&amp;#34;:200&amp;#39;&lt;/span&gt; access.log | wc -l&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;882&lt;/span&gt; // page loaded after clicking &lt;span style="color:#e6db74"&gt;&amp;#34;Posts&amp;#34;&lt;/span&gt; button&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whow, das waren 35 Einträge für zwei Klicks! Wenn ich mir das Log so ansehe, dann erklären sich die vielen Zeilen mit Assets - sprich Bildern und Logos, die geladen werden.&lt;/p&gt;&#10;&lt;p&gt;Damit ist leider klar, dass das Filterkriterium &lt;code&gt;status:200&lt;/code&gt; für meinen Rate Limiter nicht ohne Weiteres funktionieren kann. Denn die Anzahl Meldungen hängen maßgeblich von dem jeweiligen Artikel ab. Ich müsste also eine Grenze definieren, über der kein normaler Mensch Zugriffe auf mein Blog erzeugt.&lt;/p&gt;&#10;&lt;p&gt;Um nur &amp;ldquo;echte&amp;rdquo; Zugriffe auf meine Seiten zu zählen, muss ich die Assets in den Logs irgendwie ausschließen. Glücklicherweise gibt es dafür eine einfache &lt;a href="https://caddyserver.com/docs/caddyfile/directives/log_skip" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Direktive in Caddy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# caddy2/config/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enable logging for fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log_skip &lt;span style="color:#e6db74"&gt;/assets*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; log {&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; output file /&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;blog&lt;span style="color:#f92672"&gt;/&lt;/span&gt;access&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;log_skip&lt;/code&gt; sorgt nun dafür, dass alle Zugriffe auf Dateien im &lt;code&gt;assets/&lt;/code&gt;-Ordner und darunter nicht mitgeloggt werden.&lt;br&gt;&#10;Das Verhalten von Fail2ban wird mittels zweier Konfigurationsdateien festgelegt:&lt;/p&gt;&#10;&lt;h3 id="ordner-filterd"&gt;Ordner &lt;code&gt;filter.d&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;filter.d/&amp;lt;filter-name&amp;gt;.conf&lt;/code&gt; beinhaltet die Definition eines zu betrachtenden Ereignisses. So kann die &lt;code&gt;failregex&lt;/code&gt; ein &amp;ldquo;Seite nicht gefunden&amp;rdquo; &lt;code&gt;http 404&lt;/code&gt; Statuscode ebenso zum Auslösen des Paketfilters verwendet werden wie der erfolgreiche Zugriff für den Aufbau meines Rate Limiter, welcher auf &lt;code&gt;200 OK&lt;/code&gt; reagiert. Abgeschrieben habe ich für die Regex bei &lt;a href="https://www.kassner.com.br/en/2023/09/10/fail2ban-caddy-json-logs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rafael Kassner&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/config/filter.d/caddy-ratelimit.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;Definition]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;datepattern = \d+&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Außerdem werden in der Datei weitere Randbedingungen bestimmt, z.B. das Datumsformat &lt;code&gt;datepattern&lt;/code&gt; an den Log-Output des zu schützenden Systems angepasst.&lt;/p&gt;&#10;&lt;h3 id="datei-jaillocal"&gt;Datei &lt;code&gt;jail.local&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;jail.local&lt;/code&gt; bestimmt die Bedingungen, bei deren Eintritt der Paketfilter für die Client-IP aktiv wird und weitere Zugriffsversuche blockt. Für den Einsatz als Rate Limiter benötige ich folgende Felder:&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;findtime&lt;/code&gt; - das Zeitfenster, in welchem Angriffe gezählt werden, &lt;code&gt;maxretry&lt;/code&gt; ist die Anzahl zulässiger Versuche im Zeitfenster und &lt;code&gt;bantime&lt;/code&gt; ist die Zeit, für die geblockt wird. &lt;code&gt;ignoreip&lt;/code&gt; ist meist per Werkseinstellung auf die relevanten internen IP-Adressen vorkonfiguriert. In meinem Falle funkt &lt;em&gt;act_runner&lt;/em&gt; zum Beispiel an intern &lt;em&gt;gitea&lt;/em&gt; zur Abfrage, ob neue Automatisierungsaufgaben anliegen. Diese Zugriffe will ich auf keinen Fall behindern.&lt;/p&gt;&#10;&lt;p&gt;Das im Folgenden verwendete Beispiel für &lt;em&gt;fail2ban&lt;/em&gt; zeigt den Rate Limiter auf meinem Blog. Am Ende sind Dateien und Log-Einträge für &lt;em&gt;gitea&lt;/em&gt; im Wesentlichen gleich. Nur Filternamen und Parameter unterscheiden sich voneinander durch die unterschiedlichen Anforderungen an Webseite und DevOps-Platform.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/jail.local&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;caddy-ratelimit]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;enabled = true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;chain = DOCKER-USER&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;action = iptables-multiport&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;port = http,https&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;filter = caddy-ratelimit&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;logpath = /var/log/caddy2/blog/access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 10&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 6h&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sind diese beiden Dateien konfiguriert, kann man &lt;em&gt;fail2ban&lt;/em&gt; neu starten und sich die Logs anschauen. Wenn man noch Fehler in den Dateien hat, gibt &lt;em&gt;fail2ban&lt;/em&gt; folgendes aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ERROR Errors in jail &lt;span style="color:#e6db74"&gt;&amp;#39;caddy-ratelimit&amp;#39;&lt;/span&gt;. Skipping...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tritt dies auf, kann z.B. das Jail nicht dem Filter (muss gleichnamig sein!) nicht zugeordnet werden, eine Feld-Definition wie &lt;code&gt;maxretry&lt;/code&gt; ist falsch getippt oder ein Zahlenformat kann nicht gelesen werden. Wenn alles gut läuft, lautet der Eintrag:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Reading config files: /etc/fail2ban/filter.d/caddy-ratelimit.conf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="test-rate-limiter"&gt;Rate Limiter testen&lt;/h2&gt;&#10;&lt;p&gt;Nun möchte ich prüfen, ob alles wie erwartet funktioniert.&lt;/p&gt;&#10;&lt;h3 id="filter-regex-überprüfen"&gt;Filter-Regex überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Als ersten Schritt schaue ich auf den Filterbegriff und prüfe, ob er in den Logs zuverlässig gefunden wird. Praktischerweise hat &lt;em&gt;fail2ban&lt;/em&gt; hierfür ein passendes Werkzeug eingebaut: &lt;code&gt;fail2ban-regex &amp;lt;logfile&amp;gt; &amp;lt;filter&amp;gt;&lt;/code&gt;. Ich kann also schlicht die Filterdatei und ein Test-Logfile eingeben und sehe dann, ob ich Treffer bekomme.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;fail2ban-regex ./access.log /etc/fail2ban/filter.d/caddy-ratelimit.conf &#10;&#10;Running tests&#10;=============&#10;&#10;Use filter file : caddy-ratelimit, basedir: /etc/fail2ban&#10;Use datepattern : \d+ : \d+&#10;Use log file : ./access.log.2&#10;Use encoding : UTF-8&#10;&#10;Results&#10;=======&#10;&#10;Failregex: 2009 total&#10;|- #) [# of hits] regular expression&#10;| 1) [2009] &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:200&#10;`-&#10;&#10;Ignoreregex: 0 total&#10;&#10;Date template hits:&#10;|- [# of hits] date format&#10;| [2685] \d+&#10;`-&#10;&#10;Lines: 2685 lines, 0 ignored, 2009 matched, 676 missed&#10;[processed in 0.12 sec]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Quantitativ bekomme ich, was ich erwarte: das oben beschriebene Suchverfahren per &lt;em&gt;grep&lt;/em&gt; liefert mir exakt dieselben Ergebnisse.&lt;/p&gt;&#10;&lt;h3 id="check-jail"&gt;Jail überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Als nächsten Schritt muss ich testen, ob der Rate Limiter auch greift. Dafür simuliere ich mir Logeinträge. Die Jail-Regeln nehme ich für diesen Test stark zurück, sonst muss ich zu viele Eingaben in kurzer Zeit erzeugen.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /opt/fail2ban/jail.local TEST&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#ae81ff"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;findtime = 10s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;maxretry = 2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;bantime = 100s&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach einem Neustart von &lt;em&gt;fail2ban&lt;/em&gt; erzeuge ich mir eine zweite Konsole, wo ich Log-Einträge simulieren kann. Dabei müssen mindestens Zeitpunkt, IP-Adresse des Aufrufenden und Status enthalten sein; ich gehe aber auf Nummer sicher und nehme vollständige Einträge.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo &lt;span style="color:#e6db74"&gt;&amp;#39;{&amp;#34;ts&amp;#34;:1751246139,&amp;#34;remote_ip&amp;#34;:&amp;#34;1.1.1.1&amp;#34;,&amp;#34;status&amp;#34;:200,[superLongIrrelevantOtherStuffForFiltering]}&amp;#39;&lt;/span&gt; &amp;gt;&amp;gt; /var/log/caddy/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies gebe ich mehrfach ein, um über mein Limit von 2 retries zu kommen.&lt;/p&gt;&#10;&lt;p&gt;In der ersten Konsoleninstanz prüfe ich anschließend den &lt;em&gt;fail2ban&lt;/em&gt;-Status für den entsprechenden Filter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fail2ban-client status caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Total failed:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- File list:&#9;/var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Currently banned:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Total banned:&#9;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Banned IP list:&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das war nichts. Warum offenbart die fail2ban-Logdatei:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/config/log/fail2ban/fail2ban.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WARN &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignoring all log entries older than 20s; &lt;span style="color:#75715e"&gt;# probably messages generated within a fail2ban restart period&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Also nochmal mit passenden Zeitstempeln. Diese muss ich aus der aktuellen Systemzeit (&lt;code&gt;$(date +%s.%N)&lt;/code&gt;) ziehen, wenn ich sie ständig anpassen muss. Daher nehme ich eine vollständige Logzeile her, modifiziere sie auf die IP-Adresse &lt;code&gt;1.1.1.1&lt;/code&gt; und füge passende Zeitstempel ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;echo&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;{\&amp;#34;level\&amp;#34;:\&amp;#34;info\&amp;#34;,\&amp;#34;ts\&amp;#34;:$(date +%s.%N),\&amp;#34;logger\&amp;#34;:\&amp;#34;http.log.access.log0\&amp;#34;,\&amp;#34;msg\&amp;#34;:\&amp;#34;handled request\&amp;#34;,\&amp;#34;request\&amp;#34;:{\&amp;#34;remote_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;remote_port\&amp;#34;:\&amp;#34;38229\&amp;#34;,\&amp;#34;client_ip\&amp;#34;:\&amp;#34;1.1.1.1\&amp;#34;,\&amp;#34;proto\&amp;#34;:\&amp;#34;HTTP/1.1\&amp;#34;,\&amp;#34;method\&amp;#34;:\&amp;#34;GET\&amp;#34;,\&amp;#34;host\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;,\&amp;#34;uri\&amp;#34;:\&amp;#34;/vacuum-clamping/\&amp;#34;,\&amp;#34;headers\&amp;#34;:{\&amp;#34;Accept-Encoding\&amp;#34;:[\&amp;#34;gzip, deflate, br\&amp;#34;],\&amp;#34;Connection\&amp;#34;:[\&amp;#34;keep-alive\&amp;#34;],\&amp;#34;User-Agent\&amp;#34;:[\&amp;#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0\&amp;#34;],\&amp;#34;Accept\&amp;#34;:[\&amp;#34;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\&amp;#34;],\&amp;#34;Accept-Language\&amp;#34;:[\&amp;#34;en-US,en;q=0.5\&amp;#34;]},\&amp;#34;tls\&amp;#34;:{\&amp;#34;resumed\&amp;#34;:false,\&amp;#34;version\&amp;#34;:772,\&amp;#34;cipher_suite\&amp;#34;:4865,\&amp;#34;proto\&amp;#34;:\&amp;#34;http/1.1\&amp;#34;,\&amp;#34;server_name\&amp;#34;:\&amp;#34;blog.schallbert.de\&amp;#34;}},\&amp;#34;bytes_read\&amp;#34;:0,\&amp;#34;user_id\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;duration\&amp;#34;:0.001207354,\&amp;#34;size\&amp;#34;:12290,\&amp;#34;status\&amp;#34;:200,\&amp;#34;resp_headers\&amp;#34;:{\&amp;#34;Last-Modified\&amp;#34;:[\&amp;#34;Tue, 10 Jun 2025 19:33:01 GMT\&amp;#34;],\&amp;#34;Content-Encoding\&amp;#34;:[\&amp;#34;gzip\&amp;#34;],\&amp;#34;Server\&amp;#34;:[\&amp;#34;Caddy\&amp;#34;],\&amp;#34;Alt-Svc\&amp;#34;:[\&amp;#34;h3=\\\&amp;#34;:443\\\&amp;#34;; ma=2592000\&amp;#34;],\&amp;#34;Vary\&amp;#34;:[\&amp;#34;Accept-Encoding\&amp;#34;],\&amp;#34;Etag\&amp;#34;:[\&amp;#34;\\\&amp;#34;gzip\\\&amp;#34;\&amp;#34;],\&amp;#34;Content-Type\&amp;#34;:[\&amp;#34;text/html; charset=utf-8\&amp;#34;]}}&amp;#34;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#960050;background-color:#1e0010"&gt;access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nach mehrmaligem Aufruf dieses gebastelten Kommandos erhalte ich nun&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fail2ban-client status caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Status &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; the jail: caddy-ratelimit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;|- Filter&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Currently failed:&#9;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| |- Total failed:&#9;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;| &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- File list:&#9;/var/log/caddy2/blog/access.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Actions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Currently banned:&#9;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |- Total banned:&#9;&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;- Banned IP list:&#9;1.1.1.1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das sieht super aus. Auf zum letzten Schritt.&lt;/p&gt;&#10;&lt;h3 id="paketfilter-überprüfen"&gt;Paketfilter überprüfen&lt;/h3&gt;&#10;&lt;p&gt;Wenn wir nun einmal in die Logs schauen, müsste unser &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; Jail nun bei jeder &amp;ldquo;normalen&amp;rdquo; Anfrage triggern.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine: nano fail2ban/config/log/fail2ban/fail2ban.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP a&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP b&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Ignore 172.18.0.1 by ip&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP c&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP d&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-status&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP e&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;timestamp&amp;gt; &amp;lt;id&amp;gt; INFO &lt;span style="color:#f92672"&gt;[&lt;/span&gt;caddy-ratelimit&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Found &amp;lt;IP f&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Um die ganze Kette getestet zu haben müssen wir jetzt noch prüfen, ob die entsprechende IP-Adresse auch wirklich in der Hardware geblockt ist. Dafür gebe ich ein:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L | grep &lt;span style="color:#e6db74"&gt;&amp;#34;1.1.1.1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;REJECT all -- 1.1.1.1 0.0.0.0/0 reject-with icmp-port-unreachable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Scheint ja alles in Ordnung zu sein. Für einen letzten Test von einer externen IP-Adresse rufe ich im Browser einen &lt;a href="https://www.deadlinkchecker.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Link Checker&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf, der meine Webseite crawlen und damit den Rate Limiter auslösen soll. Obwohl dieser 44 Links in kürzester Zeit durchläuft, wird der Test als &amp;ldquo;bestanden&amp;rdquo; angezeigt. Sehr merkwürdig, &lt;code&gt;[caddy-ratelimit]&lt;/code&gt; hätte anschlagen müssen!&lt;/p&gt;&#10;&lt;h3 id="iptable-chain-input-statt-forward"&gt;iptable-chain INPUT statt FORWARD&lt;/h3&gt;&#10;&lt;p&gt;Verwirrt schaue ich mir mal die ganze &lt;code&gt;iptable&lt;/code&gt; an. Parallel suche ich mir die IP-Adresse des Link Checker heraus. Sie taucht in den &lt;code&gt;iptables&lt;/code&gt; auf. Und trotzdem wird offensichtlich nicht geblockt. Woran das liegt? Ein genauer Blick zeigt:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Chain INPUT (policy ACCEPT)&#10;target prot opt source destination &#10;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 22&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Problem hier ist, dass mein Jail auf der &lt;code&gt;INPUT&lt;/code&gt;-Chain liegt. Die Anfragen gehen aber doch nicht direkt an meine Server-Hardware, sondern werden über &lt;em&gt;Docker&lt;/em&gt; an &lt;em&gt;Caddyserver&lt;/em&gt; weitergeleitet. Um zu funktionieren, muss ich auf der &lt;code&gt;FORWARD&lt;/code&gt;-Chain landen, wo &lt;code&gt;DOCKER-USER&lt;/code&gt; bereits ist. Sehr merkwürdig, hatte ich doch in der &lt;code&gt;jail.local&lt;/code&gt; extra &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; angegeben. Irgend etwas muss diese Definition überschreiben.&lt;/p&gt;&#10;&lt;p&gt;Ein unscheinbarer &lt;a href="https://gist.github.com/Rankarusu/23a04ed587b05c6f2b701f2457a127b0?permalink_comment_id=5347631#gistcomment-5347631" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Forenpost&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zusammen mit dem Kommentar &lt;code&gt;multiport&lt;/code&gt; im iptables-Print bringt mich auf die Lösung: Die &lt;code&gt;action = iptables-multiport&lt;/code&gt; Direktive überschreibt mein &lt;code&gt;chain = DOCKER-USER&lt;/code&gt; Statement, denn in der zugehörigen Konfigurationsdatei &lt;code&gt;iptables.conf&lt;/code&gt; wird folgendes gesetzt: &lt;code&gt;chain = INPUT&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Also lösche ich den &lt;code&gt;action&lt;/code&gt; Eintrag einfach, sodass &lt;em&gt;fail2ban&lt;/em&gt; auf den Default für &lt;code&gt;DOCKER-USER&lt;/code&gt; zurückfällt: &lt;code&gt;multiport dports 80,443&lt;/code&gt;.&#10;Nun taucht der Jail-Name auch korrekt in &lt;code&gt;iptables&lt;/code&gt; auf.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;schallbert@machine:~# iptables -n -L&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Chain DOCKER-USER &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; references&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;target prot opt source destination &#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;f2b-caddy-ratelimit tcp -- 0.0.0.0/0 0.0.0.0/0 multiport dports 80,443&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="erfolg-genießen"&gt;Erfolg genießen&lt;/h2&gt;&#10;&lt;p&gt;Endlich zeigt ein weiterer Link Check, dass mein Rate Limiter funktioniert.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-07-10-deadlinkcheck-ratelimit-working.avif" alt="Image: deadlinkchecker view for blog.schallbert.de hits configured rate limit and gets blocked subsequentially. Thus it returns a Timeout" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="-update-oktober-2025"&gt;⚠️ Update Oktober 2025&lt;/h2&gt;&#10;&lt;p&gt;In Verbindung mit diesem Artikel gibt es ein wichtiges Update, welches den &lt;a href="https://blog.schallbert.de/fail2ban-error-configuration-bad-value/"&gt;&lt;em&gt;fail2ban&lt;/em&gt; Fehler &amp;ldquo;Bad value substitution&amp;rdquo; for &amp;lsquo;action&amp;rsquo;&lt;/a&gt; behebt. Dieses Problem kann auftauchen, wenn &lt;em&gt;fail2ban&lt;/em&gt; Services prüfen soll, die in Docker-Containern laufen.&lt;/p&gt;&#10;</description></item><item><title>Gitea stürzt ab: Zu viele Anfragen</title><link>https://blog.schallbert.de/gitea-out-of-memory/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/gitea-out-of-memory/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load-thumb.avif"&#10; class="post-cover"&#10; alt="Image: server at max load when gitea is flooded with GET requests"&#10; title="Gitea stürzt ab: Zu viele Anfragen" /&gt;&#10;&lt;p&gt;Hier möchte ich kurz darstellen, wie ein Absturz eines Dienstes meinen Server über Stunden wiederholt lahmgelegt hat. Und dies so gründlich, dass ich nur aus der Konsole des Anbieters überhaupt einen Neustart durchführen konnte. Ich erkläre, wie es dazu kam und wie ich dieses und ähnliche Probleme in Zukunft vermeiden möchte.&lt;/p&gt;&#10;&lt;h2 id="ich-wurde-angegriffen-oder"&gt;Ich wurde angegriffen. Oder?&lt;/h2&gt;&#10;&lt;p&gt;Ich saß gerade an einem Artikel, den ich später posten wollte. Zur Sicherheit erstellte ich wie üblich einen Commit und wollte ihn auf meine &lt;em&gt;Gitea&lt;/em&gt;-Instanz schieben. Doch mein &lt;code&gt;git push&lt;/code&gt; Befehl lief einfach nicht durch.&lt;/p&gt;&#10;&lt;p&gt;Verwundert versuchte ich, meine Webseite aufzurufen. Zurück kam:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-404-message.avif" alt="Image: A browser&amp;#39;s timeout error message `Could not complete your request`" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Merkwürdig. Anschließend wollte ich mich auf meinem Server einloggen, um nach dem Rechten zu sehen: &lt;code&gt;ssh &amp;lt;servername&amp;gt;&lt;/code&gt;. Wieder blieb das Terminal ohne Reaktion. Mist!&lt;/p&gt;&#10;&lt;p&gt;Timeout. Als letzte Rettung meldete ich mich bei meinem Hoster an und schaute auf die Graphen des Servers:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-server-load.avif" alt="Image: 200% CPU load on my server for nearly two hours" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Oh, was ist denn da los? Nun versuche ich über die Weboberfläche, den Server herunterzufahren. Klappt auch nicht. Erst ein harter Neustart ist erfolgreich.&#10;Ich kann mich wieder per SSH einloggen und sehe, dass auch alle &lt;em&gt;Docker&lt;/em&gt;-Container wieder ganz normal hochfahren.&lt;/p&gt;&#10;&lt;h2 id="was-war-passiert"&gt;Was war passiert?&lt;/h2&gt;&#10;&lt;p&gt;Gut, dass ich eine Woche lang alle Logs aufbewahre. So und über die Auslastung der Maschine über Zeit kann ich einigermaßen rekonstruieren, was passiert ist.&lt;/p&gt;&#10;&lt;h3 id="welche-logs-helfen"&gt;Welche Logs helfen?&lt;/h3&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Crash lokalisieren! Auslösende Anwendung finden im &lt;code&gt;kern.log&lt;/code&gt;, Zeitstempel notieren.&lt;/li&gt;&#10;&lt;li&gt;Gibt es systemweite Auswirkungen oder andere Dienste, die beeinträchtigt werden? Im &lt;code&gt;syslog&lt;/code&gt; nachsehen.&lt;/li&gt;&#10;&lt;li&gt;Falls der Verdacht aufkommt, das System sei möglicherweise gehackt worden: &lt;code&gt;auth.log&lt;/code&gt; hat die Details.&lt;/li&gt;&#10;&lt;li&gt;Sollte die betroffene Anwendung im Container laufen, helfen entsprechende Logs dort möglicherweise weiter.&lt;/li&gt;&#10;&lt;li&gt;Logs der Anwendung selbst sichten. Besonders die Zeit kurz vor dem Crash ist interessant.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Und hier nochmal im Detail die Logs, die ich mir für meine Verhalten angesehen habe und wo sie zu finden sind.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;log name&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;purpose&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;relevant content for this issue&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/syslog&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;System-wide (bare metal) messages&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;(docker) warning: &amp;ldquo;health check for container &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; timeout&amp;rdquo; (containerd) error: &amp;ldquo;ttrpc: received message on inactive stream&amp;rdquo;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/auth.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Contains authentication messages from external hosts, in my case mostly SSH&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;None&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;/var/log/kern.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Logs all app, service, daemon and system crashes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Out of memory: Killed process &lt;code&gt;&amp;lt;ID&amp;gt;&lt;/code&gt; (&lt;code&gt;&amp;lt;serviceName&amp;gt;&lt;/code&gt;)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;[.]/gitea/log/gitea.log&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Protocol for requests, actions on Gitea&amp;rsquo;s web interface, repo changes etc.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;GET&lt;/code&gt; requests, crash/restart indications&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;docker container logs &lt;code&gt;&amp;lt;containerID&amp;gt;&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Docker&amp;rsquo;s logs for the container in question&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Received signal 15; terminating. (SIGTERM)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="kernlogsyslog"&gt;kern.log/syslog&lt;/h3&gt;&#10;&lt;p&gt;Hier sehe ich neben dem Stack trace, was genau passiert ist. Die am besten verständlichste Meldung ist folgende:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Out of memory: Killed process `&amp;lt;ID&amp;gt;` (gitea)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Gitea ist also der Stecker gezogen worden, weil es praktisch alle Systemressourcen gezogen hatte.&#10;Wenn ich im Log nach oben scrolle, sehe ich ein paar Minuten vorher:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;warning msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Health check for container &amp;lt;ID&amp;gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;timestamp&amp;gt; &amp;lt;machineName&amp;gt; dockerd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;716&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;error msg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;stream copy error: reading from a closed fifo&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bereits hier sehe ich Warnungen, dass der gitea-Container nicht sauber arbeitet. Ein weiterer Vorteil, den das Einbauen von &amp;ldquo;Healthchecks&amp;rdquo; in der &lt;code&gt;docker-compose.yml&lt;/code&gt; Datei hat. Über Zweck und Einbau von Healthchecks in &lt;em&gt;docker&lt;/em&gt; hatte ich aus anderen Gründen bereits &lt;a href="https://blog.schallbert.de/fix-gitea-runner/#healthcheck"&gt;einen Artikel verfasst&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="gitealog"&gt;gitea.log&lt;/h3&gt;&#10;&lt;p&gt;Folgender Log-Eintrag zeigt wegen eines &lt;a href="https://github.com/go-gitea/gitea/issues/22275" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bugs in einem Submodul&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; indirekt an, dass &lt;em&gt;Gitea&lt;/em&gt; gerade neu gestartet ist:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cmd/web.go:205:serveInstalled&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;W&lt;span style="color:#f92672"&gt;]&lt;/span&gt; Table system_setting Column version db default is , struct default is &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und weiter oben im Log nun:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-attack-log.avif" alt="Image: Gitea log showing numerous GET request entries, many of which for a specific large file (MiB range), following a typical Gitea startup message" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Sehr interessant. Die &lt;code&gt;highlight.css&lt;/code&gt; liegt in meinem öffentlichen Repo. Auf ihrer Grundlage erhält die Seite &lt;a href="https://lectures.schallbert.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lectures.schallbert.de&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ihr Aussehen. Diese Datei ist ziemlich groß, fast &lt;code&gt;1MiB&lt;/code&gt;. Und sie wird hier gleich zig male geladen, und das praktisch für jeden Commit.&lt;/p&gt;&#10;&lt;p&gt;Nun schaue ich mir weitere Crashes in der Vergangenheit an. Immer sind es bursts von &lt;code&gt;GET&lt;/code&gt;-Befehlen auf große Dateien oder die Anforderung von &lt;code&gt;compares&lt;/code&gt; zwischen zwei Branches des Repository, die einem Aufhängen meines Servers vorausgehen.&lt;/p&gt;&#10;&lt;h2 id="wer-steckt-dahinter"&gt;Wer steckt dahinter?&lt;/h2&gt;&#10;&lt;p&gt;Alle crash-auslösenden Requests kommen aus dem selben IP-Adressraum. Die Abstürze starteten bereits vor ein paar Wochen. Zumeist aber zu Tageszeiten, die ich (und anscheinend auch viele meiner LeserInnen) nicht mitbekamen. Und nach ein paar Minuten war der Server stets wieder im Normalbetrieb.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2025-06-30-gcp-dos.avif" alt="Image: Whois-request for the IP that had my server crashed, owner: Google LLC (GCP)" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Oha, die Spur führt zu Google&amp;rsquo;s Cloud Platform (GCP).&lt;/p&gt;&#10;&lt;h3 id="aber-ich-hatte-doch-robots-ausgesperrt"&gt;Aber ich hatte doch Robots ausgesperrt?&lt;/h3&gt;&#10;&lt;p&gt;In der Tat hatte ich die &lt;a href="https://blog.schallbert.de/gitea-search-indexation/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Suchmaschinenindexierung für Gitea gestoppt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Merkwürdig. Oder habe ich es hier gar nicht mit einer der Google-spiders oder &amp;ldquo;KI&amp;rdquo;-Scraper, sondern mit einem fiesen Hacker zu tun, der sich bei &amp;ldquo;Google Cloud&amp;rdquo; eine virtuelle Maschine gemietet hat?&lt;/p&gt;&#10;&lt;h3 id="log-recherche-wie-häufig-wird-die-datei-angefragt"&gt;Log-Recherche: Wie häufig wird die Datei angefragt?&lt;/h3&gt;&#10;&lt;p&gt;Ein Spider würde ja höchstens einmal alle paar Wochen alle meine Seiten durchsuchen, richtig? Und sich hoffentlich nicht über meine &lt;code&gt;robots.txt&lt;/code&gt; hinwegsetzen. Erst recht nicht würde ein Crawler mehrfach und in kurzen Abständen denselben Request stellen, oder?&lt;/p&gt;&#10;&lt;p&gt;Zum dies zu prüfen, suche ich in den *Gitea Logs nach Einträgen von GET-Anfragen auf einer der großen und damit ressourcenintensiv zu übertragenen Dateien:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gunzip gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;grep &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;filename&amp;gt;&amp;#34;&lt;/span&gt; gitea.log.&amp;lt;date.rotateID&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In der Liste sehe ich, dass dieselbe Anfrage auf dieselbe Zieldatei von derselben IP-Adresse mehrfach und binnen Sekunden gestellt wird. Kurz vor dem Crash benötigt mein Server schon fast &lt;code&gt;4sec&lt;/code&gt;, um die Anfrage zu bedienen.&lt;/p&gt;&#10;&lt;p&gt;Auch sehe ich, dass sich die IP-Adresse alle paar Stunden ändert.&lt;/p&gt;&#10;&lt;h2 id="einen-dos-angriff-parieren"&gt;Einen DoS-Angriff parieren&lt;/h2&gt;&#10;&lt;p&gt;Zusammenfassend muss ich zum Schluss kommen, dass ich aus dem Adressraum des GCP per &lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Denial-of-service&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; angegriffen werde.&lt;/p&gt;&#10;&lt;p&gt;Um ein bisschen mehr Hintergrundwissen zu sammeln, besuche ich ein paar Websites zum Thema. Dort lerne ich, dass &lt;em&gt;gitea&lt;/em&gt; auf meinem Server unter einer &lt;a href="https://www.geeksforgeeks.org/computer-networks/types-of-dos-attacks/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Application Layer Attack&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zusammenbricht. Jetzt da ich weiß, was passiert und wie das Problem heißt, fällt das Finden von Lösungen gleich viel leichter 😅&lt;/p&gt;&#10;&lt;p&gt;Ich will mich natürlich nicht kampflos ergeben, indem ich meine Gitea-Instanz dauerhaft vom Netz nehme. Also, welche Optionen habe ich?&lt;/p&gt;&#10;&lt;h3 id="mehr-ressourcen-bereitstellen"&gt;Mehr Ressourcen bereitstellen&lt;/h3&gt;&#10;&lt;p&gt;Zugegeben, meine Maschine hat nur &lt;code&gt;40GB&lt;/code&gt; Speicher und &lt;code&gt;2GB VRAM&lt;/code&gt; sowie eine mickrige 2-Kern CPU aus 2009. Ich könnte einen leistungsfähigeren Server buchen, um Lastspitzen besser abreiten zu können. Doch dies würde den Angriff nicht verhindern, sondern nur seine Auswirkungen abmildern.&lt;/p&gt;&#10;&lt;h3 id="rate-limiting-direkt-im-webserver"&gt;Rate limiting direkt im Webserver&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rate_limiting" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Rate limiter&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; begrenzen die Anzahl Anfragen eines Clients innerhalb eines Zeitfensters. Dadurch werden die Ressourcen des Servers geschont. Dies ist eine &amp;ldquo;weiche&amp;rdquo; Abwehr von Dos-Attacken, denn auslösende IP-Adressen werden mit einer Fehlermeldung kurzzeitig und sanft abgewehrt. Üblicherweise wird &lt;code&gt;HTTP status code 429&lt;/code&gt; &amp;ldquo;Too Many Requests&amp;rdquo; zurückgegeben, wenn der Limiter eingreift.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Gitea&lt;/em&gt; verfügt über keinen Rate Limiter. In &lt;a href="https://blog.schallbert.de/projects/move-blog-to-own-server/#web-access"&gt;meinem Setup&lt;/a&gt; läuft &lt;em&gt;Gitea&lt;/em&gt; hinter einem reverse proxy, der von meinem &lt;em&gt;Caddyserver&lt;/em&gt; bereitgestellt wird. Hier also müsste ich ansetzen. Für &lt;em&gt;Caddy&lt;/em&gt; gibt es Rate limiter nur als &lt;a href="https://caddyserver.com/docs/modules/http.handlers.rate_limit" target="_blank" rel="noopener noreferrer" class="external-link"&gt;externe Module&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche manuell in &lt;em&gt;xcaddy&lt;/em&gt; nachinstalliert und eingerichtet werden müssen.&lt;/p&gt;&#10;&lt;h3 id="blocken-mit-ip-table-regeln"&gt;Blocken mit IP-table Regeln&lt;/h3&gt;&#10;&lt;p&gt;Hier könnte man sich erneut &lt;em&gt;fail2ban&lt;/em&gt; hernehmen und mehrfache Anfragen derselben Ressource von einer IP-Adresse schlicht blocken. &lt;em&gt;Gitea&lt;/em&gt; hat in der &lt;a href="https://docs.gitea.com/next/administration/fail2ban-setup" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation eine Beschreibung der Einrichtung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. In meinem Falle müsste ich für die Umsetzung dort weitermachen, &lt;a href="https://blog.schallbert.de/server-protection/#was-noch-nicht-funktioniert-gitea--fail2ban"&gt;wo ich bereits einmal aufgegeben hatte&lt;/a&gt; und damit nicht nur SSH, sondern auch normale Seitenaufrufe überwachen.&lt;/p&gt;&#10;&lt;p&gt;Für mich klingt diese Lösung am Sinnvollsten, denn sie trennt klar Funktionen in verschiedene Anwendungen auf. Ich nutze ausschließlich bei mir bereits verfügbare Applikationen: &lt;em&gt;Caddy&lt;/em&gt; würde die Zugriffslogs bereitstellen und &lt;em&gt;fail2ban&lt;/em&gt; müsste sie lesen und Filter in der Konfoguration &lt;code&gt;jail.local&lt;/code&gt; so setzen, dass es sich wie ein Rate Limiter verhält.&lt;/p&gt;&#10;&lt;p&gt;Jetzt schaue ich mal, wie ich &lt;em&gt;Caddy&lt;/em&gt; entsprechend mit &lt;em&gt;fail2ban&lt;/em&gt; verbinden kann. Den weiterführenden Artikel &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;Fail2ban mit Caddy einrichten&lt;/a&gt; verlinke ich direkt mal 🙂&lt;/p&gt;&#10;</description></item><item><title>Repo-Lookout: Sicherheitslücke beheben</title><link>https://blog.schallbert.de/repo-lookout-fix-deploy/</link><pubDate>Fri, 20 Dec 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/repo-lookout-fix-deploy/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20-repolookout-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: Repolookout reporting a possible vulnerability on my site"&#10; title="Repo-Lookout: Sicherheitslücke beheben" /&gt;&#10;&lt;h2 id="repo-informationen-öffentlich-zugänglich"&gt;Repo-Informationen öffentlich zugänglich&lt;/h2&gt;&#10;&lt;p&gt;Eines schönen Tages bekam ich eine E-Mail von &lt;a href="https://www.repo-lookout.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Repo Lookout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Darin stand, dass eines meiner Repositories für Zugriffe aus dem Internet freigegeben sei. Dies stelle ein potentielles Sicherheitsrisiko dar, da möglicherweise geheime Quelldateien, versteckte Funktionen oder sogar Passwörter enthalten sein könnten.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repolookout_warning.jpg" alt="Image: RepoLookout mail notification" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Zuerst hielt ich diese &amp;ldquo;Ihr-Repo-ist-nicht-sicher&amp;rdquo;-Warnung für einen Phishing-Versuch.&#10;Doch durch einfaches Eingeben der darin enthaltenen Links wurde klar, dass &lt;em&gt;Repo Lookout&lt;/em&gt; Recht hatte und mein &lt;a href="https://lectures.schallbert.de" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Lectures&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;-Repo nicht nur wie von mir gewünscht öffentlich war, sondern dass auch die Metadaten zur Versionskontrolle offen auf dem Webserver lagen.&lt;/p&gt;&#10;&lt;h3 id="ist-das-schlimm"&gt;Ist das schlimm?&lt;/h3&gt;&#10;&lt;p&gt;Normalerweise sollen innere Struktur und Konfiguration (Actions, Diskussionen, Wiki etc.) hinter einem Repository nicht öffentlich bleiben. Erst recht nicht, wenn das Repository als &lt;code&gt;private&lt;/code&gt; angelegt ist. Doch auch bei öffentlichen Repos sollte niemand auf die Struktur dahinter zugreifen können.&lt;/p&gt;&#10;&lt;p&gt;Daher die Mission von Repo-Lookout:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Find source code repositories that have been inadvertently exposed to the public and report them to the domain&amp;rsquo;s technical contact.&amp;rdquo; - Repo Lookout /about (&lt;a href="https://www.crissyfield.de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Crissy Field GmbH&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In diesem Falle unkritisch, aber unerwartet und unschön.&lt;/p&gt;&#10;&lt;p&gt;Schadet mir nicht, weil mein Webserver die Dateien lediglich zum Abruf dort liegen hat und sie selbst bei Manipulation keine Rückwirkung auf mein Repository gehabt hätten. Außerdem hatte ich alle Secrets wie &lt;a href="https://blog.schallbert.de/server-config-version-control/#secrets-in-docker-composeyml"&gt;in diesem Artikel beschrieben&lt;/a&gt; in eigens angelegte Dateien ausgelagert, sodass sie nicht mehr in den Konfigurationsdateien auftauchen. Das Repository liegt zudem getrennt auf der &lt;a href="https://git.schallbert.de/schallbert/lectures" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Gitea-Server-Instanz&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Dennoch sollte nur am Internet hängen, was ich bewusst freigeben möchte.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-12-20_repo_commithistory.jpg" alt="Image: The repository&amp;#39;s commit history is public anyways" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Der Bildausschnitt von &lt;em&gt;Gitea&lt;/em&gt; zeigt denselben Commit wie die Warnung von &lt;em&gt;Repo Lookout&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h3 id="wie-es-dazu-kam"&gt;Wie es dazu kam&lt;/h3&gt;&#10;&lt;p&gt;In der Deploy-Pipeline für meine Subdomain &lt;code&gt;lectures.schallbert.de&lt;/code&gt; und Landing Page &lt;code&gt;schallbert.de&lt;/code&gt; habe ich einen direkt-Checkout von der &lt;em&gt;Gitea&lt;/em&gt;-Instanz zum Webserver &lt;em&gt;Caddy&lt;/em&gt;. Dieser läuft automatisch an, sobald die &lt;code&gt;main&lt;/code&gt;-Branches ein Update erhalten. Der Runner startet eine &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Checkout-Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welche das Repository ins entsprechende Verzeichnis des Webservers kopiert.&lt;/p&gt;&#10;&lt;h3 id="checkout-action-kopiert-auch-den-git-ordner"&gt;Checkout-Action kopiert auch den &lt;code&gt;.git&lt;/code&gt;-Ordner&lt;/h3&gt;&#10;&lt;p&gt;Dabei wird der &lt;code&gt;.git&lt;/code&gt;-Ordner einfach mit aufgesetzt. Dort drin liegen alle von der Versionskontrollsoftware &lt;a href="https://git-scm.com/docs/gitrepository-layout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;benötigten Daten zur Zustandsverwaltung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; des Repos. Den Kopiervorgang selbst kann ich auf &lt;em&gt;Gitea&lt;/em&gt; nicht sehen, denn dort taucht der versteckte &lt;code&gt;.git&lt;/code&gt;-Ordner gar nicht erst im Verzeichnis auf. Verständlich, denn auf Basis dieses Ordners findet die gesamte Darstellung auf &lt;em&gt;Gitea&lt;/em&gt; statt.&lt;/p&gt;&#10;&lt;p&gt;So blieb das unerwünschte Verhalten unter meinem Radar - und laut &lt;em&gt;Repo Lookout&lt;/em&gt; bin ich bei Weitem nicht der Einzige, dem es so geht.&lt;/p&gt;&#10;&lt;h2 id="option-1-fix-auf-dem-webserver"&gt;Option 1: Fix auf dem Webserver&lt;/h2&gt;&#10;&lt;p&gt;Die naheliegendste Lösung ist, den Zugriff auf die Datei serverseitig zu blockieren. Dies kostet nur wenige Ressourcen und ist leicht einzurichten.&lt;/p&gt;&#10;&lt;p&gt;Dieser Foreneintrag zeigt, wie das geht: &lt;a href="https://caddy.community/t/v2-hide-entire-folder-caddyfile/7234" target="_blank" rel="noopener noreferrer" class="external-link"&gt;hide-entire-folder-caddyfile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Für meine zu schützenden Dateien füge ich also folgende Einträge der &lt;code&gt;Caddyfile&lt;/code&gt; hinzu:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /caddy2/Caddyfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;respond /.git/* &amp;#34;Access denied&amp;#34; 403&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;respond /.gitea/* &amp;#34;Access denied&amp;#34; 403&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Damit sage ich Caddy, dass es bei Aufruf einer beliebigen Datei &lt;code&gt;/*&lt;/code&gt; im Ordner &lt;code&gt;/.git&lt;/code&gt; mit dem Fehlercode &lt;code&gt;403&lt;/code&gt; &amp;ldquo;Forbidden&amp;rdquo; antworten soll. Dabei ist die Wildcard (&lt;code&gt;*&lt;/code&gt;) unbedingt erforderlich, denn sonst wird &lt;em&gt;lediglich der Ordner selbst&lt;/em&gt; und nicht die enthaltenen Dateien gesperrt.&lt;/p&gt;&#10;&lt;p&gt;Zur Kontrolle prüfe ich was passiert, wenn ich die Git Logs anfrage:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;# Terminal&#10;curl &amp;lt;lectures.schallbert.de&amp;gt;/.git/logs/HEAD&#10;Access denied &#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Funktioniert prima!&lt;/p&gt;&#10;&lt;h2 id="option-2-fix-in-der-checkout-action"&gt;Option 2: Fix in der Checkout-Action&lt;/h2&gt;&#10;&lt;p&gt;Es gibt allerdings eine noch viel elegantere Lösung: bereits vorher in der Pipeline dafür sorgen, dass der Ordner gar nicht erst auf dem Server auftaucht.&lt;/p&gt;&#10;&lt;h3 id="variante-1-mit-hilfe-von-sparse-checkout"&gt;Variante 1: Mit Hilfe von &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/33933702/git-checkout-except-one-folder" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sparse-checkout&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ermöglicht die Auswahl von Ordnern und Dateien, die zum Checkout gehören sollen. Alle anderen Dateien im Repository bleiben unberührt und tauchen im Branch nicht auf. Dies spart vor allem bei großen Repositories Zeit und Speicherplatz. Macht aber natürlich nur Sinn wenn bereits vorher bekannt ist, dass nicht alle Dateien angefasst werden müssen.&lt;/p&gt;&#10;&lt;h3 id="negativliste-für-sparse-checkout"&gt;Negativliste für &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;In meinem Falle will ich die oben erwähnten Ordner gerade &lt;em&gt;nicht&lt;/em&gt; per Checkout auf den Server kopieren, den ganzen Rest aber schon. Wie ich das hinbekomme? Mit Hilfe der Negation im &lt;code&gt;no-cone&lt;/code&gt; mode.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;The user has explicitly said &amp;lsquo;I want these directories and not those directories.&amp;rsquo;&amp;rdquo; - Derrick Stolee, Microsoft, auf &lt;a href="https://github.com/git/git/commit/55dfcf9591b088ce60ec80eb5425dda18223cac0" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;In der &lt;a href="https://github.com/marketplace/actions/checkout" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Anleitung zu Checkout-Action&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; steht, dass &lt;code&gt;sparse-checkout&lt;/code&gt; auch für die vom Runner automatisierte Aktion unterstützt wird.&lt;/p&gt;&#10;&lt;p&gt;Nun programmiere ich mit Hilfe des &lt;a href="https://github.github.com/actions-cheat-sheet/actions-cheat-sheet.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Github Actions Cheet Sheet&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/deploy-lectures.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;sparse-checkout&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; /*&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; !.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; !.gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;sparse-checkout-cone-mode&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zur Erläuterung: Das Skript zum Veröffentlichen auf meinem Webserver verwendet die Aktion &lt;code&gt;checkout&lt;/code&gt;, Unterfunktion &lt;code&gt;sparse-checkout&lt;/code&gt; und bezieht sämtliche Dateien im Ordner im Stammverzeichnis &lt;code&gt;tmp&lt;/code&gt; und darunter ein bis auf &lt;code&gt;.git&lt;/code&gt; und &lt;code&gt;.gitea&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="was-ist-der-no-cone-mode"&gt;Was ist der &lt;code&gt;No-Cone Mode&lt;/code&gt;?&lt;/h3&gt;&#10;&lt;p&gt;Standardmäßig erwartet &lt;code&gt;sparse-checkout&lt;/code&gt; eine Liste von Ordnern, die für den Checkout erfasst werden sollen. Im &lt;code&gt;no-cone&lt;/code&gt; Modus wird stattdessen eine Liste von Patterns erwartet. Hier sind sämtliche Operatoren möglich, die auch in der &lt;code&gt;.gitignore&lt;/code&gt; zum Spezifizieren von Dateien, Ordnern, Auslassungen etc. verwendet werden können. Dies ermöglicht mir den Ausschluss bestimmter Ordner, hat aber einige &lt;a href="https://git-scm.com/docs/git-sparse-checkout#_internalsnon_cone_problems" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gewichige Nachteile&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Durch viel höhere Komplexität der Pattern-Befehle, die damit einhergehende Fehleranfälligkeit sowie die deutlich rechenintensivere Auswertung bei größeren Repositories wird die Verwendung des &lt;code&gt;no-cone&lt;/code&gt; Mode nicht empfohlen und ist in der Dokumentation als &amp;ldquo;deprecated&amp;rdquo; (überholt) angegeben. Dennoch, probieren geht über Studieren!&lt;/p&gt;&#10;&lt;h3 id="test-mit-sparse-checkout"&gt;Test mit &lt;code&gt;sparse-checkout&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Nun lade ich die Action auf meine Gitea-Instanz hoch und lass meinen &lt;em&gt;runner&lt;/em&gt; einmal drüber laufen. Anschließend logge ich mich auf dem Webserver ein und schaue, ob der &lt;code&gt;.git&lt;/code&gt;-Ordner erstellt wurde oder nicht:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;lectures# ls -la&#10;[...]&#10;drwxr-xr-x 8 root root 4096 Dec 20 10:41 .git&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Mist, der Ordner ist ja immer noch da. Ich schaue in den Logs der Action auf meiner Gitea-Instanz nach:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;[...]&#10;hint: &#9;git branch -m &amp;lt;name&amp;gt;&#10;Initialized empty Git repository in /workspace/schallbert/lectures/tmp/.git/&#10;[...]&#10;::group::Setting up sparse checkout&#10;[command]/usr/bin/git config core.sparseCheckout true&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An &lt;code&gt;sparse-checkout&lt;/code&gt; liegt es also nicht. Dafür aber an der Art und Weise, wie &lt;em&gt;checkout&lt;/em&gt; funktioniert: Offensichtlich wird für ein ordentliches Aufsetzen des Repository auf meinem Webserver der &lt;code&gt;.git&lt;/code&gt;-Ordner zwingend benötigt. Also bleibt mir nur übrig, ihn nach dem Auschecken automatisch zu löschen.&lt;/p&gt;&#10;&lt;h3 id="variante-2-rm--rf"&gt;Variante 2: &lt;code&gt;rm -rf&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;Und so probiere ich es mit Gewalt:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /.gitea/workflows/deploy-lectures.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;CHECKOUT ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v4&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;path&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./tmp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: --- &lt;span style="color:#ae81ff"&gt;REMOVE TEMPORARY FILES ---&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rm -rfv ./tmp/.git ./tmp/.gitea&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nun endlich taucht der &lt;code&gt;.git&lt;/code&gt;-Ordner auf meinem Webserver nicht mehr auf und mein &amp;ldquo;Repo-Leak&amp;rdquo; ist geflickt. Nochmal danke an &lt;em&gt;Repo Lookout&lt;/em&gt;!&lt;/p&gt;&#10;&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;&#10;&lt;p&gt;Ich hatte hier das Problem, dass der versteckte &lt;code&gt;.git&lt;/code&gt;-Ordner, wo Konfiguration und Struktur von Repositories gespeichert werden, unbeabsichtigt und ohne mein Wissen auf meinem Webserver veröffentlicht waren.&lt;/p&gt;&#10;&lt;p&gt;Ich habe hier zwei funktionierende Optionen zur Lösung des Problems vorgestellt:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Ein Zugriffsverbot auf dem Webserver einrichten&lt;/li&gt;&#10;&lt;li&gt;Die Pipeline so umbauen, dass sie den &lt;code&gt;.git&lt;/code&gt;-Ordner nach erfolgtem Ausrollen selbstständig löscht.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Die zweite Option ist zwar etwas aufwendiger einzubauen, doch packt sie das Problem bei der Wurzel, anstatt lediglich die Symptome zu beheben. Außerdem entspricht sie dem ersten Grundsatz aus dem Datenschutz: Datenminimierung geht vor Schutzmaßnahmen.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Was nicht existiert, kann nicht verloren gehen&amp;rdquo; - Schallbert&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;</description></item><item><title>NVME-Festplatte kaputt?</title><link>https://blog.schallbert.de/broken-nvme/</link><pubDate>Tue, 10 Sep 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/broken-nvme/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_nvme-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: My NVME-SSD that suddenly broke down"&#10; title="NVME-Festplatte kaputt?" /&gt;&#10;&lt;h2 id="das-ereignis"&gt;Das Ereignis&lt;/h2&gt;&#10;&lt;p&gt;Mitte April 2024:&lt;/p&gt;&#10;&lt;p&gt;Nach einem Systemupdate fährt mein Laptop plötzlich nicht mehr hoch. Der Bootloader &lt;em&gt;grub-core&lt;/em&gt; meldet, er habe Probleme beim Schreiben von Sektoren.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;error: ../grub-core/efi/efidisk.c:638: failure writing sector 0xXXXXXXXX&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Beim Reboot bleibt mir der Bildschirm nach Anzeige des Firmenlogos mit blinkendem Cursor schwarz. Wenn ich hinter die Maske schaue, dann schlägt das Laden diverser Services fehl:&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&lt;video controls&gt;&#10; &lt;source src="https://blog.schallbert.de/assets/video/posts/2024-09-10_nvme-boot-errors.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10; &lt;/video&gt;&lt;figcaption class="media-caption"&gt;&#10; &lt;span class="caption-text"&gt;Boot errors due to read-only nvme&lt;/span&gt;&#10; &lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="erste-analyse"&gt;Erste Analyse&lt;/h2&gt;&#10;&lt;p&gt;Ich vermutete ein Problem beim Update des Rechners und führte aus dem &lt;a href="https://en.wikipedia.org/wiki/UEFI" target="_blank" rel="noopener noreferrer" class="external-link"&gt;UEFI&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; von Lenovo heraus einen Festplattentest durch. Ergebnis:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;The media check is complete, the result is: FAIL&#10;&lt;/code&gt;&lt;/pre&gt;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_diskcheck-fail.jpg" alt="Image: disk check tool diagnoses a defective device" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Eher kein Problem mit dem Betriebssystem sondern eines mit der Platte. Mist. Muss ich um meine Daten bangen? Ich habe zwar Backups herumliegen, das letzte ist aber drei Wochen alt und somit sind einige Stunden Arbeit auf der Kippe. Zudem kostet mich ein Wiederaufsetzen einiges an Zeit. Daher probiere ich erst einmal, ob sich noch etwas retten lässt.&lt;/p&gt;&#10;&lt;h3 id="selbsttest"&gt;Selbsttest&lt;/h3&gt;&#10;&lt;p&gt;Ich lasse also die &lt;a href="https://en.wikipedia.org/wiki/Self-Monitoring,_Analysis_and_Reporting_Technology" target="_blank" rel="noopener noreferrer" class="external-link"&gt;SMART&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Tests für die Platte mit &lt;code&gt;smartctl -x /dev/nvm0&lt;/code&gt; durchlaufen und weiß nun, dass sie etwa &lt;code&gt;7600&lt;/code&gt; Betriebsstunden hinter sich und &lt;code&gt;240GB&lt;/code&gt; nutzbare Kapazität hat. Die unter dem Namen &lt;code&gt;nvme0n1&lt;/code&gt; eingesetzte Disk meint, sie sei nun &amp;ldquo;read-only&amp;rdquo;. Und zwar sämtliche Partitionen inklusive Kernel. Na klasse. Das Ergebnis meiner Online-Suche war in etwa:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Die Platte ist irreparabel beschädigt. Es muss eine Neue her.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="reparaturversuch"&gt;Reparaturversuch&lt;/h3&gt;&#10;&lt;p&gt;Man muss ja nicht alles glauben, was im Netz steht. Also nehme ich das Lenovo-eigene Reparaturwerkzeug für Festplatten zur Hand. Es lässt sich aus dem UEFI heraus aufrufen und firmiert unter dem Namen &lt;a href="https://download.lenovo.com/pccbbs/thinkvantage_en/userguide_uefi_v04.08.000.pdf" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Bad Block Recovery Tool&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Das Programm scannt die Platte auf defekte Sektoren und versucht, betroffene Blöcke wiederherzustellen. Der Durchlauf benötigt für die letzten 10% fast eine halbe Stunde.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_summary.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s summary for my disk" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_detail.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s output messages" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Ergebnis: Wiederherstellung fehlgeschlagen. Ich führe das Werkzeug erneut aus. Und noch ein drittes Mal im langsamen &lt;code&gt;pedantic&lt;/code&gt;-Modus. Immer wieder ohne Erfolg. Ständig bekomme ich &lt;code&gt;critical medium error&lt;/code&gt; angezeigt.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_badblockrecovery_log.jpg" alt="Image: Bad Block Recovery tool&amp;#39;s log" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Ob dies alles nicht klappt, weil sich die Platte in den Read-only mode gesetzt hat?&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Da ich nicht mehr booten kann und keine Konsole zur Verfügung habe, sind meine Möglichkeiten mit Bordmitteln nun erschöpft.&lt;/p&gt;&#10;&lt;h3 id="einwurf-der-read-only-mode-bei-ssds"&gt;Einwurf: Der Read-only mode bei SSDs&lt;/h3&gt;&#10;&lt;p&gt;Laut Internet &lt;a href="https://www.disktuna.com/a-write-protected-ssd-nvme-read-only/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;(DiskTuna)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; ist der &lt;em&gt;read only mode&lt;/em&gt; zur Schadensbegrenzung vorhanden. Stellt der Controller des Speichergerätes einen schwerwiegenden Fehler fest, versetzt er die Platte in Nur-Lesezugriff. Somit wird das Risiko minimiert, dass weitere Blöcke korrumpiert oder mit in den Tod gerissen werden.&lt;/p&gt;&#10;&lt;p&gt;Im Forum &lt;a href="https://superuser.com/questions/1802470/ssd-in-read-only-mode-after-power-failure" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Superuser&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; beschreibt jemand einen identischen Fehler zu meinem, und tatsächlich ist da wohl nichts mehr zu machen.&lt;/p&gt;&#10;&lt;h2 id="datenrettung-per-boot-stick"&gt;Datenrettung per Boot-Stick&lt;/h2&gt;&#10;&lt;p&gt;Weiter geht&amp;rsquo;s mit einem USB-Stick, den ich von einem anderen Rechner aus zum Linux-Boot-Stick mache. Ich stopfe ihn an meinen Laptop und kann ihn endlich wieder hochfahren. Als Medium zur Datenrettung schließe ich eine externe Festplatte an.&lt;/p&gt;&#10;&lt;h3 id="übersicht-verschaffen-mit-fdisk"&gt;Übersicht verschaffen mit &lt;em&gt;fdisk&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Zuerst schaue ich, ob alle Platten und Partitionen auftauchen. Dafür tippe ich das Kommando &lt;code&gt;sudo fdisk -l&lt;/code&gt; in die Konsole.&lt;/p&gt;&#10;&lt;div class="media-wrapper center"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_fdisk.jpg" alt="Image: fdisk&amp;#39;s output on my live system" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Glück gehabt, alles ist noch da! Vielleicht kann ich die Dateien ja einfach von der defekten Festplatte herunter kopieren?&lt;/p&gt;&#10;&lt;h3 id="kopieren-mit-cp"&gt;Kopieren mit &lt;em&gt;cp&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Probieren wir&amp;rsquo;s aus.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo cp -r -v /mnt/nvme0n1p3/Documents/my/important/files /mnt/externalHdd/save&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ein paar Sekunden denke ich, dass alles glatt geht. Doch dann bricht &lt;code&gt;cp&lt;/code&gt; mit Fehlermeldung ab, das Volume sei nicht verfügbar. Tatsächlich ist die Platte nicht mehr gemountet. Ich kann sie allerdings direkt wieder finden und mounten. Ein paar Versuche später habe ich meine wirklich wichtigen Daten gesichert, der Kopiervorgang bricht aber stets nach ein paar Sekunden ab. Für die ganze Platte würde mir dieses Vorgehen viel zu lange dauern.&lt;/p&gt;&#10;&lt;p&gt;Genau dieses Verhalten - mounten, ein paar Sekunden kopieren, Platte verschwindet - habe ich online in ein paar Foreneinträgen bestätigt bekommen.&lt;/p&gt;&#10;&lt;p&gt;Also muss eine andere Lösung her.&lt;/p&gt;&#10;&lt;h3 id="daten-retten-mit-ddrescue"&gt;Daten retten mit &lt;em&gt;ddrescue&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-09-10_ddrescue.jpg" alt="Image: ddrescue output when saving my broken SSD&amp;#39;s files to an external HDD." /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Nach kurzer Online-Suche installiere ich das Programm &lt;a href="https://www.gnu.org/software/ddrescue/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ddrescue&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;In der zugehörigen Dokumentation stehen einige Dinge sehr klar drin. Dass man z.B. verstanden haben sollte was es genau tut, bevor man es verwendet. Oder das Folgende:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;Never try to repair a file system on a drive with I/O errors; you will probably lose even more data. &amp;quot; - ddrescue Manual&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Jetzt stellt sich für mich die Frage, ob ich oben mit dem Block-Reparaturversuch genau so einen Fehler begangen habe. Oder bin ich mit der Bad Block Recovery auf der sicheren Seite, da hier von Datenblöcken auf der Platte und nicht vom &amp;ldquo;darüber liegenden&amp;rdquo; Dateisystem die Rede ist?&#10;Hier steht auch, dass die sogenannte &lt;em&gt;Mapfile&lt;/em&gt; eine Kernfunktion des Programmes ist. Nach Abbruch, Neustart, für die Zusammenführung von Backups und zum Verkürzen der Datenrettung bietet sie einen großen Zeitvorteil. Daher sei es keine schlechte Idee, sich die Mapfile schon bei Erstellung einer Offlinhe-Sicherheitskopie zu erzeugen und gut wegzulegen. Insgesamt fand ich das Tool extrem hilfreich, effektiv und leicht zu bedienen und kann es an dieser Stelle nur weiterempfehlen.&lt;/p&gt;&#10;&lt;p&gt;Zurück zur Sache: Ich finde einen ganz einfachen Befehl, die komplette Datenpartition meines beschädigten NVME-Speichers auf eine externe Festplatte zu kopieren:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ddrescue --sparse /dev/nvme0n1p3 /run/media/liveuser/myExternalHDD/datapartition&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Die &lt;code&gt;--sparse&lt;/code&gt;-Option überspringt mit Nullen belegte Bereiche der Quellplatte und kann so eine Menge Platz auf dem Zieldatenträger einsparen.&#10;Das Programm benötigt eine Viertelstunde für den kompletten Durchlauf. Es kann wie oben im Bild zu erkennen tatsächlich &lt;code&gt;99.6%&lt;/code&gt; der Daten retten und auch mehrere Monate später habe ich die restlichen &lt;code&gt;0.4%&lt;/code&gt; noch nicht vermisst. Glück gehabt.&lt;/p&gt;&#10;&lt;h3 id="überprüfung-output-von-ddrescue-ansehen"&gt;Überprüfung: Output von &lt;em&gt;ddrescue&lt;/em&gt; ansehen&lt;/h3&gt;&#10;&lt;p&gt;Jedenfalls legt das Programm am Ende eine einzige Datei auf meiner externen Platte ab: &lt;code&gt;datapartition.img&lt;/code&gt;&#10;Dieses Image kann ich mit &lt;code&gt;fdisk -l&lt;/code&gt; anschauen:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;datapartition.img: 236.89 GiB, 254356226048 bytes, 496789504 sectors&#10;Units: sectors of 1 * 512 = 512 bytes&#10;Sector size (logical/physical): 512 bytes / 512 bytes&#10;I/O size (minimum/optimal): 512 bytes / 512 bytes&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Alles da, das ist ja schon einmal gut. Ob denn nun auch alles drin ist?&lt;/p&gt;&#10;&lt;p&gt;Dafür mounte ich die Partition mal und schaue mich per &lt;code&gt;cd&lt;/code&gt; und &lt;code&gt;ls -l&lt;/code&gt; darin um:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mkdir /mnt/olddisk &lt;span style="color:#75715e"&gt;# Mount point of the rescued data image&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mount /run/media/liveuser/myExternalHDD/datapartition.img /mnt/olddisk&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zum Glück war alles drin, was drin sein sollte. Nun konnte ich das Image unmounten, den Rechner herunterfahren und eine neue Festplatte einbauen lassen.&lt;/p&gt;&#10;&lt;h2 id="die-neue-festplatte"&gt;Die neue Festplatte&lt;/h2&gt;&#10;&lt;p&gt;Auch hier habe ich noch einmal Glück gehabt: Auf meinem Gebrauchtlaptop war noch Garantie und so bekam ich ihn binnen einer Woche mit einer neuen (und doppelt so großen) Festplatte zurück gesendet. Da ich so große Angst vor Datenklau bekam, hatte ich meine alte Platte vor dem Versenden ausgebaut. Dabei kam ich mir ganz schön paranoid vor.&lt;/p&gt;&#10;&lt;p&gt;Diese Arbeit hätte ich mir aber vielleicht sparen können, denn &lt;em&gt;ddrescue&lt;/em&gt; hat auch für paranoide Leute wie mich eine Software-Lösung parat: Die Option &lt;code&gt;--fill-mode&lt;/code&gt;.&#10;Hiermit können nach erfolgter Rettung die &amp;ldquo;noch guten&amp;rdquo; Blöcke und Sektoren gelöscht werden. Nur die ohnehin defekten Sektoren bleiben erhalten. Aus der &lt;a href="https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html#Fill-mode" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Online-Dokumentation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ddrescue --fill-mode&lt;span style="color:#f92672"&gt;=&lt;/span&gt;+ --force /dev/zero bad_drive mapfile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ob dies allerdings auch einer Platte wie meiner funktioniert hätte, die der On-Board-Controller auf &amp;ldquo;read-only&amp;rdquo; gesetzt hat, ist mir nicht klar. Egal.&lt;/p&gt;&#10;&lt;p&gt;Ich kann jetzt jedenfalls mit einem voll funktionsfähigen Laptop weiter Blog-Einträge verfassen ☺️.&lt;/p&gt;&#10;</description></item><item><title>Fehler-Logdateien versenden</title><link>https://blog.schallbert.de/server-deploy-logging/</link><pubDate>Tue, 20 Aug 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-deploy-logging/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_deploy_logging-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: A notification message sent on error"&#10; title="Fehler-Logdateien versenden" /&gt;&#10;&lt;p&gt;In diesem Artikel schaue ich mir an, wie ich &amp;ldquo;Monitoring&amp;rdquo; für meinen Server einrichte. Anwendungen und Dienste sollen mir im Fehlerfalle Benachrichtigungen senden können.&lt;/p&gt;&#10;&lt;h2 id="worum-geht-es-hier"&gt;Worum geht es hier?&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sendemechaniken kennenlernen und auswählen&lt;/li&gt;&#10;&lt;li&gt;Testnachrichten verfassen und die Automation verifizieren&lt;/li&gt;&#10;&lt;li&gt;Fehlerbericht von &lt;em&gt;Borgmatic&lt;/em&gt; automatisch senden&lt;/li&gt;&#10;&lt;li&gt;Senden von Runner-logs durch &lt;em&gt;Gitea&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Benachrichtigung bei Login auf meinem Server per &lt;em&gt;ssh&lt;/em&gt;&lt;/li&gt;&#10;&lt;li&gt;Erstellen und Senden eigener Logs zu Server-Update / Server-Fehlern&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="verifikation-und-überwachung"&gt;Verifikation und Überwachung&lt;/h3&gt;&#10;&lt;p&gt;Ich möchte nach dem Ausführen einer Automation wissen, ob sie erfolgreich durchgeführt wurde und alle Programme und Dienste ihre Arbeit erwartungsgemäß aufgenommen haben. Dies soll für jegliche Automation gelten - ob es sich konkret um das Senden eines Updates, die Anfertigung automatischer Backups oder eine Action von &lt;em&gt;Gitea&lt;/em&gt; handelt, muss egal sein.&lt;/p&gt;&#10;&lt;p&gt;Normalerweise würde ich für so etwas Berichtsmechanismen von &lt;em&gt;act_runner&lt;/em&gt; verwenden. Im Falle eines Server-Updates steht der Runner durch den bereits im Artikel &lt;a href="https://blog.schallbert.de/server-config-deploy/"&gt;Serverkonfiguration ausrollen&lt;/a&gt; erwähnten &lt;a href="https://blog.schallbert.de/server-config-version-control/#vor%c3%bcberlegungen"&gt;Zirkelbezug&lt;/a&gt; nicht zur Verfügung, da zeitweise alle Anwendungen heruntergefahren werden müssen.&lt;/p&gt;&#10;&lt;p&gt;Zudem kann es sein, dass Anwendungen eigene Vorgehensweisen haben, Monitoring zu betreiben. Also muss ich mir Mechanismen anschauen, mit denen ich leicht an die Informationen herankomme.&lt;/p&gt;&#10;&lt;h3 id="was-zeichnet-ein-gutes-monitoring-für-mich-aus"&gt;Was zeichnet ein gutes Monitoring für mich aus?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Es ist unaufdringlich, meldet sich also nur im Fehlerfall oder bei ungewöhnlichen Vorgängen.&lt;/li&gt;&#10;&lt;li&gt;Es liefert spezifische Informationen und gut zu verstehende Fehlermeldungen.&lt;/li&gt;&#10;&lt;li&gt;Es nutzt einen Nachrichtenkanal, der auch bei Absturz des zu überwachenden Systems funktioniert.&lt;/li&gt;&#10;&lt;li&gt;Es stellt Berichte und Fehlermeldungen isoliert von anderen Themen dar und vermischt nichts.&lt;/li&gt;&#10;&lt;li&gt;Es fasst sich kurz.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="methoden-automatischer-berichterstattung"&gt;Methoden automatischer Berichterstattung&lt;/h2&gt;&#10;&lt;p&gt;Diesen Abschnitt gliedere ich ich auf. Im Allgemeinen Teil gehe ich auf das mir auf dem Ubuntu-Server zur Verfügung stehende Bordwerkzeug zum asynchronen Monitoring ein. Danach schaue ich mir die bei meinen Diensten teils eingebauten Lösungen bzw. mit ihnen kompatible Anwendungen an. Dabei möchte ich mich nicht auf das klassische Instrument der E-Mail beschränken, sondern auch kurz &amp;ldquo;modernere&amp;rdquo; Kommunikationskanäle wie Messengerprogramme oder RSS-Feed beleuchten.&lt;/p&gt;&#10;&lt;h3 id="mail-per-konsole---curl"&gt;Mail per Konsole - &lt;em&gt;curl&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Versand von Mails als Benachrichtigungen sind in vielen Firmen üblich. Dies geht mit Linux meist sogar ganz ohne zusätzliche Programme: Das standardmäßig vorhandene &lt;a href="https://curl.se/docs/manpage.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;curl&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kann hier bereits weiterhelfen.&#10;&lt;em&gt;curl&lt;/em&gt; it ein Programm zum Datentransfer von oder zu einem Server. Gebe ich meinen Blog als Ziel ein, bekomme ich die HTML-Seite als Textdatei auf die Konsole ausgegeben:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl https://blog.schallbert.de&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bei der Gelegenheit fällt mir auf, wie viel unnötige Daten meine Blogsoftware erzeugt. Da werde ich später mal aufräumen müssen. Zurück zum Thema: Man kann &lt;em&gt;curl&lt;/em&gt; allerdings auch nutzen, um ein beliebiges Webbackend anzusprechen - Zum Beispiel einen Mailserver:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# copied from https://stackoverflow.com/questions/8260858/how-to-send-email-from-terminal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl --url &lt;span style="color:#e6db74"&gt;&amp;#39;smtps://smtp.gmail.com:465&amp;#39;&lt;/span&gt; --ssl-reqd &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-from &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mail-rcpt &lt;span style="color:#e6db74"&gt;&amp;#39;to-email@gmail.com&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --user &lt;span style="color:#e6db74"&gt;&amp;#39;from-email@gmail.com:YourPassword&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T &amp;lt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;echo -e &lt;span style="color:#e6db74"&gt;&amp;#39;From: from-email@gmail.com\nTo: to-email@gmail.com\nSubject: Curl Test\n\nHello&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dies funktioniert nur dann auch für meinen Mail-Provider, wenn ich Logins durch externe Clients zulasse. Google nennt diese z.B. &amp;ldquo;weniger sichere Apps&amp;rdquo;. Ich bin wie in meinem Beitrag &lt;a href="https://blog.schallbert.de/server-config-version-control/#frisch-ans-werk"&gt;Server-Konfiguration mit Git&lt;/a&gt; beschrieben kein Fan davon, Secrets irgendwo hart hineinzuschreiben - daher würde ich den direkten Weg über &lt;em&gt;curl&lt;/em&gt; eher nicht nutzen.&lt;/p&gt;&#10;&lt;h3 id="mail-per-konsole---mail-mailx-mailutils-swaks"&gt;Mail per Konsole - &lt;em&gt;mail&lt;/em&gt;, &lt;em&gt;mailx&lt;/em&gt;, &lt;em&gt;mailutils&lt;/em&gt;, &lt;em&gt;swaks&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Wer all die Konfigurationsinformationen für den Server und Secrets nicht immer mitgeben möchte, dem stehen diverse handliche Werkzeuge für die Konsole wie &lt;a href="https://mailutils.org/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailutils&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; oder &lt;a href="https://github.com/jetmore/swaks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;swaks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zur Verfügung. Hier wird die Verbindung zum Mailserver über das Werkzeug einmalig konfiguriert und kann in z.B. in Umgebungsvariablen abgelegt werden. Die Syntax variiert von Programm zu Programm, hinten kommt aber stets eine Mail heraus.&lt;/p&gt;&#10;&lt;p&gt;An sich eine simple Lösung. Der einzige große Nachteil dabei für mich ist, dass sich hier Informationsdomänen vermischen. Ich hätte nur ungern noch einen &amp;ldquo;Berichtsthread&amp;rdquo; in meinen Mails, nach dem ich im Wust der Nachrichten irgendwann würde suchen müssen. Bei Benachrichtigungen auf anderen Kanälen kann ich zudem ein automatisches Verfallsdatum einstellen, sodass sie nach eingestellter Zeit aus meiner Liste verschwinden.&lt;/p&gt;&#10;&lt;h3 id="rss-feed-erzeugen"&gt;RSS Feed erzeugen&lt;/h3&gt;&#10;&lt;p&gt;Ungewöhnlich aber möglich: Ich könnte das Monitoring als &lt;a href="https://en.wikipedia.org/wiki/RSS" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RSS-Feed&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; wie meinen Blog auch (&lt;a href="https://blog.schallbert.de/index.xml"&gt;schallberts-blog-feed&lt;/a&gt;) z.B. per Jekyll-Instanz erzeugen und als Website online stellen. Dieser wäre leicht zu abonnieren, mit praktisch jedem Reader zu lesen und ich könnte ihn sogar für jede Applikation ganz einfach getrennt vorsehen. Doch es gibt offensichtliche Nachteile:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Hoher Aufwand: Gitea runner mit Jekyll-Instanz, Webserver und Subdomain erforderlich.&lt;/li&gt;&#10;&lt;li&gt;Öffentlich verfügbar: Plötzlich werden Bauvorgänge, Updates, Upgrades und Fehlermeldungen für jedermann zugänglich.&lt;/li&gt;&#10;&lt;li&gt;Fehleranfällig: Schmiert Gitea, der Runner, mein Proxy oder der Webserver ab, bekomme ich keine Berichte.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Spätestens mit dem letzten Punkt ist diese Option für mich raus. Ich möchte ja gerade dann einen Bericht bekommen, wenn meine Applikationen nicht das tun, was sie sollen.&lt;/p&gt;&#10;&lt;p&gt;Werfen wir nun mal einen Blick auf die von mir bereits betriebenen Anwendungen und schauen, wie sie dieses Problem angehen.&lt;/p&gt;&#10;&lt;h3 id="borgmatic"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic bringt von Haus aus Kompabitilität zu einer Menge &lt;a href="https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Monitoring-Optionen&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; mit. Darunter befinden sich &lt;a href="https://github.com/caronc/apprise" target="_blank" rel="noopener noreferrer" class="external-link"&gt;apprise&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://ntfy.sh/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://healthchecks.io/docs/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;healthchecks&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronitor.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronitor&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://www.pagerduty.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;pagerduty&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://cronhub.io/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;cronhub&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://grafana.com/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;grafana&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;Apprise&lt;/em&gt; ist eine Bibliothek. Sie ist quelloffen und kann von einer bestehenden Anwendung als Abhängigkeit eingebunden werden. Wie ein Adapter ermöglicht sie die asynchrone Kommunikation der Anwendung mit diversen verschiedenen Kommunikationsdiensten wie SMS, Mail, Messenger (z.B. &lt;a href="https://signal.org/de/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;), diverse Heimautomation oder den Benachrichtigungssystemen diverser Betriebssysteme. Letzteres funktioniert allerdings nur auf der lokalen Maschine. Der Trigger für die Kommunikation muss dabei stets von der Anwendung ausgehen.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;ntfy&lt;/em&gt; ist ein Push-Benachrichtigungsdienst. Er ist quelloffen und kann sowohl selbst gehostet als auch über eine Webanwendung als Dienstleistung in Anspruch genommen werden. &lt;em&gt;Apprise&lt;/em&gt; z.B. unterstützt &lt;em&gt;ntfy&lt;/em&gt; als Kommunikationsdienst. Der Aufbau ist recht einfach und funktioniert wie bei &lt;a href="https://de.wikipedia.org/wiki/MQTT" target="_blank" rel="noopener noreferrer" class="external-link"&gt;MQTT&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auch über einen Publication-Subscription bzw. Broker-Client Mechanismus, allerdings HTTP-basiert.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Healthchecks&lt;/em&gt; ist ein Dienst. Er ist quelloffen und kann selbst gehostet werden. Der Dienst ist dazu da, regelmäßige Aktivitäten zu überwachen und kann als Totmannschalter fungieren: Erfolgt anders als erwartet keine Rückmeldung vom überwachten Programm, so kann er selbst eine Fehlermeldung absetzen. Diese kann wiederum an diverse Kommunikationsdienste gesendet werden.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Cronitor&lt;/em&gt; ist eine Monitoringlösung und Webanwendung, die neben den von mir benötigten Benachrichtigungen eine Menge Analysetools, Leistungsmessungen und Metriken - zumeist gegen Geld - bereitstellt. Es gibt zwar einen kostenfreien &amp;ldquo;Hacker&amp;rdquo;-Account mit begrenztem Funktionsumfang, jedoch ist auch dieses Werkzeug mir entschieden zu groß und zu komplex.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;PagerDuty&lt;/em&gt; sieht sich als kommerzielle &amp;ldquo;Operations&amp;rdquo; Plattform, die sowohl &amp;ldquo;Incident Management&amp;rdquo;, Automation, &amp;ldquo;Business Operations&amp;rdquo;, &amp;ldquo;AIOps&amp;rdquo; etc. bereitstellt. Ist für mich direkt raus. Spätestens beim Wort &amp;ldquo;AIOps&amp;rdquo; 😅&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;cronhub&lt;/em&gt; sieht mir wie eine kommerzielle Webanwendung aus, die ähnlich wie &lt;em&gt;Healthchecks&lt;/em&gt; Cron-Jobs erstellen, überwachen und bei Fehlern melden kann. Sie ist für mich uninteressant, denn sie scheint weder open source zu sein noch könnte ich sie selbst hosten.&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;Grafana&lt;/em&gt; ist eine Open-Source-Webanwendung, die entweder selbst gehostet oder als Cloud-Dienst verwendet werden kann. Obwohl viele größere Unternehmen und Projekte die Anwendung nutzen, ist sie für meine Zwecke um Größenordnungen zu umfang- und funktionsreich.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="fail2ban"&gt;&lt;em&gt;fail2ban&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Fail2ban hat keine Automationslösung wie &lt;em&gt;borgmatic&lt;/em&gt;. Es erzeugt schlicht Logdateien die ausgewertet werden müssten, um Inhalte für Benachrichtigungen zu gewinnen. Mir fällt im Moment nichts ein, was ich unbedingt von Fail2ban wissen müsste. Daher erstelle ich mir hierfür erst einmal keine Berichte.&lt;/p&gt;&#10;&lt;h3 id="gitea"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Bei Gitea lassen sich die Logdateien sehr feingliedrig konfigurieren. So können Access Logs getrennt von Service Logs, Repository Logs oder Action Logs herausgeschrieben und dann weiterverarbeitet werden. Als Benachrichtigungssystem bietet Gitea nach meiner Recherche ausschließlich einen &lt;a href="https://docs.gitea.com/next/administration/config-cheat-sheet#mailer-mailer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;mailer&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; an. Dieser ist meiner Ansicht nach vor Allem für Repository- und Action Logs gedacht.&lt;/p&gt;&#10;&lt;p&gt;Auch hier würde es mir am meisten bringen, die Logs zu analysieren und bei Bedarf einen Bericht selbst zu erstellen.&lt;/p&gt;&#10;&lt;h3 id="server"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;Das Ausrollen meiner Server-Konfigurationsdateien übernehme ich per Skript ja sowieso schon selber. Also könnte ich auch hier im Fehlerfalle die Logs in eine Datei umleiten und sie dann einem Bericht in einen beliebigen Kanal anhängen.&lt;/p&gt;&#10;&lt;p&gt;Außerdem wären erfolgreiche Logins auf dem Server selbst eine Nachricht an mich wert. Dann kann ich sofort feststellen, ob ich das selbst war oder nicht.&lt;/p&gt;&#10;&lt;h2 id="die-auswahl-des-berichtsprogramms"&gt;Die Auswahl des Berichtsprogramms&lt;/h2&gt;&#10;&lt;p&gt;Ich muss nicht nur ein Monitoring-Programm heraussuchen, sondern auch einen Kommunikationsdienst wählen über den die Berichte versendet werden.&lt;/p&gt;&#10;&lt;h3 id="berichtsprogramm"&gt;Berichtsprogramm&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Programm&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Vorteil&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Nachteil&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;curl&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;simpel&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Konfiguration muss mitgegeben werden&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;funktioniert super einfach mit &lt;em&gt;ntfy&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Überwachung bei Absturz des Servers&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Bordwerkzeug&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kein Service erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Apprise&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ermöglicht Einbindung der Signal-Api&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Komplex im Zusammenspiel mit externen Diensten&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kein Service erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Healthchecks&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kann als Watchdog/Totmannschalter agieren&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kann lokal gehostet&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt; werden&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lokales Hosting widerspricht Watchdog-Konzept&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Ich habe ja nur einen kleinen Server mit ein paar wenigen Anwendungen. Daher halte ich es hier so einfach wie möglich und wähle &lt;em&gt;Apprise&lt;/em&gt; für Dienste wie &lt;em&gt;borgmatic&lt;/em&gt;, die diese Bibliothek sowieso mitbringen und &lt;em&gt;curl&lt;/em&gt; für jene, deren Berichtsmechanik ich selbst programmieren muss.&lt;/p&gt;&#10;&lt;h3 id="kommunikationsweg"&gt;Kommunikationsweg&lt;/h3&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Programm&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Vorteil&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Nachteil&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SMS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hohe Zuverlässigkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung erforderlich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Beschränkung auf wenige Zeichen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Anhänge möglich&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keine Topics, Domänenvermischung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Mail&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;einfach einzurichten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Schlechte Durchsuchbarkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;RSS-Feed&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Gute Sortierbarkeit&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kompliziert einzurichten, fehleranfällig&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client sehr leichtgewichtig&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lokales Hosting widerspricht Watchdog-Konzept&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Inhalte öffentlich verfügbar&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;ntfy&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Simpel und leichtgewichtig&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Registrierung optional bei Web-Nutzung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client zweckorientiert&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unsicher: Keine Verschlüsselung ohne Registrierung&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kostenlos für Kleinanwender&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unabhängig von der eigenen Maschine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;em&gt;Signal&lt;/em&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Note to self&amp;rdquo; einfach einzurichten&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Einrichtung und Konfiguration der Signal-API komplex&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Unabhängig von der eigenen Maschine&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Topics etwas schwierig umzusetzen&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Nutzlos bei Absturz des Servers&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;SMS, Mail und RSS-Feed sind durch teils oben bereits erläuterte Nachteile für mich bereits raus. Bleiben also &lt;em&gt;ntfy&lt;/em&gt; und &lt;em&gt;Signal&lt;/em&gt;. &lt;em&gt;ntfy&lt;/em&gt; besticht durch seine Einfachheit: HTTP PUSH-request auf ein selbst definiertes &amp;ldquo;Topic&amp;rdquo; senden und selbiges auf dem Handy abonnieren - fertig. Es ist auch gut erweiterbar, denn durch self-hosting kann ich später bei Bedarf die Sicherheit erhöhen (Verschlüsselung) und das Sendeverhalten steuern. &lt;em&gt;Signal&lt;/em&gt; hingegen macht einen eigenen Client mit verhältnismäßig aufwändiger Konfiguration erforderlich. Außerdem müssten die Geräte Server-Handy verbunden werden, sodass nicht einfach neue Abonnenten hinzuzufügen sind. Dafür ist die Übertragung bestens Ende-zu-Ende gesichert, ich muss mich nicht registrieren und kostenlos ist es auch noch.&lt;/p&gt;&#10;&lt;p&gt;Vorerst entscheide ich mich auch hier für die weniger komplexe Lösung mit &lt;em&gt;ntfy&lt;/em&gt;.&lt;/p&gt;&#10;&lt;h2 id="logging"&gt;Logging&lt;/h2&gt;&#10;&lt;p&gt;Gut. Nun ist geklärt, dass ich per &lt;em&gt;curl&lt;/em&gt; und &lt;em&gt;apprise&lt;/em&gt; über den &lt;em&gt;ntfy&lt;/em&gt;-Dienst nach Hause telefoniere. Nun schaue ich mal, welcher Inhalt zu übermitteln ist und wie ich möglichst unaufdringlich, dabei aber kurz und prägnant bleibe.&lt;/p&gt;&#10;&lt;h3 id="wann-sollen-logs-gesendet-werden"&gt;Wann sollen Logs gesendet werden?&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;em&gt;borgmatic&lt;/em&gt;: Wenn ein Backup fehlschlägt&lt;/li&gt;&#10;&lt;li&gt;Bei erfolgreichem Login auf meinen Server oder Gitea&lt;/li&gt;&#10;&lt;li&gt;Nach Ausrollen eines Updates der Server-Konfiguration&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;docker&lt;/em&gt;: Falls eine Applikation ausfällt oder nicht hochfährt&lt;/li&gt;&#10;&lt;li&gt;&lt;em&gt;gitea&lt;/em&gt;: Fehlgeschlagener Durchlauf von &lt;em&gt;act_runner&lt;/em&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="was-gehört-für-mich-in-einen-bericht"&gt;Was gehört für mich in einen Bericht?&lt;/h3&gt;&#10;&lt;p&gt;Ich möchte die klassischen &amp;ldquo;W-Fragen&amp;rdquo; beantwortet bekommen.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Wann ist es passiert (Zeitstempel)?&lt;/li&gt;&#10;&lt;li&gt;Welche Anwendung berichtet?&lt;/li&gt;&#10;&lt;li&gt;Was ist passiert?&lt;/li&gt;&#10;&lt;li&gt;Wo (Modul, Codezeile) etc. ist das passiert?&lt;/li&gt;&#10;&lt;li&gt;Wie viele Verletzte (Schweregrad, Recovery)?&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Eine Benachrichtigung sieht dann etwa so aus:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;TIMESTAMP APPLICATION PRIORITY MESSAGE EFFECT DETAIL &#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="rohfassung-eines-berichts"&gt;Rohfassung eines Berichts&lt;/h3&gt;&#10;&lt;p&gt;Eine &lt;em&gt;ntfy&lt;/em&gt;-Nachricht könnte ungefähr so aussehen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: Fehler Borgmatic&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: urgent&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: warning&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;YYYY-MM-DD HH:MM:SS Backup-Erstellung abgebrochen. Zugriff auf Repository blockiert&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallberts-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest.jpg" alt="Image: First ntfy test on my phone app sent with curl" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;Ich installiere die entsprechende App auf meinem Handy, registriere mich auf das &amp;ldquo;Topic&amp;rdquo; und sende die Nachricht ab. Es ist schön, wenn Dinge einfach funktionieren!&#10;Aha, die App zeigt den Zeitpunkt des Einganges an. Das ist mir genau genug.&lt;/p&gt;&#10;&lt;h2 id="durchführung"&gt;Durchführung&lt;/h2&gt;&#10;&lt;p&gt;Ich nehme mir hier nacheinander alle Dienste vor, für die ich gern Benachrichtigungen einrichten möchte.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-1"&gt;&lt;em&gt;borgmatic&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Starten wir mit einem Pilotversuch wieder in kleinen Schritten. Als Erstes konfiguriere ich Borgmatic zum Senden einer Nachricht über &lt;em&gt;Apprise&lt;/em&gt; an &lt;em&gt;ntfy&lt;/em&gt; bei Fehlschlag der Backup-Erstellung:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /borgmatic.d/config.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on_error&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;echo &amp;#34;Error while creating a backup.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;apprise -vv --title &amp;#34;Borgmatic Error&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --&lt;span style="color:#ae81ff"&gt;body &amp;#34;Could not run {output}. Aborted {error}.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;ntfy://schallberts-topic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dann teste ich das Kommando, indem ich innerhalb des Borgmatic-Containers &lt;em&gt;apprise&lt;/em&gt; anspreche. Und tatsächlich, es funktioniert. Aber der Weg hierhin hat mich locker eine Stunde gekostet, da die &lt;code&gt;yml&lt;/code&gt; mit ihren Syntaxregeln sogar innerhalb von Strings interpretiert und &lt;em&gt;borgmatic&lt;/em&gt; aufgrund von &lt;code&gt;:&lt;/code&gt; sowie &lt;code&gt;-&lt;/code&gt; Zeichen ständig die Konfigurationsdatei nicht lesen wollte. Wenn ihr einen Fehler sieht ähnlich diesem hier:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;At &amp;#39;on_error[1]&amp;#39;: {&amp;#39;apprise -vv --title &amp;#34;Borgmatic Error&amp;#34; --body &amp;#34;Could not run {output}&amp;#39;: &amp;#39;Aborted {error}.&amp;#34; ntfy://schallberts-topic&amp;#39;} is not of type &amp;#39;string&amp;#39;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Daran ist zu erkennen, dass die Interpretation von Zeichen oder Einrückungen schiefgelaufen ist und die Zeichensetzung zu prüfen ist. Alternativ kann auch der Pipe-Operator &lt;code&gt;|&lt;/code&gt; verwendet werden, um ein Kommando zu kombinieren. Referenz: &lt;a href="https://yaml.org/spec/1.2-old/spec.html#id2795688" target="_blank" rel="noopener noreferrer" class="external-link"&gt;yml-Spezifikation&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="server-1"&gt;Server&lt;/h3&gt;&#10;&lt;p&gt;Eine Anwendung von &lt;em&gt;ntfy&lt;/em&gt; zur Überwachung von Logins auf einem Server findet sich bereits in der &lt;a href="https://docs.ntfy.sh/examples/#ssh-login-alerts" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation von &lt;em&gt;ntfy&lt;/em&gt; selbst&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Die Beschreibung zeigt, dass so etwas leicht selbst umzusetzen ist und gibt unter Verwendung von Pluggable Authentication Modules (&lt;a href="https://en.wikipedia.org/wiki/Linux_PAM" target="_blank" rel="noopener noreferrer" class="external-link"&gt;PAM-Bibliothek&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;) ein tolles Beispiel.&lt;/p&gt;&#10;&lt;p&gt;Am Ende der Datei &lt;code&gt;sshd&lt;/code&gt; im Verzeichnis &lt;code&gt;etc/pam.d&lt;/code&gt; folgenden Code einfügen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Diese Zeile sagt &lt;em&gt;PAM&lt;/em&gt;, dass sie bei Öffnen einer &lt;code&gt;session&lt;/code&gt; per &lt;code&gt;ssh&lt;/code&gt; das ausführende Modul &lt;code&gt;pam_exec&lt;/code&gt; aufrufen soll, welches dann das hernach angegebene Skript abspult. Der Wert &lt;code&gt;optional&lt;/code&gt; bedeutet, dass die Konfigurationsdatei auch bei Fehlschlag der Aktion weiter durchlaufen werden soll. Mehr Details zum Umgang mit &lt;em&gt;PAM&lt;/em&gt; gibt es z.B. auf &lt;a href="https://www.baeldung.com/linux/pam-ssh-login-notifications" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Baeldung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Anschließend ist im Skript &lt;code&gt;ntfy-ssh-login.sh&lt;/code&gt; schlicht der &lt;em&gt;ntfy&lt;/em&gt;-Aufruf zu hinterlegen, wenn das Skript feststellt, dass PAM ein &lt;code&gt;open_session&lt;/code&gt; Event festgestellt hat.&lt;/p&gt;&#10;&lt;p&gt;Genau so habe ich es bei mir umgesetzt und es funktioniert auf Anhieb. Klasse! Der einzige Nachteil: Diese Modifikation habe ich direkt auf dem Server gemacht. Ohne Container und außerhalb meiner Konfigurations-Sicherung. Wenn ich den Server jetzt aus irgendeinem Grund neu aufsetzen muss, ist die Änderung im &lt;em&gt;PAM&lt;/em&gt; futsch und ich bekomme keine Benachrichtigungen mehr, bis ich die Änderung manuell erneut einpflege.&lt;/p&gt;&#10;&lt;h3 id="server-konfiguration"&gt;Server-Konfiguration&lt;/h3&gt;&#10;&lt;p&gt;Für das Senden eines Berichts nach Ausführen der Konfigurationsautomation muss ich eigentlich nur das im Rahmen des letzten Artikel verfasste &lt;a href="https://blog.schallbert.de/server-config-deploy/#die-fertige-automation"&gt;server-config-action&lt;/a&gt; Skript anpassen.&lt;/p&gt;&#10;&lt;p&gt;Zunächst möchte ich informiert werden, wenn das Skript fehlerfrei durchlief. Dafür füge ich am Ende der Datei einen &lt;em&gt;curl&lt;/em&gt;-Befehl ein.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# [...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# send success notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: low&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: white_check_mark&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;Rollout successful&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sollte das Skript allerdings nicht fehlerfrei durchlaufen, so möchte ich folgendes tun:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Aktionen nach Auftreten des Fehlers abbrechen&lt;/li&gt;&#10;&lt;li&gt;Ein Fehler-Log erstellen&lt;/li&gt;&#10;&lt;li&gt;Dieses Log versenden&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Den ersten Punkt kann ich durch Hinzufügen einer Falle (Englisch &amp;ldquo;Trap&amp;rdquo;) für Fehler erreichen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;trap &lt;span style="color:#e6db74"&gt;&amp;#39;handle_error $LINENO&amp;#39;&lt;/span&gt; ERR&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hiermit wird die Funktion &lt;code&gt;handle_error&lt;/code&gt; aufgerufen. Ihr wird die Zeilennummer mitgegeben, an der der Fehler &lt;code&gt;ERR&lt;/code&gt; auftrat. Damit decke ich auch Fehler ab, die beim Neustart der Container entstehen können. Das Fehler-Log erstelle ich mir durch Wegschreiben der Ausgabe der einzelnen Skriptbefehle. Dies erreiche ich mit der folgenden Zeile:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;exec 3&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; 1&amp;gt;server-config-action.log 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jeglicher Standard-Output &lt;code&gt;stdout&lt;/code&gt; soll mit diesem Befehl an den Dateideskriptor &lt;code&gt;3&lt;/code&gt;, hier angegeben mit der Logdatai &lt;code&gt;server-config-action-log&lt;/code&gt;, überschreibend weitergereicht werden. Für &amp;ldquo;Anhängen&amp;rdquo; müssten es zwei Umleitungsoperatoren &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; sein. Somit ersetzt die Logdatei die Konsolenausgabe, welche ich im vorigen Artikel noch an dieser Stelle hatte.&lt;/p&gt;&#10;&lt;p&gt;Um das Log zugesandt zu bekommen, definiere ich nun folgende Funktion am Anfang des Bash-Skripts:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# server-config-action.sh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# on error, send a notification&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;handle_error&lt;span style="color:#f92672"&gt;()&lt;/span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# stop redirecting to file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exec 1&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; 1&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; curl &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Title: server-config-action FAILED&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Priority: high&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Tags: x&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -T server-config-action.log &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -H &lt;span style="color:#e6db74"&gt;&amp;#34;Filename: server-config-action.log&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ntfy.sh/schallbert-server-config-push-topic&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; exit &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# set error trap&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# redirect stdout and stderr to file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# action commands...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="media-wrapper right"&gt;&#10; &lt;figure class="media-frame"&gt;&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-08-20_ntfytest_allchannels.jpg" alt="Image: Phone screenshot of my ntfy messages" /&gt;&lt;/figure&gt;&#10;&lt;/div&gt;&#10;&lt;h3 id="gitea-1"&gt;&lt;em&gt;Gitea&lt;/em&gt;&lt;/h3&gt;&#10;&lt;p&gt;Für Berichte von Github Actions gibt es bereits ein existierendes Beispiel auf &lt;a href="https://docs.ntfy.sh/examples/#github-actions" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy_examples/#github-actions&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Und das Beste: Da der &lt;em&gt;act_runner&lt;/em&gt; von &lt;em&gt;Gitea&lt;/em&gt; an den wesentlichen Stellen &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in den Umgebungsparametern&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; kompatibel ist, funktioniert das Senden an &lt;em&gt;ntfy&lt;/em&gt; bei mir auf Anhieb. In der Workflow-Datei des runners muss nur der auf der Website angegebene &lt;em&gt;curl&lt;/em&gt;-Befehl angegeben werden und fertig.&lt;/p&gt;&#10;&lt;h2 id="ergebnis"&gt;Ergebnis&lt;/h2&gt;&#10;&lt;p&gt;Hiermit sind jetzt fünf Benachrichtigungen über die wichtigsten Vorgänge auf meinem Server eingerichtet. Ich habe die zugrundeliegenden Mechaniken verstanden und kann jederzeit weitere Benachrichtigungen erstellen, wenn ich sie benötige. Löse ich alle Benachrichtigungen testweise aus, zeigt mein Handy das dargestellte Bild.&lt;/p&gt;&#10;&lt;p&gt;Eine Unabhängigkeit vom zu überwachenden System habe ich noch nicht erreicht. Die Benachrichtigungen gehen sämtlich von der betroffenen Maschine aus und es bestehen zum Teil &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/#reverse-proxy-connection-refused"&gt;sogar Abhängigkeiten&lt;/a&gt; zwischen Docker-Containern. Der &lt;em&gt;act_runner&lt;/em&gt; muss sich zum Beispiel über Websockets mit &lt;em&gt;Gitea&lt;/em&gt; verbinden. Und das funktioniert nur, wenn &lt;em&gt;Caddy&lt;/em&gt; den Reverse Proxy bereitstellt.&lt;/p&gt;&#10;&lt;p&gt;Stelle ich in den nächsten Monaten fest, dass ein Dienst &amp;ldquo;unter meinem Radar&amp;rdquo; nicht mehr arbeitsfähig ist, werde ich diese Unabhängigkeit herstellen müssen und entsprechend darüber berichten.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Kontext zum Setup mit &lt;a href="https://docs.ntfy.sh/#getting-started" target="_blank" rel="noopener noreferrer" class="external-link"&gt;ntfy&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Kontext zur &lt;a href="https://github.com/caronc/apprise/wiki/Notify_signal" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Signal-API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Bericht von &lt;code&gt;asad-awadia&lt;/code&gt; zur &lt;a href="https://blog.aawadia.dev/2023/04/24/signal-api/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Einrichtung der Signal-API&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;Healthchecks ist direkt verfügbar als &lt;a href="https://hub.docker.com/r/healthchecks/healthchecks" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Docker-Image&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;Übrigens sind die Operatoren &lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; und &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; für mich nicht schwer zu merken, denn früher habe ich viel mit der Programmiersprache &lt;code&gt;C&lt;/code&gt; zu tun gehabt. Dort - und in vielen anderen Programmiersprachen auch - sind dies Shift-Operatoren, die einen Wert bitweise &amp;ldquo;verschieben&amp;rdquo; bzw. ein Feld auf ein anderes schieben können. Bei den &lt;a href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_blank" rel="noopener noreferrer" class="external-link"&gt;RISC&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; Architekturen, die ich damals verwendet habe, war die Schiebe-Operation &amp;ldquo;billig&amp;rdquo;, also sehr schnell und speicherplatzschonend umgesetzt.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Den Server absichern</title><link>https://blog.schallbert.de/server-protection/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-protection/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-12-server-protection-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: side-by-side image of fail2ban and borgmatic logos"&#10; title="Den Server absichern" /&gt;&#10;&lt;h2 id="motivation"&gt;Motivation&lt;/h2&gt;&#10;&lt;p&gt;OK. Nun läuft mein Server, &lt;a href="https://blog.schallbert.de/gitea-action-runner-jekyll-dockerimage/"&gt;baut automatisch&lt;/a&gt; meinen Blog und &lt;a href="https://blog.schallbert.de/self-hosted-jekyll-page-broken-links/"&gt;zeigt ihn auch problemlos an&lt;/a&gt;. So ganz fertig bin ich aber noch nicht. Denn ich könnte mehr für die Sicherheit des Servers tun, als überall Passwort-Logins zu deaktivieren.&lt;/p&gt;&#10;&lt;p&gt;Außerdem habe ich noch keine Backups. Und das ist ja nie gut. Also, packen wir&amp;rsquo;s an!&lt;/p&gt;&#10;&lt;h2 id="unerwünschte-besucher-aussperren"&gt;Unerwünschte Besucher aussperren&lt;/h2&gt;&#10;&lt;p&gt;Ich bekomme in meinem SSH-Log im Root-Account des Servers einen Haufen Verbindungsversuche, die nicht von mir kommen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;29&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;31&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user admin from &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;19&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93842&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Connection&lt;/span&gt; closed by invalid user admin &lt;span style="color:#ae81ff"&gt;41&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;207&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;248&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;204&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;37194&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Invalid&lt;/span&gt; user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): check pass; user unknown&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: pam_unix(&lt;span style="color:#e6db74"&gt;sshd&lt;/span&gt;:auth): authentication failure; logname&lt;span style="color:#f92672"&gt;=&lt;/span&gt; uid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; euid&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; tty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ssh ru&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Failed&lt;/span&gt; password &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; invalid user svn from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; ssh2&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Received&lt;/span&gt; disconnect from &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Bye&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;Jan&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;00&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; sshd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;93886&lt;/span&gt;&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Disconnected&lt;/span&gt; from invalid user svn &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;108&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt; port &lt;span style="color:#ae81ff"&gt;44968&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;preauth&lt;span style="color:#f92672"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wenn ich nicht wüsste, dass dies das inzwischen übliche &amp;ldquo;Rauschen&amp;rdquo; im Internet ist, würde es mich schon etwas beunruhigen. Ist ja fast so, als würde alle paar Sekunden jemand mit bösen Absichten versuchen, irgendeinen Schlüssel an meiner Haustür auszuprobieren. Was kann man dagegen also tun? Wegschicken!&lt;/p&gt;&#10;&lt;h3 id="fail2ban"&gt;fail2ban&lt;/h3&gt;&#10;&lt;p&gt;Genau dies soll die Software &lt;a href="https://github.com/fail2ban/fail2ban" target="_blank" rel="noopener noreferrer" class="external-link"&gt;fail2ban&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; für mich übernehmen.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; __ _ _ ___ _ &#10; / _|__ _(_) |_ ) |__ __ _ _ _ &#10; | _/ _` | | |/ /| &amp;#39;_ \/ _` | &amp;#39; \ &#10; |_| \__,_|_|_/___|_.__/\__,_|_||_|&#10; v1.1.0.dev1 20??/??/??&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Vereinfacht gesagt durchforstet &lt;code&gt;fail2ban&lt;/code&gt; Zugriffslogs&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; nach IP-Adressen, für die fehlgeschlagene Anmeldeversuche registriert wurden, und &amp;ldquo;bannt&amp;rdquo; sie bei Überschreiten einer benutzerdefinierten Anzahl Versuche innerhalb einer bestimmten Zeit für einen gewünschten Zeitraum.&lt;/p&gt;&#10;&lt;p&gt;Wie dieser Bannspruch umgesetzt wird? Fail2ban modifiziert die &lt;a href="https://en.wikipedia.org/wiki/Iptables" target="_blank" rel="noopener noreferrer" class="external-link"&gt;iptables&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, greift also auf Paketfilterregeln zu (Stichwort Firewall), welche sich unten auf der Netzwerkschicht befinden. So werden hereinkommende Anfragen bereits geblockter Adressen gar nicht erst bis zu meinen Applikationen durchkommen&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;h3 id="fail2ban-installieren"&gt;fail2ban installieren&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;fail2ban&lt;/code&gt; scheint so etwas wie Industriestandard bei der Abwehr unerwünschter Zugriffsversuche auf Linux zu sein. Jedem Hobby- und Profiadmin den ich kenne war das Programm geläufig. Ich erntete für meine Unwissenheit des Öfteren ein müdes Lächeln.&lt;/p&gt;&#10;&lt;p&gt;Zu Installation und Konfiguration gibt es bereits einen Haufen Anleitungen da draußen plus der (sehr gut geschriebenen), die im Repo von Fail2ban gleich mitgeliefert wird. Daher gehe ich nicht besonders tief hierauf ein.&lt;/p&gt;&#10;&lt;p&gt;Ich entschied mich für die Installation im Docker-Container, damit ich die üblichen Abhängigkeiten gleich mitgeliefert bekomme. Dafür verwende ich die open-source Distribution von &lt;a href="https://docs.linuxserver.io/images/docker-fail2ban/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;linuxserver&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und verfasse die folgende &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /fail2ban/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;2.1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fail2ban&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;lscr.io/linuxserver/fail2ban:latest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;fail2ban&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cap_add&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;NET_ADMIN&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;NET_RAW&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;network_mode&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;host&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;PUID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;PGID=1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;TZ=Etc/UTC&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;VERBOSITY=-vv&lt;/span&gt; &lt;span style="color:#75715e"&gt;#optional&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./config:/config&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/log/auth.log:/var/log/auth.log:ro&lt;/span&gt; &lt;span style="color:#75715e"&gt;# host ssh&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;/var/log/caddy2:/var/log/caddy2:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# gitea via caddy, caddyserver&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unless-stopped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das Einzige, was hier zu beachten gilt: &lt;code&gt;fail2ban&lt;/code&gt; benötigt die Access logs per Volume zur Verfügung gestellt (oben als &lt;code&gt;:ro&lt;/code&gt; nur mit Lesezugriff angegeben). Eine Menge Filterregeln liegen bereits vorkonfiguriert im &lt;code&gt;config&lt;/code&gt;-Ordner, daher muss ich nur die &lt;code&gt;jail.local&lt;/code&gt; &lt;a href="https://github.com/linuxserver/fail2ban-confs" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nach &lt;code&gt;linuxserver/fail2ban-confs&lt;/code&gt;&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; sowie &lt;code&gt;filter.d&lt;/code&gt; für Caddy &lt;a href="https://muetsch.io/how-to-integrate-caddy-with-fail2ban.html" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nach muetsch.io&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; anpassen und schon habe ich einen automatischen Türwächter.&lt;/p&gt;&#10;&lt;h3 id="fail2ban-beispiel"&gt;fail2ban Beispiel&lt;/h3&gt;&#10;&lt;p&gt;So sieht ein fail2ban-Log nun für meinen SSH-Daemon aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# schallbert:/opt/fail2ban/config/log/fail2ban# grep &amp;#34;220.124.89.47&amp;#34; fail2ban.log &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;600&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;23&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;003&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;22&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;25&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;205&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;24&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;27&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;206&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;26&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;610&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB3630BB38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Found&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2024&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;01&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;045&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;FBB36104B38&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOTIC&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sshd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Ban&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;220&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;124&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;89&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;47&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Und tschüss!&lt;/p&gt;&#10;&lt;h3 id="was-noch-nicht-funktioniert-gitea--fail2ban"&gt;Was (noch) nicht funktioniert: Gitea &amp;amp; fail2ban&lt;/h3&gt;&#10;&lt;p&gt;Ich bekomme auch auf meiner Gitea-Instanz ssh-Anfragen rein, die ich ebenfalls gern wegblocken möchte. Allerdings kann ich Gitea bis jetzt partout nicht dazu bekommen, die Logs dafür auch in eine Datei zu schreiben. Bis jetzt werden die stets an die Konsole geschickt, wo ich sie fail2ban natürlich nicht zuführen kann und möchte. Tatsächlich wird die Verbindung zu Gitea per SSH zum Glück bereits gesichert, da sie über denselben &lt;code&gt;sshd&lt;/code&gt; geht wie der Shell-Zugriff auf meinen Server selbst.&lt;/p&gt;&#10;&lt;p&gt;Was mir noch fehlt sind nur die Überwachung fehlgeschlagener Logins auf dem Web-Frontend.&lt;/p&gt;&#10;&lt;p&gt;Dabei sieht Giteas &lt;code&gt;app.ini&lt;/code&gt; für mich sauber aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# gitea/conf/app.ini&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[log]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;warn&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ROOT_PATH&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/data/gitea/log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_ACCESS_LOGS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ENABLE_SSH_LOG&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;logger.access.MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access-file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[log.access-file]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;MODE&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ACCESS&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;LEVEL&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;info&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;FILE_NAME&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;access.log&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ich habe sowohl die Access-Logs aktiviert &lt;code&gt;ENABLE_ACCESS_LOGS&lt;/code&gt; und den Access-Logger in eine Datei schreiben lassen. Die Logs werden auch erstellt, nur sind da keine Zugriffe drin aufgelistet - diese gehen nach wie vor in die Konsole des Containers. Ich bin mir aber noch unsicher, ob der Reverse-Proxy von Caddy hier vielleicht einen Einfluss hat und er zum Beispiel die Zugriffe vor Gitea bereits abfängt. Aber das finde ich schon irgendwann noch heraus.&lt;/p&gt;&#10;&lt;h3 id="update-aug-2024"&gt;Update Aug-2024&lt;/h3&gt;&#10;&lt;p&gt;Irgendwann fragte ich mich, ob &lt;em&gt;fail2ban&lt;/em&gt; auch tatsächlich auf den iptables meines Servers arbeitet oder nur innerhalb des Containers korrekt funktioniert. Zum Glück war ich &lt;a href="https://stackoverflow.com/questions/59996070/how-to-understand-if-the-fail2ban-ssh-filter-is-working-with-a-new-port" target="_blank" rel="noopener noreferrer" class="external-link"&gt;nicht der Erste mit dieser Frage (stackoverflow)&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und fand die vorgeschlagene Lösung ganz charmant:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;im Docker-Container eine beliebige IP sperren (&lt;code&gt;docker exec -it fail2ban sh&lt;/code&gt;): &lt;code&gt;fail2ban-client set sshd banip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;In den iptables nachsehen, ob diese IP dort auftaucht: &lt;code&gt;iptables -n -L --line-numbers&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;In meinem Falle: freuen, denn sie ist da: &lt;code&gt;1 REJECT all -- 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Die IP wieder entsperren &lt;code&gt;fail2ban-client set sshd unbanip 111.111.111.111&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="update-jul-2025"&gt;Update Jul-2025&lt;/h3&gt;&#10;&lt;p&gt;Ich habe jetzt eine Lösung für &lt;em&gt;Gitea&lt;/em&gt;, aber auch alle anderen meiner Websites zustande gebracht:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Schutz vor Brute-Force Angriffen per &lt;code&gt;ssh&lt;/code&gt; wie oben beschrieben&lt;/li&gt;&#10;&lt;li&gt;Schutz vor Überlast mittels &lt;a href="https://blog.schallbert.de/fail2ban-with-caddy/"&gt;Rate Limiter&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Schutz vor Angriffen auf die APIs (&lt;code&gt;404/403&lt;/code&gt; Attacken) mittels &lt;code&gt;caddy-status&lt;/code&gt; Konfiguration für fail2ban, Logs in &lt;code&gt;JSON&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /config/fail2ban/filter.d/caddy-status.conf&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# this regex works for caddy with json-style logs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;failregex = &amp;#34;client_ip&amp;#34;:&amp;#34;&amp;lt;HOST&amp;gt;&amp;#34;(.*)&amp;#34;status&amp;#34;:(400|401|403|404|500)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;datepattern = \d+&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;ignoreregex =&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="regelmäßige-backups"&gt;Regelmäßige Backups&lt;/h2&gt;&#10;&lt;p&gt;Ein wichtiger Aspekt für mich ist die Möglichkeit, den Server wiederherstellen zu können. Sollten unvorhergesehene Ereignisse eintreten wie ein Update einer Komponente welches die Funktion anderer Programme beeinträchtigt, der Ausfall des Servers, oder sogar der Zugriff fremder Personen auf selbigen - in jedem Falle könnte ich mit einem Backup recht schnell eine neue Instanz des Servers erzeugen, konfigurieren und die Website wieder zum Laufen bringen.&lt;/p&gt;&#10;&lt;p&gt;Diese Updates möchte ich aber ungern selbst von Hand aus meinen Ordnern erzeugen, komprimieren und per &lt;code&gt;SCP / SFTP&lt;/code&gt; herunterladen. Besser soll das vollautomatisch vonstatten gehen, und das im Optimalfall noch kostenfrei. Nach einer kurzen Recherche stellen sich für meine Zwecke die quelloffenen Werkzeuge &lt;a href="https://torsion.org/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;borgmatic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und &lt;a href="https://restic.net/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;restic&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; als geeignet heraus.&lt;/p&gt;&#10;&lt;p&gt;Ich entscheide mich willkürlich für Borgmatic.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-in-docker-installieren"&gt;Borgmatic in Docker installieren&lt;/h3&gt;&#10;&lt;p&gt;Wie bei allen anderen Komponenten auch, möchte ich Borgmatic in einem Container laufen lassen. Glücklicherweise gibt es da bereits &lt;a href="https://hub.docker.com/r/b3vis/borgmatic/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;eine fertige Lösung&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, die ich nur noch ein wenig konfigurieren muss.&lt;/p&gt;&#10;&lt;p&gt;Mein &lt;code&gt;docker-compose&lt;/code&gt; File für Borgmatic sieht wie folgt aus:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yml" data-lang="yml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /borgmatic/docker-compose.yml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;services&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;borgmatic&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;ghcr.io/borgmatic-collective/borgmatic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;container_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;borgmatic&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_SOURCE}:/mnt/source:ro &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup source&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_TARGET}:/mnt/repository &lt;/span&gt; &lt;span style="color:#75715e"&gt;# backup target&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_ETC_BORGMATIC}:/etc/borgmatic.d/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# borgmatic config file(s) + crontab.txt&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_BORG_CONFIG}:/root/.config/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# config and keyfiles&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_SSH}:/root/.ssh &lt;/span&gt; &lt;span style="color:#75715e"&gt;# ssh key for remote repositories&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;${VOLUME_BORG_CACHE}:/root/.cache/borg &lt;/span&gt; &lt;span style="color:#75715e"&gt;# checksums used for deduplication&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# - /var/run/docker.sock:/var/run/docker.sock # add docker sock so borgmatic can start/stop containers to be backupped&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;environment&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;TZ=${TZ}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;BORG_PASSPHRASE=${BORG_PASSPHRASE}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;restart&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;always&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Als Inspiration hierfür habe ich reichlich in der &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/README.md" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Dokumentation auf Github&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; gestöbert.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-konfigurieren"&gt;Borgmatic konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Alle konkreten Daten (die Angaben in &lt;code&gt;${}&lt;/code&gt;) habe ich zur besseren Übersichtlichkeit in einer &lt;code&gt;.env&lt;/code&gt;-Datei im selben Verzeichnis abgelegt. Ganz nebenbei verhindere ich, aus Versehen Passphrases zu veröffentlichen. Diese Passphrase habe ich mir zusätzlich auf einem Zettel notiert - man weiß ja nie, ob man sie nochmal benötigt.&lt;/p&gt;&#10;&lt;p&gt;Ich muss anschließend nur noch die sich in dem Ordner &lt;code&gt;borgmatic.d/&lt;/code&gt; befindliche &lt;code&gt;config.yml&lt;/code&gt; leicht verändern, indem ich Backup-Quelle, -Ziel und die von mir gewünschten Zeitintervalle für die Sicherungserstellung eintrug und war quasi schon bereit für einen ersten Test.&lt;/p&gt;&#10;&lt;h3 id="borgmatic-funktionstest"&gt;Borgmatic Funktionstest&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;cd &amp;amp;&amp;amp; borgmatic --stats -v 1 --files 2&amp;gt;&amp;amp;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mit diesem Befehl führe ich über Docker im Container &lt;code&gt;borgmatic&lt;/code&gt; den Befehl zum Anlegen einer Sicherung an, um die Funktion von Borgmatic zu verifizieren.&lt;/p&gt;&#10;&lt;p&gt;Hier kam dann direkt eine Fehlermeldung à la &lt;code&gt;repository does not exist&lt;/code&gt; zurück. Also existiert das Backup-Ziel noch gar nicht. Ein kurzer Blick in die Dokumentation zeigt, dass das Repository erst initialisiert werden muss.&lt;/p&gt;&#10;&lt;p&gt;Dies hole ich mit dem Kommando&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec borgmatic bash -c &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;borgmatic init --encryption repokey-blake2&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;nach, was mit ein &amp;ldquo;leeres&amp;rdquo;, verschlüsseltes Repository anlegt.&#10;Versuche ich erneut, ein Backup zu erzeugen, läuft Borgmatic nun durch und erzeugt mir das Backup im Repository.&lt;/p&gt;&#10;&lt;h3 id="ein-backup-ist-nur-ein-backup"&gt;Ein Backup ist nur ein Backup&amp;hellip;&lt;/h3&gt;&#10;&lt;p&gt;&amp;hellip;wenn man es einspielen kann, sagt mein Kumpel.&lt;/p&gt;&#10;&lt;p&gt;Er hat Recht, aber so richtig traue ich mich nicht, über meine funktionierende Serverkonfiguration drüberzubügeln. Also erstelle ich mir eine &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; Datei nach &lt;a href="https://github.com/borgmatic-collective/docker-borgmatic/blob/master/docker-compose.restore.yml" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Vorbild auf dem Repository&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; und ziehe &lt;a href="https://www.modem7.com/books/docker-backup/page/backup-docker-using-borgmatic" target="_blank" rel="noopener noreferrer" class="external-link"&gt;modem7s Anleitung zum Thema&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; hinzu.&lt;/p&gt;&#10;&lt;p&gt;Tatsächlich kann ich durch Ausführen der &lt;code&gt;docker-compose.restore.yml&lt;/code&gt; dann in der Container-Shell folgende Kommandos ausführen:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir backuprestoremount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borg mount /mnt/repository /backuprestoremount&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic extract --archive latest --destination /backuprestore&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hier erstelle ich den Ordner &lt;code&gt;backuprestoremount&lt;/code&gt; und lasse ihn von &lt;code&gt;borg&lt;/code&gt; auf mein Backup zeigen. Anschließend extrahiere ich das Backup in den ebenfalls neu erstellten Ordner &lt;code&gt;backuprestore&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Nun prüfe ich, ob auch &amp;ldquo;alles da ist&amp;rdquo;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cd /mnt/backuprestore&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /restore/mnt/source ls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;borgmatic caddy2 containerd fail2ban gitea hostedtoolcache watchtower&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yeah, alle Applikationen sind vorhanden und die enthaltenen Daten sind gesichert! Weiter gehe ich jetzt mal nicht, denn ich bin zu feige (und zu faul), die auf dem Server unter &lt;code&gt;/opt&lt;/code&gt; befindlichen Container zu überschreiben.&lt;/p&gt;&#10;&lt;h3 id="was-noch-nicht-funktioniert-borgmatic--docker-compose-down"&gt;Was (noch) nicht funktioniert: Borgmatic &amp;amp; docker-compose down&lt;/h3&gt;&#10;&lt;p&gt;Borgmatic bietet eine einfache Möglichkeit in der Konfigurationsdatei, Aktionen vor- und nach dem Backup auszuführen.&lt;/p&gt;&#10;&lt;p&gt;Da ich die Konsistenz der Datenbanken meiner zu sichernden Container sicherstellen möchte, sollten die Container zum Zeitpunkt des Backups heruntergefahren sein. Daher schrieb ich mir ein Skript, was alle Applikationen vor dem Backup per &lt;code&gt;docker compose down&lt;/code&gt; stoppt und nach dem Backup wieder hochfährt.&lt;/p&gt;&#10;&lt;p&gt;Dies würde auch alles prima funktionieren, wenn Borgmatic nicht selbst in einem Container liefe. Durch die Kapselung bin ich nun nicht in der Lage, die im Stamm-Dateisystem der Maschine befindlichen Skripte auszuführen. Kurzzeitig dachte ich, dass ich dies mittels Durchreichung von &lt;code&gt;var/run/docker.sock&lt;/code&gt; in den Borgmatic-Container umgehen könne, scheiterte dann aber an der Ankopplung an Docker compose.&lt;/p&gt;&#10;&lt;p&gt;Ich bin mir sicher, dass das zu lösen ist. Allerdings habe ich gerade dringendere Themen. Daher begnüge ich mich mit der Annahme, dass schon nichts passiert, wenn ich das Backup nachts ziehe&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#10;&lt;p&gt;Auch bei den Auto-Backups scheint es noch zu haken:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;/ # borgmatic --stats&#10;Starting a backup job.&#10;Failed to create/acquire the lock /mnt/repository/lock (timeout).&#10;local: Error running actions for repository&#10;Command &amp;#39;borg create --stats /mnt/repository::{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f} /etc/borgmatic.d/config.yml /mnt/source /root/.borgmatic&amp;#39; returned non-zero exit status 2.&#10;Error while creating a backup.&#10;/etc/borgmatic.d/config.yml: An error occurred&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Dies entnehme ich der über Docker aufgerufenen Log-Konsole von Borgmatic. Ich muss also beizeiten nochmal um meine Backups kümmern.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;Beim SSH-Daemon meines Servers liegen die Zugriffslogs z.B. in &lt;code&gt;/var/log/auth.log&lt;/code&gt;, können aber auch als &lt;code&gt;access.log&lt;/code&gt; oder ähnlich abgelegt werden. Auch von mir verwendete Dienste wie Gitea und Caddy legen solche Logdateien an.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;Was ich allerdings noch nicht verstehe ist, wie &lt;code&gt;fail2ban&lt;/code&gt; im Container laufend überhaupt an die iptables herankommt. Ich dachte, dass ein positiver Nebeneffekt der Containerisierung in der Kapselung liegt. Oder ist sie nur bei &amp;ldquo;rootless&amp;rdquo;-Containern zu erreichen?&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;Hintergrund: Ich bin zur Zeit der Einzige, der Inhalte auf &lt;code&gt;gitea&lt;/code&gt; hochlädt. Meine Webseite ist statisch. Auf dem Webserver &lt;code&gt;caddy&lt;/code&gt; ändern sich also nur dann Dateien, wenn ich sie über Gitea hochlade. Programmupdates über &lt;code&gt;watchtower&lt;/code&gt; und &lt;code&gt;unattended-upgrades&lt;/code&gt; &lt;a href="https://blog.schallbert.de/server-auto-upgrade/"&gt;(Thema eines weiteren Artikels)&lt;/a&gt; habe ich so gelegt, dass sie nicht während des Durchlaufs von &lt;code&gt;borgmatic&lt;/code&gt; stattfindet. Einzig die Logs, welche bei &lt;code&gt;fail2ban&lt;/code&gt; aufschlagen, werden auch zur Zeit der Erstellung des Backups geschrieben. Das Risiko des Datenverlustes gehe ich an der Stelle bewusst aber ein, da die Paketfilterung für geblockte IP-Adressen durch fail2ban nach einer von mir bestimmten Zeit sowieso wieder aufgehoben wird.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item></channel></rss>