<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Fail2ban on Schallberts Blog</title><link>https://blog.schallbert.de/tags/fail2ban/</link><description>Recent content in Fail2ban on Schallberts Blog</description><generator>Hugo</generator><language>de</language><lastBuildDate>Thu, 23 Oct 2025</lastBuildDate><atom:link href="https://blog.schallbert.de/tags/fail2ban/index.xml" rel="self" type="application/rss+xml"/><item><title>fail2ban: Update zerstört Konfiguration</title><link>https://blog.schallbert.de/fail2ban-error-configuration-bad-value/</link><pubDate>Thu, 23 Oct 2025</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;a href="https://github.com/sebres" target="_blank" rel="noopener noreferrer" class="external-link"&gt;sebres&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, Dec-2024&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>Rate limiter für Gitea</title><link>https://blog.schallbert.de/gitea-rate-limiter/</link><pubDate>Fri, 25 Jul 2025</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;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fehlerbehebung &amp;#39;Bad value substitution&amp;#39;&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 09. Okt. 2025&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; 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;lsquo;Bad value substitution&amp;rsquo; 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.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&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</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;figure class="media-frame media-frame--center"&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;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;figure class="media-frame media-frame--center"&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;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Fehlerbehebung &amp;#39;Bad value substitution&amp;#39;&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2025-10-09T00:00:00Z"&gt;&#10; 09. Okt. 2025&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; 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;lsquo;Bad value substitution&amp;rsquo; 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.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;</description></item><item><title>Automatische Updates für alles!</title><link>https://blog.schallbert.de/server-auto-upgrade/</link><pubDate>Sat, 20 Jan 2024</pubDate><author>Schallbert</author><guid>https://blog.schallbert.de/server-auto-upgrade/</guid><description type="html">&#10; &lt;img src="https://blog.schallbert.de/assets/images/posts/2024-01-20-server-auto-upgrade-thumb.jpg"&#10; class="post-cover"&#10; alt="Image: unattended-upgrades and Watchtower keep my server and application packages up to date."&#10; title="Automatische Updates für alles!" /&gt;&#10;&lt;p&gt;Wie der Titel schon sagt, möchte ich Sicherheitsupdates gern installiert haben, sobald sie erscheinen. Dies gilt gleichsam für das Ubuntu meines Cloud-Servers wie auch für alle Applikationen, die ich in Docker betreibe. Da ich faul bin, möchte ich diese Sicherheitsupdates nicht manuell vornehmen, sondern vollautomatisch ablaufen lassen.&lt;/p&gt;&#10;&lt;p&gt;Für Funktionsupdates und -Erweiterungen hingegen ist es auch weiterhin OK für mich, von Zeit zu Zeit manuell einzugreifen.&#10;Beginnen wir also mit den Systemupgrades.&lt;/p&gt;&#10;&lt;h2 id="sicherheitsupdates-per-unattended-upgrades"&gt;Sicherheitsupdates per &lt;code&gt;unattended-upgrades&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;Ubuntu stellt - wie eine kurze &lt;a href="https://askubuntu.com/questions/9/how-do-i-enable-automatic-updates" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Suchmaschinenabfrage ergibt&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; - bereits ein konfigurierbares Werkzeug für solche automatisierten Updates zur Verfügung: &lt;code&gt;unattended-upgrades&lt;/code&gt;. Ich prüfe zuerst, ob es bereits auf meinem System vorhanden ist:&lt;/p&gt;&#10;&lt;h3 id="installation"&gt;Installation&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;schallbert:~# which unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/usr/bin/unattended-upgrades&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prima, es ist bereits da. Ansonsten hätte ich die Installation schnell mit &lt;code&gt;apt install unattended-upgrades&lt;/code&gt; nachgeholt.&lt;/p&gt;&#10;&lt;h3 id="unattended-upgrades-konfigurieren"&gt;unattended-upgrades konfigurieren&lt;/h3&gt;&#10;&lt;p&gt;Hier hilft das &lt;a href="https://wiki.debian.org/UnattendedUpgrades" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Debian Wiki&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; weiter: Die Konfigurationsdateien liegen in &lt;code&gt;/etc/apt/apt.conf.d&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Zuerst gehe ich sicher, dass automatische Updates aktiviert sind. Hierfür prüfe ich die Datei &lt;code&gt;20auto-upgrades&lt;/code&gt; und stelle sicher, dass Paketindex-Updates und Upgradeinstallation aktiviert sind:&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;APT::Periodic::Update-Package-Lists &lt;span style="color:#e6db74"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;APT::Periodic::Unattended-Upgrade &lt;span style="color:#e6db74"&gt;&amp;#34;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;Sehr gut. Jetzt sollte ich noch festlegen, zu welchem Zeitpunkt das Tool loslaufen und welcher Typ Updates installiert werden soll. Hierfür öffne ich die Datei &lt;code&gt;50unattended-upgrades&lt;/code&gt; und entferne die Kommentare bei den entsprechenden Zeilen.&lt;/p&gt;&#10;&lt;h3 id="updatequellen-festlegen"&gt;Updatequellen festlegen&lt;/h3&gt;&#10;&lt;p&gt;Der erste Abschnitt der Konfigurationsdatei widmet sich der erlaubten Quellen für die Updates. Hier sollten selbstverständlich nur vertrauenswürdige Provider stehen. Meine Konfig sieht hier 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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Allowed-Origins &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:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&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; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-security&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:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESMApps:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-apps-security&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:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_id&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;ESM:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;distro_codename&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;-infra-security&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;}&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Erlaubt werden hier Quellen für die von mir verwendete Linux-Distribution im Allgemeinen und Sicherheitsupdates inklusive &lt;code&gt;ESM&lt;/code&gt;, was für &amp;ldquo;Extended Security Maintenance&amp;rdquo; steht.&lt;/p&gt;&#10;&lt;h3 id="updates-auch-für-dev-releases"&gt;Updates auch für Dev Releases?&lt;/h3&gt;&#10;&lt;p&gt;Die folgende Zeile legt fest, ob auch Vorabversionen von Ubuntu Updates erhalten sollen. Bei mir steht der Wert auf &lt;code&gt;auto&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::DevRelease &lt;span style="color:#e6db74"&gt;&amp;#34;auto&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="automatischer-neustart"&gt;Automatischer Neustart&lt;/h3&gt;&#10;&lt;p&gt;Einige Sicherheitsupdates (z.B. den &lt;a href="https://en.wikipedia.org/wiki/Kernel_%28operating_system%29" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Kernel&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; betreffend) erfordern einen Neustart der Maschine. Sie werden daher nur in Kraft treten, wenn man in der Datei entsprechende Werte gesetzt hat. Dabei ist &lt;code&gt;WithUsers&lt;/code&gt; natürlich Geschmackssache, denn man wird ausgeloggt, falls zufälligerweise gerade ein Update installiert wurde, was einen Neustart erfordert. Um das praktisch auszuschließen, habe ich unter &lt;code&gt;Reboot-Time&lt;/code&gt; eine Zeit eingetragen, bei der ich sicher nicht an der Maschine hocke.&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;Unattended-Upgrade::Automatic-Reboot &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-WithUsers &lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Unattended-Upgrade::Automatic-Reboot-Time &lt;span style="color:#e6db74"&gt;&amp;#34;03:45&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Unangekündigte Neustarts haben natürlich Rückwirkung auf alle Programme, die ich verwende. Daher stelle ich in meinen &lt;code&gt;docker-compose.yml&lt;/code&gt; Dateien sicher, dass dort bei &lt;code&gt;restart:&lt;/code&gt; auch wirklich &lt;code&gt;always&lt;/code&gt; oder &lt;code&gt;unless-stopped&lt;/code&gt; notiert ist.&lt;/p&gt;&#10;&lt;h3 id="benachrichtigungen"&gt;Benachrichtigungen&lt;/h3&gt;&#10;&lt;p&gt;Automatische Mails oder anderweitige Push-Benachrichtigungen habe ich erst einmal nicht eingeschaltet, da ich mich noch im Testbetrieb befinde und keine große Lust auf einen Haufen weiterer Nachrichten habe. Sobald ich dies ändere, gibt es hier ganz bestimmt ein Update.&lt;/p&gt;&#10;&lt;h3 id="funktionsprüfung"&gt;Funktionsprüfung&lt;/h3&gt;&#10;&lt;p&gt;Um zu sehen, ob unattended-upgrades tut was es soll, schaue ich ein paar Tage später mal in die Logs unter &lt;code&gt;/var/log/unattended-upgrades&lt;/code&gt;.&#10;Hier steht in der Datei &lt;code&gt;unattended-upgrades.log&lt;/code&gt; zum Beispiel folgendes:&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;# [...]&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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Starting&lt;/span&gt; unattended upgrades script&#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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Allowed&lt;/span&gt; origins &lt;span style="color:#e6db74"&gt;are&lt;/span&gt;: o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;Ubuntu&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;UbuntuESMApps&lt;/span&gt;,a&lt;span style="color:#f92672"&gt;=&lt;/span&gt;jammy&lt;span style="color:#f92672"&gt;-&lt;/span&gt;apps&lt;span style="color:#f92672"&gt;-&lt;/span&gt;security, o&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:#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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;781&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; &lt;span style="color:#e6db74"&gt;blacklist&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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;17&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;59&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;782&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Initial&lt;/span&gt; whitelist (&lt;span style="color:#f92672"&gt;not&lt;/span&gt; strict):&#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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Packages&lt;/span&gt; that will be &lt;span style="color:#e6db74"&gt;upgraded&lt;/span&gt;: libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev&lt;span style="color:#f92672"&gt;-&lt;/span&gt;bin libc&lt;span style="color:#f92672"&gt;-&lt;/span&gt;devtools libc6 libc6&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dev locales python3&lt;span style="color:#f92672"&gt;-&lt;/span&gt;twisted&#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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;33&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;956&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;Writing&lt;/span&gt; dpkg log to &lt;span style="color:#e6db74"&gt;/var/&lt;/span&gt;log&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;/&lt;/span&gt;unattended&lt;span style="color:#f92672"&gt;-&lt;/span&gt;upgrades&lt;span style="color:#f92672"&gt;-&lt;/span&gt;dpkg&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;&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;12&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;06&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;14&lt;/span&gt;:&lt;span style="color:#ae81ff"&gt;54&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;578&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INFO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;All&lt;/span&gt; upgrades installed&#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 Datei &lt;code&gt;unattended-upgrades-shutdown.log&lt;/code&gt; ist allerdings noch leer. Ich werde zu einem späteren Zeitpunkt prüfen, ob hier auch alles funktioniert.&lt;/p&gt;&#10;&lt;h2 id="container-updates-mit-watchtower-automatisieren"&gt;Container-Updates mit &lt;code&gt;Watchtower&lt;/code&gt; automatisieren&lt;/h2&gt;&#10;&lt;p&gt;Nun zu den Updates meiner Applikationen. Ein kurzes Gespräch mit ein paar Admins ergab:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Es ist mühsam, alle Programme von Hand auf Stand zu halten&lt;/li&gt;&#10;&lt;li&gt;Für Anwendungen im Container gibt es Dienste, die diese Aufgabe zentral lösen&lt;/li&gt;&#10;&lt;li&gt;Es gab bereits schlechte Erfahrungen, wenn in der &lt;code&gt;docker-compose.yml&lt;/code&gt; auf den aktuellsten Release referenziert wird&lt;/li&gt;&#10;&lt;li&gt;Bei &lt;code&gt;image: &amp;lt;application&amp;gt;:latest&lt;/code&gt; seien hier nicht alle Anwendungen stabil bzw. erzeugten Probleme mit Abhängigkeiten&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Einer Lösung für dieses Problem bietet &lt;a href="https://containrrr.dev/watchtower/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Watchtower&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;, welches in meinem Falle selbst wieder in einem Container läuft.&lt;/p&gt;&#10;&lt;h3 id="installation-1"&gt;Installation&lt;/h3&gt;&#10;&lt;p&gt;Wie üblich schreibe ich mir in einem neuen Ordner unter &lt;code&gt;opt/watchtower&lt;/code&gt; eine &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;# copied from https://containrrr.dev/watchtower/notifications/&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;# watchtower/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;3&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;watchtower&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;containrrr/watchtower&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;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;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Watchtower benötigt &lt;code&gt;docker.sock&lt;/code&gt;, um die Updates zu ziehen und auf den vorhandenen Containern anzuwenden.&#10;Ansonsten habe ich hier erst einmal nichts weiter unternommen. Keine Benachrichtigungen (aus obengenannten Gründen), keine Logs, nichts. Ich hoffe, dieses Setup bleibt geräuschlos und ich muss mich hier lange nicht mit einem &lt;em&gt;Update&lt;/em&gt; zu Wort melden.&lt;/p&gt;&#10;&lt;aside class="update-box update-box--warn" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ⚠️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: (haha) neue Container-Quelle für watchtower&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-02-20T00:00:00Z"&gt;&#10; 20. Feb. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Die Watchtower-Lösung von &lt;em&gt;containrrr&lt;/em&gt; funktioniert auf meinem System nicht mehr. Sie scheint nicht mehr gewartet zu werden. Also wechsele ich in der &lt;code&gt;docker-compose.yml&lt;/code&gt; zu: &lt;code&gt;image: nickfedor/watchtower:latest&lt;/code&gt;. Langfristig aber ist die Übergabe des docker sock an einen Container &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-1-do-not-expose-the-docker-daemon-socket-even-to-the-containers" target="_blank" rel="noopener noreferrer" class="external-link"&gt;gar keine gute Idee&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;. Auf meinem Zettel steht bereits, dafür eine weniger vulnerable Lösung per Skript zu finden.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; Update: Watchtower durch Skript abgelöst&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2026-11-01T00:00:00Z"&gt;&#10; 01. Nov. 2026&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ich habe automatische Image-Updates meiner Container durch ein Skript eingeführt. Mein Vorgehen beschreibe ich im Artikel &lt;a href="https://blog.schallbert.de/auto-upgrade-docker-images/"&gt;auto-upgrade docker images&lt;/a&gt;&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&gt;&#10;&lt;p&gt;Weitergehende Schritte für die Validierung der Funktion von Watchtower finden sich z.B. &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-automatically-update-docker-container-images-with-watchtower-on-ubuntu-22-04" target="_blank" rel="noopener noreferrer" class="external-link"&gt;in einem Tutorial von DigitalOcean&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="ablauf"&gt;Ablauf&lt;/h3&gt;&#10;&lt;p&gt;Watchtower prüft die Installierten Docker images &lt;a href="https://containrrr.dev/watchtower/introduction/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;lt. Doku täglich&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; auf Updates. Sollte des ein neues Image-release geben, schickt Watchtower ein &lt;code&gt;SIGTERM&lt;/code&gt;-Signal an upzudatende Container, worauf sich selbige herunterfahren (&lt;a href="https://containrrr.dev/watchtower/stop-signals/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Quelle&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt;). Anschließend werden die Container wieder hochgefahren.&lt;/p&gt;&#10;&lt;h3 id="unfreiwillige-funktionsprüfung"&gt;Unfreiwillige Funktionsprüfung&lt;/h3&gt;&#10;&lt;p&gt;Ein paar Tage nach Start des Containers wählte ich mich nochmal in meinen Server ein und sah auf gut Glück ein paar Logs durch. Ich wollte nur kurz nach dem Rechten schauen.&lt;/p&gt;&#10;&lt;p&gt;In den &lt;code&gt;fail2ban&lt;/code&gt; Logdateien sah ich zu einem bestimmten Zeitpunkt, Uhrzeit &lt;code&gt;14:42:56&lt;/code&gt; plötzlich seitenweise Einträge. Mehr davon, als ich die Tage vorher insgesamt gesehen hatte. Als ich zum Beginn dieser Kette von Einträgen hochscrollte, sah ich, dass fail2ban mir wohl kurz ausgestiegen war.&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Shutdown in progress...&#10; 2024-01-26 14:42:50,817 &amp;lt;container_id&amp;gt; INFO Observer stop ... try to end queue 5 seconds&#10; 2024-01-26 14:42:50,837 &amp;lt;daemon_id&amp;gt; INFO Observer stopped, 0 events remaining.&#10; 2024-01-26 14:42:50,878 &amp;lt;container_id&amp;gt; INFO Stopping all jails&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/auth.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/gitea_access.log&amp;#39;&#10; 2024-01-26 14:42:50,879 &amp;lt;container_id&amp;gt; INFO Removed logfile: &amp;#39;/var/log/caddy2/server_access.log&amp;#39;&#10; 2024-01-26 14:42:51,052 &amp;lt;daemon_id&amp;gt; NOTIC [sshd] Flush ticket(s) with iptables&#10; 2024-01-26 14:42:51,063 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;sshd&amp;#39; stopped&#10; 2024-01-26 14:42:51,137 &amp;lt;daemon_id&amp;gt; NOTIC [caddy-status] Flush ticket(s) with iptables-multiport&#10; 2024-01-26 14:42:51,137 &amp;lt;container_id&amp;gt; INFO Jail &amp;#39;caddy-status&amp;#39; stopped&#10; 2024-01-26 14:42:51,138 &amp;lt;container_id&amp;gt; INFO Connection to database closed.&#10; 2024-01-26 14:42:51,139 &amp;lt;container_id&amp;gt; INFO Exiting Fail2ban&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO --------------------------------------------------&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Starting Fail2ban v1.0.2&#10; 2024-01-26 14:42:56,173 &amp;lt;new_container_id&amp;gt; INFO Observer start...&#10;[...]&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Die ganzen Log-Einträge zur selben Uhrzeit beginnen am Ende des oben gezeigten Abschnitts. Ein ganzer Haufen IP-Adressen wird dort zur Vorbereitung des Banns in Fail2ban geladen.&#10;Leicht beunruhigt schaute ich ins Syslog von Ubuntu:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Deactivated successful&#10;Jan 26 14:42:55 schallbert systemd[1]: docker-&amp;lt;ID&amp;gt;.scope: Consumed 21:05 CPU time&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.228607617Z&amp;#34; level=info msg=&amp;#34;ignoring event&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.229864632Z&amp;#34; level=info msg=&amp;#34;shim disconnected&amp;#34; &#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230125409Z&amp;#34; level=warning msg=&amp;#34;cleaning up after shim disconnected&amp;#34; i&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.230145478Z&amp;#34; level=info msg=&amp;#34;cleaning up dead shim&amp;#34;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.240586557Z&amp;#34; level=warning msg=&amp;#34;cleanup warnings time=\&amp;#34;2024-01-26T14:&amp;gt;&#10;Jan 26 14:42:55 schallbert dockerd[690]: time=&amp;#34;2024-01-26T14:42:55.241839655Z&amp;#34; level=warning msg=&amp;#34;ShouldRestart failed&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870912387Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.event.v1.p&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.870997689Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.internal.v&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871007368Z&amp;#34; level=info msg=&amp;#34;loading plugin \&amp;#34;io.containerd.ttrpc.v1.t&amp;gt;&#10;Jan 26 14:42:55 schallbert containerd[639]: time=&amp;#34;2024-01-26T14:42:55.871150831Z&amp;#34; level=info msg=&amp;#34;starting signal loop&amp;#34; namespace=moby path&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hm, &lt;code&gt;cleaning up dead shim&lt;/code&gt;. Das klingt ja bedrohlich. Eine kurze &lt;a href="https://iximiuz.com/en/posts/implementing-container-runtime-shim/" target="_blank" rel="noopener noreferrer" class="external-link"&gt;Recherche&lt;span class="external-link-icon" aria-hidden="true"&gt;↗&lt;/span&gt;&lt;/a&gt; zeigt, dass die Shim eine Zwischenschicht zwischen Containermanager und dem Container selbst bildet und Ein-und Ausgaben des Containers weiterleitet. Ihre Laufzeit ist an die des Containers gekoppelt.&lt;/p&gt;&#10;&lt;p&gt;Also war dies ein normales Herunterfahren mit anschließendem Neustart. Zum Glück kein Einbruch!&#10;Bleibt nur noch der Grund für den Neustart von Fail2ban herauszufinden. Da fällt mir ein, dass ich noch gar nicht weiß, wann Watchtower genau die Updates fährt&amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;Also mal schnell in die Logs von Watchtower geschaut mit &lt;code&gt;docker container logs &amp;lt;container_id&amp;gt;&lt;/code&gt; und siehe da:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;time=&amp;#34;2024-01-26T14:42:47Z&amp;#34; level=info msg=&amp;#34;Found new lscr.io/linuxserver/fail2ban:latest image (9bda077d765e)&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:50Z&amp;#34; level=info msg=&amp;#34;Stopping /fail2ban (e0317a105d53) with SIGTERM&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Creating /fail2ban&amp;#34;&#10;time=&amp;#34;2024-01-26T14:42:55Z&amp;#34; level=info msg=&amp;#34;Session done&amp;#34; Failed=0 Scanned=6 Updated=1 notify=no&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Damit habe ich den Beweis: Watchtower tut was es soll. Es zieht Container auf eine neue Version hoch, sobald Updates verfügbar werden. Prima!&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; Da ich Watchtower nicht groß konfiguriert habe, nehme ich anhand der in den Logs befindlichen Zeitstempel an, dass die Updates täglich zur Zeit des Starts von Watchtower gefahren werden. Bald werde ich von &lt;code&gt;latest&lt;/code&gt; für meine Container wohl auf ein älteres Tag release umschwenken.&lt;/p&gt;&#10;</description></item><item><title>Den Server absichern</title><link>https://blog.schallbert.de/server-protection/</link><pubDate>Fri, 12 Jan 2024</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;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; fail2ban mit ipdables und banip überprüfen&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 22. Aug. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; 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 untenstehende Lösung ganz charmant.&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&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;aside class="update-box update-box--note" role="note"&gt;&#10; &lt;span class="update-box__icon" aria-hidden="true"&gt;&#10; ℹ️&#10; &lt;/span&gt;&#10;&#10; &lt;div class="update-box__body"&gt;&#10; &lt;div class="update-box__heading"&gt;&#10; &lt;strong class="update-box__title"&gt;&#10; &#10; fail2ban für mehr als nur `ssh` verwenden&#10; &#10; &lt;/strong&gt;&#10;&#10; &lt;time datetime="2024-08-22T00:00:00Z"&gt;&#10; 22. Aug. 2024&#10; &lt;/time&gt;&#10; &#10; &lt;/div&gt;&#10;&#10; &#10; &lt;div class="update-box__content"&gt;&#10; Ich habe jetzt eine Lösung für &lt;em&gt;Gitea&lt;/em&gt;, aber auch alle anderen meiner Websites zustande gebracht:&#10; &lt;/div&gt;&#10; &#10; &lt;/div&gt;&#10;&lt;/aside&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>