<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Server und Datenbanken - Matteo Cisilino</title>
	<atom:link href="https://www.cisilino.com/de/category/server-datenbanken/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com/de/</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Sat, 31 Oct 2026 10:03:58 +0000</lastBuildDate>
	<language>de</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>Datenbank-Zugangsdaten-Rotation nach einem ISPConfig-Sicherheitsvorfall</title>
		<link>https://www.cisilino.com/de/2026/10/31/ispconfig-zugangsdaten-rotieren-nach-angriff/</link>
					<comments>https://www.cisilino.com/de/2026/10/31/ispconfig-zugangsdaten-rotieren-nach-angriff/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 10:03:56 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[Bash-Skript]]></category>
		<category><![CDATA[Incident Response]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Server-Management]]></category>
		<category><![CDATA[Zugangsdaten-Rotation]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1415</guid>

					<description><![CDATA[<p>Wie ich ISPConfig-Zugangsdaten rotieren lasse, sobald ich einen Einbruch vermute: mit reset_DB_password.sh, Backups und Dry-Run, Phase für Phase für Standalone-, Master- und Slave-Knoten.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/31/ispconfig-zugangsdaten-rotieren-nach-angriff/">Datenbank-Zugangsdaten-Rotation nach einem ISPConfig-Sicherheitsvorfall</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">ISPConfig-Zugangsdaten rotieren bedeutet für mich: handeln, sobald ich einen Einbruch vermute. Nicht erst, nachdem ich einen bestätigt habe. Datenbank-Zugangsdaten auf einem ISPConfig-Server zu rotieren klingt nach Routinewartung. Routine bleibt es nicht mehr, sobald man eine Webshell auf einer gehosteten Website findet. Oder eine Konfigurationsdatei, die man sich nicht erklären kann, in einem öffentlichen Backup. Ab diesem Punkt muss jedes Passwort, das zu diesem Datenbank-Stack gehört, neu sein. Überprüft. Dokumentiert. Nicht einfach als unbedenklich angenommen.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/database-credential-rotation-ispconfig.webp" alt="ISPConfig-Zugangsdaten rotieren: Schloss und Schlüssel über einer ISPConfig-Datenbank" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Wann hört Rotation auf, optional zu sein?</h2>



<p class="wp-block-paragraph">Ein paar Szenarien bringen das Thema regelmäßig genug zur Sprache, dass ich einen Rotationsplan dafür bereithalte. Ich improvisiere lieber keinen unter Druck.</p>



<p class="wp-block-paragraph">Ein Plugin oder CMS auf einer gehosteten Website wird kompromittiert. Über einen echten Fall auf dieser Art von Server habe ich hier geschrieben: <a href="https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/">PHP2Shell: So habe ich meine Server bis zum Patch geschützt</a>, eine PHP-Schwachstelle, die einem Angreifer erlaubte, eine Webshell abzulegen, bevor es einen Patch gab. Sobald eine Webshell unter einem Hosting-Benutzer läuft, kann sie oft die eigene config.inc.php dieser Website lesen. Auf einer gemeinsam genutzten ISPConfig-Box liegt diese Datei direkt neben den Zugangsdaten für die eigene Datenbank des Kontrollpanels. Nicht nur das eine Zugangsdatum, hinter dem der Angreifer eigentlich her war.</p>



<p class="wp-block-paragraph">Eine Konfigurationsdatei landet irgendwo, wo sie nicht hingehört. Ein Backup-Archiv, das in einem öffentlichen Ordner liegen bleibt. Ein Repository, das mit einer versehentlichen Datei gepusht wird. Ein Log, das lesbarer ist, als es sein sollte. config.inc.php und mysql_clientdb.conf haben an keinem dieser Orte etwas zu suchen. Fehler passieren trotzdem, auch mir.</p>



<p class="wp-block-paragraph">Ein Systemadministrator verlässt das Team. Wie einvernehmlich der Abschied auch war, gemeinsam genutzte Datenbank-Zugangsdaten, die diese Person einmal hatte, sind Zugangsdaten, denen ich nicht mehr vollständig vertraue. Vertrauen wird nicht automatisch übertragen, wenn jemand zur Tür hinausgeht.</p>



<p class="wp-block-paragraph">Ein vermuteter Einbruch, selbst ein unbestätigter. Dieser Fall lässt sich am schwersten jemandem erklären, der noch nie einen Server unter Angriffsverdacht betrieben hat. Man wartet nicht auf einen Beweis, bevor man handelt. Man geht von einer Kompromittierung aus und rotiert. Eine unnötige Rotation kostet einen Nachmittag. Eine ausgelassene, nach einem echten Einbruch, kann jede Website kosten, die der Server hostet.</p>



<h2 class="wp-block-heading">Warum ISPConfig-Zugangsdaten rotieren von Hand so fehleranfällig ist</h2>



<p class="wp-block-paragraph">Als ich die Datenbank-Zugangsdaten von ISPConfig zum ersten Mal von Hand rotiert habe, habe ich unterschätzt, wie viele Dateien betroffen sind. Sie müssen alle zusammen geändert werden, sonst geht ein paar Stunden später irgendwo leise etwas kaputt.</p>



<p class="wp-block-paragraph">Es gibt config.inc.php auf der Server-Seite und eine zweite Kopie auf der Interface-Seite. Beide müssen dieselben neuen Werte bekommen. Es gibt mysql_clientdb.conf, das ISPConfig ausliest, um die eigene Datenbank jeder gehosteten Website zu verwalten, nicht nur seine eigene Haushaltsführung. Dovecot, Pure-FTPd und Postfix führen jeweils eine eigene Kopie einiger dieser Zugangsdaten. Mail und FTP können still aufhören, sich zu authentifizieren, wenn man den Datenbank-Benutzer aktualisiert und dabei nur eine dieser drei Dienste vergisst.</p>



<p class="wp-block-paragraph">Dann gibt es die Master/Slave-Topologie, die einige meiner Setups verwenden. Ein Slave-Knoten authentifiziert sich über das Netzwerk an der Datenbank seines Masters, mit einem separaten Passwort. Dieses Passwort muss auf dem Master selbst rotiert werden, nicht nur lokal. Bringt man die Reihenfolge durcheinander, kann sich ein Slave vom eigenen Master aussperren, von dem er eigentlich replizieren soll.</p>



<p class="wp-block-paragraph">Konzeptionell ist nichts davon schwierig. Es sind nur viele Dateien. Mehrere Dienste, die in sinnvoller Reihenfolge neu gestartet werden müssen. Fast keine Toleranz für einen Tippfehler, besonders wenn man das unter Druck macht, gleich nachdem man etwas gefunden hat, das man nicht finden wollte. Genau diese Kombination ist der Grund, warum sich ISPConfig-Zugangsdaten rotieren von Hand immer riskanter anfühlte, als die Aufgabe selbst sein sollte. Deshalb habe ich reset_DB_password.sh geschrieben, nachdem ich das Problem mehr als einmal selbst durchgemacht hatte.</p>



<h2 class="wp-block-heading">Was reset_DB_password.sh tatsächlich macht</h2>



<p class="wp-block-paragraph">reset_DB_password.sh erkennt, ob der Knoten Standalone, Master oder Slave ist. Dann führt es nur die Phasen aus, die diese Rolle benötigt. Genau so, wie man ISPConfig-Zugangsdaten rotieren sollte: in der richtigen Reihenfolge, für die richtige Rolle.</p>



<p class="wp-block-paragraph">Phase A rotiert das lokale MySQL-Root-Passwort. Das ist das Passwort, mit dem ISPConfig selbst die Datenbank jeder gehosteten Website verwaltet. Das Skript aktualisiert mysql_clientdb.conf entsprechend dem neuen Wert.</p>



<p class="wp-block-paragraph">Phase B rotiert das Passwort des ispconfig-Kontrollbenutzers. Sie aktualisiert config.inc.php sowohl auf der Server- als auch auf der Interface-Seite. Danach aktualisiert sie die passenden Zugangsdaten in Dovecot, Pure-FTPd und Postfix, damit Mail und FTP weiter authentifizieren, ohne einen separaten manuellen Durchgang durch drei weitere Konfigurationsdateien.</p>



<p class="wp-block-paragraph">Phase C läuft nur auf einem Slave. Sie verbindet sich remote mit der Datenbank des Masters. Dort rotiert sie das Passwort des Slave-Benutzers und aktualisiert anschließend lokal dbmaster_password, damit die Replikation weiter korrekt gegen den neuen Wert authentifiziert.</p>



<p class="wp-block-paragraph">Nach Phase B bietet das Skript an, Dovecot, Pure-FTPd und Postfix für einen neu zu starten. Ich sage fast immer Ja. Wenn man ISPConfig-Zugangsdaten rotiert und dabei einen Mail-Dienst mit dem alten Passwort weiterlaufen lässt, ist man nicht fertig, man ist nur still kaputt. Der Neustart im gleichen Durchlauf, direkt nachdem sich die Konfigurationsdateien geändert haben, hält das Ganze in einem kurzen Wartungsfenster statt in drei separaten, die man später entdeckt.</p>



<h2 class="wp-block-heading">Backups, bevor sich irgendetwas ändert</h2>



<p class="wp-block-paragraph">Bevor es auch nur eine Datei anfasst, erstellt das Skript eine vollständige rsync-Kopie von /usr/local/ispconfig/ in einem zeitgestempelten Ordner. Es versucht außerdem einen nicht blockierenden mysqldump der Datenbanken mysql und dbispconfig. Nicht blockierend mit Absicht: Ich schließe lieber eine Rotation der Datenbank-Zugangsdaten ab, als auf einen Dump zu warten, der aus irgendeinem unzusammenhängenden Grund hängen bleibt. Jede Konfigurationsdatei, die bearbeitet wird, bekommt erst ihre eigene .bak-Kopie, bevor auch nur eine Zeichenkette darin ersetzt wird.</p>



<p class="wp-block-paragraph">Nichts davon ersetzt eine unabhängige Backup-Strategie. Ich mache weiterhin ein separates Snapshot oder ein vollständiges Backup, bevor ich das auf einem Server ausführe, der wirklich wichtig ist. Der eigene Disclaimer des Skripts sagt dasselbe, nur direkter, als ich es gerade getan habe.</p>



<h2 class="wp-block-heading">Erst der Trockenlauf, jedes Mal</h2>



<p class="wp-block-paragraph">Jede Phase fragt nach Bestätigung, bevor irgendetwas geändert wird. &#8211;dry-run simuliert einen kompletten Durchlauf, ohne eine einzige Datei zu berühren. Ich benutze dry-run zuerst auf jedem Server, jedes Mal. Auch auf solchen, auf denen ich schon einmal Zugangsdaten rotiert habe und die ich zu kennen glaube. &#8211;skip-local-root überspringt Phase A, für den Fall, dass nur das Passwort des Kontrollbenutzers rotiert werden muss. &#8211;role=master oder &#8211;role=slave überschreibt die automatische Erkennung, für den seltenen Fall, dass sie die Topologie eines Servers nicht selbst korrekt erkennt.</p>



<p class="wp-block-paragraph">Die erzeugten Passwörter sind 32 Zeichen lang, nur Buchstaben und Ziffern. Lang genug, um Raten zu widerstehen. Einfach genug, dass die Parser von MySQL und PHP niemals über ein vereinzeltes Sonderzeichen in einer Verbindungszeichenfolge stolpern. Ich habe das Skript auf ISPConfig 3.3.1p1 getestet. Gegen ältere oder neuere Zweige habe ich es nicht laufen lassen. Ich würde dort kein identisches Verhalten annehmen, ohne es selbst vorher zu prüfen.</p>



<p class="wp-block-paragraph">Die Rolle eines Knotens ist nicht immer auf den ersten Blick offensichtlich, besonders auf einem Server, den man geerbt und nicht selbst aufgebaut hat. Deshalb gibt es &#8211;role, und deshalb lohnt es sich, die Ausgabe des Trockenlaufs beim ersten Mal Zeile für Zeile zu lesen, nicht nur zu überfliegen. Ich bestätige lieber zwei Minuten lang, dass ein Slave wirklich ein Slave ist, als es mitten in der Rotation auf die harte Tour herauszufinden.</p>



<h2 class="wp-block-heading">Was ich dir sagen würde, bevor du es ausführst</h2>



<p class="wp-block-paragraph"><strong>Hinweis</strong>: Das Skript wird wie es ist, zu Informationszwecken bereitgestellt. Die Nutzung erfolgt ausschließlich auf eigenes Risiko: Ich übernehme keine Verantwortung für Datenverlust, Ausfallzeiten oder Fehler, die durch falsche Anwendung, Veränderung des Codes oder einen anderen Kontext als den, in dem ich es selbst einsetze, entstehen. Erstelle immer ein unabhängiges Backup oder einen VM-Snapshot, bevor du es in Produktion ausführst, unabhängig davon, was der eigene Backup-Schritt des Skripts bereits für dich erfasst.</p>



<p class="wp-block-paragraph">Es ist freie Software. Du darfst sie weiterverbreiten und verändern, mit Nennung meines Namens als ursprünglichen Autor. Der Quellcode von reset_DB_password.sh, und der Rest der ispconfig-fast-tools-Sammlung, liegt auf GitHub unter <a href="https://github.com/matteocisilino/ispconfig-fast-tools" target="_blank" rel="noopener">github.com/matteocisilino/ispconfig-fast-tools</a>.</p>



<h2 class="wp-block-heading">ISPConfig-Zugangsdaten rotieren ist eine Gewohnheit, kein Aufräumjob</h2>



<p class="wp-block-paragraph">Datenbank-Zugangsdaten nach einem bestätigten Einbruch zu rotieren, ist einfach kompetente Reaktion auf einen Sicherheitsvorfall. Die meisten Administratoren würden es tun, ohne dass man es ihnen zweimal sagen muss. Die schwerere Gewohnheit ist, es auch nach einem bloß vermuteten Einbruch zu tun, bevor jemand Beweise hat. Das ist die einzige Version des Plans, die den Schaden tatsächlich begrenzt, solange er noch begrenzbar ist. Ein Skript, das die richtigen Phasen für die Rolle eines Servers ausführt, zuerst eigene Backups erstellt und mich den ganzen Vorgang per Trockenlauf prüfen lässt, bevor irgendetwas Reales berührt wird. Das macht die Gewohnheit praktisch an einem Dienstagabend, statt theoretisch in einem Blogartikel.</p>



<p class="wp-block-paragraph">ISPConfig, MySQL, PHP, Dovecot, Pure-FTPd, Postfix und die anderen genannten Namen sind Marken ihrer jeweiligen Inhaber. Sie werden hier nur zur Identifikation der Produkte verwendet.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/31/ispconfig-zugangsdaten-rotieren-nach-angriff/">Datenbank-Zugangsdaten-Rotation nach einem ISPConfig-Sicherheitsvorfall</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/31/ispconfig-zugangsdaten-rotieren-nach-angriff/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Linux-Kernel-Schwachstelle: ein Use-after-free im TCP IPv4</title>
		<link>https://www.cisilino.com/de/2026/10/31/linux-kernel-schwachstelle-tcp-cve-2026-98432/</link>
					<comments>https://www.cisilino.com/de/2026/10/31/linux-kernel-schwachstelle-tcp-cve-2026-98432/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 09:19:23 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Linux-Kernel]]></category>
		<category><![CDATA[Server-Sicherheit]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1406</guid>

					<description><![CDATA[<p>Eine Schwachstelle im Linux-Kernel im TCP-IPv4-Code (CVE-2026-98432, CVSS 9,8) erklärt: was sie bedeutet, warum die Hersteller sich nicht einig sind, und wie es bei Debian, RHEL und Ubuntu mit den Patches aussieht.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/31/linux-kernel-schwachstelle-tcp-cve-2026-98432/">Linux-Kernel-Schwachstelle: ein Use-after-free im TCP IPv4</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Ich verwalte seit Jahren Debian-Server im produktiven Einsatz. Jedes Mal, wenn eine neue CVE zum Kernel erscheint, halte ich an und lese mir die Details durch. Dann entscheide ich, ob ich sofort aktualisiere oder warte. Dieses Mal ist die Geschichte verzwickter als sonst: Es gibt eine <strong>Schwachstelle im Linux-Kernel</strong> im TCP-IPv4-Code, einen zwischen den Herstellern umstrittenen CVSS-Score, und sogar einen angeblichen Root-Exploit, der von einer künstlichen Intelligenz gebaut wurde. Fangen wir mit einer Frage an, die nebensächlich wirkt, es aber nicht ist: Was ist MPTCP, und warum ist es wichtig, selbst wenn du es nicht nutzt?</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/vulnerabilita-kernel-linux-tcp-cve-2026-98432.webp" alt="Schwachstelle im Linux-Kernel im TCP IPv4, stilisierter Pinguin mit korrigiertem Debian-Schild" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Was ist MPTCP, und warum betrifft es auch dich, wenn du es nicht nutzt</h2>



<p class="wp-block-paragraph">Multipath TCP (MPTCP) ist der Standard RFC 8684. Er erweitert das klassische TCP, damit eine einzelne Verbindung gleichzeitig über mehrere Netzwerkpfade laufen kann. Ein Smartphone kann WiFi und Mobilfunk im selben Moment nutzen, für dieselbe Verbindung. Ein Server mit mehreren Schnittstellen kann dasselbe tun, für Redundanz oder um Bandbreite zu summieren. Apple nutzt es produktiv schon seit iOS 7, im Jahr 2013, für Siri: das ist der bekannteste reale Anwendungsfall. Manche Mobilfunkanbieter, zum Beispiel in Südkorea, setzen es für WiFi/LTE-Bonding ein. Mehrere Rechenzentren nutzen es für Multihoming zwischen verschiedenen Uplinks. Im Linux-Mainline-Kernel ist es seit Version 5.6 enthalten.</p>



<p class="wp-block-paragraph">Der Punkt, der mich die Augenbraue hochziehen ließ, ist ein anderer. Seit Version 1.24 verlangen in Go geschriebene Programme standardmäßig MPTCP für lauschende Sockets. Das Go Security Team hat das am 29. Oktober bestätigt. Gleichzeitig hat es eine unangenehme Warnung ausgesprochen: Der Bug, von dem wir heute sprechen, ist nicht MPTCP-spezifisch. Ein System ohne aktive MPTCP-Listener kann trotzdem ähnlichen Angriffen ausgesetzt sein. Der verwundbare Code liegt im generischen TCP-IPv4-Pfad, nicht in einem optionalen Modul. Man kann MPTCP pro Programm deaktivieren, mit der Variable <code>GODEBUG=multipathtcp=0</code> zur Laufzeit. Doch das Go Security Team bezeichnet diese Abmilderung als „brüchig“: Ein einziger verbleibender MPTCP-Listener reicht aus, um sie zunichtezumachen. Niemand hat bewiesen, dass jeder funktionierende Exploit tatsächlich von MPTCP abhängt. Deshalb bleibt der MPTCP-Code eine Angriffsfläche, auch wenn du ihn in deinem Stack nie bewusst aktivierst: Es reicht, dass er in den Kernel compiliert und erreichbar ist.</p>



<p class="wp-block-paragraph">Das MPTCP-Projekt selbst erklärt auf seiner eigenen Seite, dass die wichtigsten aktuellen Linux-Distributionen den Kernel standardmäßig mit MPTCP-Unterstützung compiliert ausliefern (<code>CONFIG_MPTCP=y</code>). Es nennt ausdrücklich Debian, Ubuntu, RHEL und Fedora. Das ist eine Aussage des Projekts selbst, keine unabhängige Überprüfung Distribution für Distribution, aber das Konzept bleibt gültig: Die Unterstützung ist auf Kernel-Ebene aktiviert, nicht „opt-in“ auf Betriebssystemebene. Nur die Aushandlung pro einzelner Verbindung bleibt Aufgabe der Anwendung.</p>



<h2 class="wp-block-heading">Die Schwachstelle im Linux-Kernel CVE-2026-98432 im Überblick</h2>



<p class="wp-block-paragraph">Das CVE-Team des Linux-Kernels hat CVE-2026-98432 am 15. Oktober 2026 veröffentlicht. Der Upstream-Fix war bereits zehn Tage zuvor angewendet worden, am 5. Oktober. Es kommt häufig vor, dass die Veröffentlichung dem Commit mit einer gewissen Verzögerung folgt. Der CVSS-Score von 9,8 (kritisch) kam erst am 24. Oktober, neun Tage nach der Veröffentlichung der CVE.</p>



<p class="wp-block-paragraph">Das Problem liegt in der Behandlung des TCP-Handshakes für IPv4, genauer in der Funktion <code>tcp_v4_syn_recv_sock()</code>. Wenn eine Verbindung erstellt wird, werden die IP-Optionen des SYN-Pakets vom wartenden Request-Socket an den neuen Socket übergeben. Zum Beispiel Record Route oder Timestamp, falls der Client sie in einem eigens konstruierten SYN einfügt. Das Problem ist, dass sie per Referenz übergeben werden, nicht per Kopie, und ohne das Lock des Listeners. Laut der technischen Analyse des Patches heißen die betroffenen Strukturen <code>inet_opt</code> und <code>ireq_opt</code>. Eine zweite CPU kann diesen Puffer mit <code>kfree()</code> freigeben, aufgerufen von <code>inet_sock_destruct()</code> oder von <code>tcp_v4_reqsk_destructor()</code>. Währenddessen liest eine erste CPU ihn noch in <code>ip_options_build()</code>, um das SYNACK zu bauen. Es ist ein klassisches Use-after-free, verursacht durch eine Race Condition. Der entsprechende IPv6-Pfad, <code>tcp_v6_syn_recv_sock()</code>, kopiert die Optionen statt sie per Referenz zu übergeben: Er ist von der Haupt-Race nicht betroffen. Es gibt auch einen zweiten Angriffsweg, über die Migration von Request-Sockets: <code>net.ipv4.tcp_migrate_req=1</code> oder ein eBPF-Programm mit <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code>. Beide sind standardmäßig deaktiviert.</p>



<p class="wp-block-paragraph">Der Bug ist remote ausnutzbar und erfordert laut Einschätzung des Kernel-Teams weder Authentifizierung noch Benutzerinteraktion. Er ist seit Kernel-Version 4.4 vorhanden. Der Fix stammt von Eric Dumazet, gemeldet von Xinyang Ge von Anthropic. Er verschiebt die Freigabe des Speichers auf die Zeit nach einer RCU-Grace-Period, mit <code>kfree_rcu()</code>: So kann keine CPU mehr einen bereits freigegebenen Puffer lesen. Die korrigierten stabilen Versionen sind 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10 und die Mainline ab 7.3-rc7.</p>



<h2 class="wp-block-heading">Widersprüchliche CVSS-Scores: Nicht alle Hersteller sind sich einig</h2>



<p class="wp-block-paragraph">Hier wird die Geschichte komplizierter. Das Kernel-CVE-Team und Ubuntu bewerten die Angriffskomplexität als niedrig und vergeben 9,8 (kritisch). Oracle und SUSE vergeben dagegen 8,1 (wichtig): derselbe CVSS-Vektor, aber hohe Angriffskomplexität. Red Hat hat merkwürdigerweise zwei Bewertungen, die nicht übereinstimmen. Die öffentliche CVE-Seite zeigt „Moderate, 5.9“. Die maschinenlesbare Advisory-Datei, aktualisiert am 29. Oktober, zeigt dagegen „Important, 8.1“, mit hoher Auswirkung auf Vertraulichkeit und Integrität. Am selben Tag hat der interne Bugtracker von Red Hat die Schwere von medium auf high angehoben. Amazon vergibt seinerseits 7,0. Zum Zeitpunkt, an dem ich das schreibe, hat die NVD noch keinen eigenen Score veröffentlicht.</p>



<p class="wp-block-paragraph">Das sollte man ehrlich sagen. Diese interne Unstimmigkeit bei Red Hat ist kein Detail, das ich selbst hergeleitet habe: Sie wird so vom Originalartikel von The Hacker News berichtet, meiner Hauptquelle für diese Daten. Es ist nicht das erste Mal, dass ein Hersteller für denselben Bug zwei unterschiedliche Zahlen auf unterschiedlichen Kanälen veröffentlicht. Aber hier sagt der Sprung von „moderat“ zu „wichtig“ am selben Tag viel aus. Auch für diejenigen, die den Code schreiben, ist es schwierig, die tatsächliche Ausnutzbarkeit einer Race Condition im Kernel mit Sicherheit einzuschätzen.</p>



<h2 class="wp-block-heading">Patch-Status: Debian schon korrigiert, andere noch nicht</h2>



<p class="wp-block-paragraph">Das ist der Punkt, der mich am direktesten betrifft: Die Hosting-Umgebungen, die ich verwalte, laufen auf Debian Stable. Und hier sprechen die Fakten klar, ohne dass man etwas betonen müsste: <strong>Debian hat diese Schwachstelle im Linux-Kernel bereits korrigiert</strong>, sowohl auf trixie als auch auf bookworm. Debian trixie (stable) wurde am 16. Oktober 2026 korrigiert, mit dem Paket <code>linux 6.12.113-1</code>, Advisory DSA-6568-1. Debian 12 (bookworm) wurde in zwei Schritten korrigiert: am 17. und am 22. Oktober 2026, mit <code>linux 6.1.190-1</code> (Advisory DLA-4841-1) und <code>linux-6.12 6.12.113-1~deb12u1</code> (Advisory DLA-4850-1). In seiner eigenen Tracking-Tabelle listet Debian für diese CVE keinen eigenen CVSS-Score auf. Es markiert nur den Fix-Status für jedes Release: Das ist die Information, die wirklich zählt, für alle, die entscheiden müssen, ob sie einen Server neu starten.</p>



<p class="wp-block-paragraph">Es hilft, das mit dem Status anderer weit verbreiteter Distributionen zu vergleichen, um der Sache Gewicht zu geben. Am 30. Oktober hat Red Hat den Status für RHEL 8, 9, 10 und OpenShift 4 aktualisiert. Zuvor war es „fix deferred“ (markiert am 29. Oktober), jetzt ist es „affected“. Kein Veröffentlichungsdatum wurde mitgeteilt. Ubuntu gilt auf allen unterstützten Versionen (22.04, 24.04, 26.04, 26.10) noch als „in Bearbeitung“, ohne Datum. Ubuntu 16.04, 18.04 und 20.04 gelten als verwundbar, ohne dass überhaupt ein Fix aufgelistet ist. SUSE Linux Enterprise Server, von SP4 bis SP7 plus 16.0, befindet sich in derselben Lage. Oracle Linux (UEK), openSUSE Leap 16.0, SUSE Linux Micro 6.2 und Amazon Linux 2023 sind dagegen schon korrigiert. RHEL 7 ist nicht betroffen: Der verwundbare Codepfad wurde auf dieser Release nie ausgeliefert.</p>



<p class="wp-block-paragraph">Ich sage das nicht, um Ubuntu oder RHEL schlechtzumachen. Es bleiben seriöse Distributionen mit robusten Sicherheitsprozessen. Ich sage es, weil bei diesem einzelnen Bug, in diesem Zeitfenster, die Fakten so sind: Wer sich auf Hosting mit Debian Stable verlässt, hat den Fix schon installiert. Auf anderen, stark genutzten Distributionen bleibt der Server dagegen für einen unbestimmten Zeitraum exponiert. Das ist ein konkreter, nachprüfbarer Vorteil, wenn man sich die Daten ansieht. Es ist keine Meinung über das Debian-Projekt im Allgemeinen.</p>



<h2 class="wp-block-heading">Der (berichtete, nicht unabhängig verifizierte) Exploit von Anthropic</h2>



<p class="wp-block-paragraph">Der ungewöhnlichste Teil dieser Geschichte betrifft Anthropic, das Unternehmen, das den Bug als erstes über Xinyang Ge gemeldet hat. Laut Branchenberichten hat eines seiner internen KI-Modelle, nicht öffentlich veröffentlicht, selbstständig einen funktionierenden Root-Exploit gebaut. Ausgehend nur vom Patch und vom Report als Prompt. Der zweistufige Angriff würde so funktionieren. In der ersten Phase lässt die Race den Angreifer zusätzliche Bytes aus der Antwort des Kernels empfangen. Von dort gewinnt er eine Kernel-Adresse, um KASLR auszuhebeln. In der zweiten Phase würde derselbe Bug einen Buffer-Overflow verursachen. Er überschreibt einen Callback-Pointer mit der Adresse eines Payloads, den der Angreifer bereits im Kernel-Speicher platziert hat. Wenn der Kernel diesen Pointer aufruft, führt das Payload einen beliebigen Shell-Befehl als Root aus. Eine verlorene Race würde nur diesen einzelnen Verbindungsversuch scheitern lassen, ohne das System abstürzen zu lassen: Wiederholte Versuche wären also möglich. Im von Anthropic beschriebenen Test: 48 Minuten, um die Kernel-Adresse zu stehlen, weitere 5 Minuten bis zur Befehlsausführung.</p>



<p class="wp-block-paragraph">Red Hat sagt, dass ein entfernter Angreifer, der einen exponierten TCP-Dienst erreicht, die Race generell auslösen kann. Der von Anthropic beschriebene Root-Exploit hätte jedoch gezielt Server mit einem aktiven Multipath-TCP-Listener angegriffen. Der Beitrag von Anthropic klärt nicht, ob ein Server ohne MPTCP genauso ausnutzbar ist. Weder der CVE-Eintrag noch der Upstream-Fix erwähnen MPTCP an irgendeiner Stelle. Es ist die Fachpresse, keine Primärquelle, die diesen Zusammenhang berichtet: Ich schreibe das deshalb mit der Skepsis, die es verdient, nicht als zu hundert Prozent gesicherte Tatsache. Anthropic hat weder den Exploit-Code veröffentlicht, noch die genaue verwendete Kernel-Version, noch die Konfiguration des Zielservers. Ein anderes Unternehmen, Innora, hat eine Root-Cause-Analyse veröffentlicht, mit einer QEMU-Reproduktionsumgebung. Es hat erklärt, kurzfristig keinen bewaffneten Exploit zu veröffentlichen. Derzeit gibt es keine Beweise für eine tatsächliche Ausnutzung durch Hersteller oder CERTs. Der Bug ist nicht im CISA-KEV-Katalog, laut der von CIRCL Luxemburg aktualisierten Kopie vom 28. Oktober.</p>



<h2 class="wp-block-heading">Was jetzt auf deinen Servern zu tun ist</h2>



<p class="wp-block-paragraph">Kein Hersteller hat einen offiziell sanktionierten Workaround für diese <strong>Schwachstelle im Linux-Kernel</strong> veröffentlicht. Red Hat sagt ausdrücklich, dass keine der bisher kursierenden Maßnahmen seinem Standard an Deployment-Einfachheit und Stabilität entspricht. Die primäre Abmilderung bleibt, den korrigierten Kernel einzuspielen und neu zu starten, sobald der Patch für deine Distribution verfügbar ist. Auf Debian ist er das schon. Auf RHEL und Ubuntu noch nicht, zum Zeitpunkt, an dem ich das schreibe.</p>



<p class="wp-block-paragraph">Als Verteidigung in der Tiefe, nicht als endgültige Lösung, ergeben einige Maßnahmen Sinn. Prüfen, dass <code>net.ipv4.tcp_migrate_req</code> auf 0 gesetzt ist. Kontrollieren, dass keine eBPF-Programme mit <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code> geladen sind. Eine Filterung eingehender IPv4-Pakete mit Record-Route- oder Timestamp-Optionen erwägen. Abstürze überwachen, die <code>ip_options_build</code>, <code>ip_build_and_send_pkt</code> oder <code>skb_under_panic</code> zitieren. Ein Inventar eventuell exponierter MPTCP-Listener erstellen und vermeiden, sie ohne Grund erreichbar zu lassen. Wenn du auch Schutz gegen häufigere Angriffe auf Anwendungsebene verwaltest, findest du unter <a href="https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/">wie ich meine Server bis zum Patch für PHP2Shell abgesichert habe</a> denselben Ansatz. Er lautet: abmildern, was möglich ist, während man auf den endgültigen Fix wartet, ohne Schutz zu versprechen, den es nicht gibt.</p>



<p class="wp-block-paragraph">Für alle, die mich fragen, ob sie sofort aktualisieren sollen: Wenn du Debian Stable nutzt, ist der korrigierte Kernel schon in den Security-Repositories. Das Update ist einfach, und das Zeitfenster der Exposition ist schon geschlossen. Wenn du andere Distributionen nutzt, gibt es heute noch keinen Patch zu installieren. Es lohnt sich, die Advisories deines Herstellers in den nächsten Tagen genau zu verfolgen: Die Lage ändert sich schnell.</p>



<p class="wp-block-paragraph">Quellen: <a href="https://thehackernews.com/2026/10/linux-kernel-tcp-flaw-with-98-cvss.html" target="_blank" rel="nofollow noopener">The Hacker News</a>, <a href="https://security-tracker.debian.org/tracker/CVE-2026-98432" target="_blank" rel="nofollow noopener">Debian Security Tracker</a> und der <a href="https://github.com/torvalds/linux/commit/e3f33b0a1d89d738e2b913dab519226eaf76e15b" target="_blank" rel="noopener">Fix-Commit</a>. Debian, Red Hat, Ubuntu und die anderen genannten Namen sind Marken ihrer jeweiligen Eigentümer.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/31/linux-kernel-schwachstelle-tcp-cve-2026-98432/">Linux-Kernel-Schwachstelle: ein Use-after-free im TCP IPv4</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/31/linux-kernel-schwachstelle-tcp-cve-2026-98432/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>E-Mail-Protokoll in WordPress: warum ich es mit FluentSMTP kontrolliere</title>
		<link>https://www.cisilino.com/de/2026/10/31/email-protokoll-wordpress-fluentsmtp/</link>
					<comments>https://www.cisilino.com/de/2026/10/31/email-protokoll-wordpress-fluentsmtp/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[E-Mail]]></category>
		<category><![CDATA[FluentSMTP]]></category>
		<category><![CDATA[SMTP]]></category>
		<category><![CDATA[WordPress-Plugin]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1358</guid>

					<description><![CDATA[<p>Wie ich das E-Mail-Protokoll in WordPress mit FluentSMTP kontrolliere, um fehlgeschlagene E-Mails sofort zu finden und erneut zu senden.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/31/email-protokoll-wordpress-fluentsmtp/">E-Mail-Protokoll in WordPress: warum ich es mit FluentSMTP kontrolliere</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Ich betreue mehrere WordPress-Websites. Auf jeder geht eine Menge E-Mails raus, die kaum jemand auf dem Schirm hat: Kontaktbenachrichtigungen, Passwort-Resets, Bestellbestätigungen, Hinweise an die Betreiber. Das E-Mail-Protokoll in WordPress von FluentSMTP ist das Werkzeug, das ich als Erstes öffne, sobald ein Kunde schreibt &#8222;ich habe nichts bekommen&#8220;. Früher sah ich in die Logs des SMTP-Servers per Kommandozeile. Ich musste grep und Zeitstempel von Hand abgleichen. Heute öffne ich eine Seite im WordPress-Panel und habe alles vor Augen.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/fluent-smtp-log-invii-email-wordpress.webp" alt="E-Mail-Protokoll in WordPress im Panel von FluentSMTP" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Warum ich das E-Mail-Protokoll in WordPress vor den Server-Logs kontrolliere</h2>



<p class="wp-block-paragraph">Der typische Fall ist immer derselbe. Ein Kontaktformular, das &#8222;nicht ankommt&#8220;. Ein Kunde, der die Passwort-Reset-E-Mail nicht erhält. Eine Bestellung ohne Bestätigung. Ohne ein Protokoll zur Hand ist die erste Vermutung der Spam-Ordner. Dann die Logs des SMTP-Servers. Dann vielleicht die Firewall. Mit FluentSMTP überspringe ich diese Schritte. Ich sehe sofort, ob WordPress wirklich versucht hat, diese E-Mail zu versenden. An wen, wann, mit welcher Antwort vom Versandserver.</p>



<p class="wp-block-paragraph">FluentSMTP protokolliert jeden Aufruf von <code>wp_mail()</code>. Nicht nur die E-Mails von Drittanbieter-Plugins, auch die vom Core selbst: Benachrichtigungen über neue Benutzer, Update-Erinnerungen, Fehlerhinweise. Für jeden Versand zeigt das Protokoll Absender, Empfänger, Betreff, vollständige Header, Nachrichtentext. Und die Antwort des SMTP-Providers oder der verwendeten API. Ist eine E-Mail fehlgeschlagen, steht diese Serverantwort genau dort. Ich muss sie nicht raten.</p>



<h2 class="wp-block-heading">Was wirklich im Versandprotokoll steht</h2>



<p class="wp-block-paragraph">Das Plugin unterstützt Dutzende Connectoren: Amazon SES, SendGrid, Mailgun, Brevo, Postmark, SparkPost. Auch Google Workspace und Microsoft 365 über OAuth, neben dem klassischen generischen SMTP. Auf den Websites, die ich betreue, nutze ich fast immer authentifiziertes SMTP mit der Domain des Kunden. Manchmal eine dedizierte API, wenn das E-Mail-Volumen wächst. In beiden Fällen bleibt das Protokoll gleich. Jede Zeile hat einen Status, versendet oder fehlgeschlagen. Es gibt eine Schaltfläche, um den Inhalt anzusehen oder die Nachricht erneut zu senden, ohne dass der Nutzer die Aktion wiederholen muss, die sie ausgelöst hat.</p>



<p class="wp-block-paragraph">Der erneute Versand ist die Funktion, die ich am häufigsten nutze. Ein Formular, das zweimal ausgefüllt wurde, weil &#8222;es nicht ankam&#8220;, wird zu einem Klick im Protokoll. Keine neue Meldung an den Kunden. Für wiederholte Fehlschläge habe ich Benachrichtigungen über Telegram aktiviert. Sobald eine Verbindung nicht mehr funktioniert, weiß ich es, bevor mir jemand schreibt. Statt es aus einem Support-Ticket zu erfahren.</p>



<p class="wp-block-paragraph">Das Protokoll betrifft nicht nur Benachrichtigungen direkt an mich oder den Kunden, der die Seite verwaltet. Dort landet jede ausgehende E-Mail, die die Website an Dritte schickt: die Bestellbestätigung, die ein Online-Shop an den Endkunden sendet, die Zahlungsquittung, die Versandbenachrichtigung. Kommt diese Bestätigung nicht an, zeigt das E-Mail-Protokoll in WordPress das genauso wie eine interne Benachrichtigung, mit derselben Schaltfläche, um sie an den richtigen Empfänger neu zu senden, ohne dass er den Kauf wiederholen muss.</p>



<h2 class="wp-block-heading">Wie ich FluentSMTP auf den Websites konfiguriere, die ich betreue</h2>



<p class="wp-block-paragraph">Nach dem Verbinden des passenden Connectors ändere ich die Aufbewahrungsdauer des Protokolls. Standardmäßig bleibt es lange aktiv. Ich stelle sie auf 30 bis 60 Tage, je nach Traffic der Website. Das reicht, um einem spät gemeldeten Problem nachzugehen. Ohne die Datenbank mit Zeilen zu füllen, die ich nie ansehen werde. Danach aktiviere ich mindestens einen Benachrichtigungskanal für Fehlschläge, Telegram oder Slack, je nach Kunde. Das Protokoll allein warnt niemanden, es muss geöffnet werden, um gelesen zu werden.</p>



<p class="wp-block-paragraph">Auf Websites mit Kontaktformular nutze ich zusätzlich <a href="https://www.cisilino.com/de/2026/10/29/sicherheits-plugin-fur-wordpress/">Cisilino Site Defense Network</a> zum Blockieren von Bots. Weniger eingehender Spam bedeutet weniger E-Mails aus den Formularen. Das Protokoll bleibt übersichtlicher, mit weniger Rauschen, das ich beim Suchen der richtigen Nachricht ausblenden muss.</p>



<h2 class="wp-block-heading">Die SMTP-Zugangsdaten bleiben nicht im Klartext in der Datenbank</h2>



<p class="wp-block-paragraph">Ein Detail, das ich immer prüfe, bevor ich eine Verbindung speichere: wo die Zugangsdaten landen. FluentSMTP verschlüsselt sie standardmäßig mit den SALT-Schlüsseln von WordPress, denselben, die schon in <code>wp-config.php</code> für die Sicherheit der Sitzungen stehen. Man kann die Verschlüsselung deaktivieren, aber das Plugin selbst rät davon ab. Auf den Websites, die ich betreue, bevorzuge ich eine andere Option: das API- oder SMTP-Passwort direkt in <code>wp-config.php</code> speichern, außerhalb der Datenbank, mit Konstanten, die definiert sind, bevor das Plugin sie liest. So legt auch ein Datenbank-Dump oder ein unbefugter Zugriff aufs Panel den Schlüssel des Versandproviders nicht offen. Es ist dasselbe Prinzip, das ich auch sonst anwende: weniger Geheimnisse in der Datenbank, umso besser.</p>



<h2 class="wp-block-heading">Mehrere Verbindungen gleichzeitig, nicht nur eine nach der anderen</h2>



<p class="wp-block-paragraph">FluentSMTP erlaubt es, mehrere Verbindungen gleichzeitig einzurichten, nicht nur eine nach der anderen. Auf einer Website lege ich die Standardverbindung fest und füge eine zweite als Reserve hinzu: Antwortet der erste Provider nicht, wechselt der Versand automatisch zur Reserveverbindung, ohne dass ich eingreifen muss. Die Wahl der Verbindung hängt von der Absenderadresse ab: Jede Verbindung hat ihr eigenes &#8222;Von&#8220;, der Rest läuft über die Standardverbindung, mit der Reserve nur bereit, falls die Standardverbindung wirklich ausfällt (ein Fehler des Providers, keine E-Mail, die im Spam landet). Das ist einer der Gründe, warum ich das einem einzelnen SMTP ohne Alternative vorziehe: Haben SendGrid oder Amazon SES ein paar Stunden lang ein Problem, versendet die Website trotzdem weiter, und das E-Mail-Protokoll in WordPress zeigt es sofort. Alles in der kostenlosen Version enthalten, ohne eine kostenpflichtige Pro-Version: Man bezahlt nur den gewählten Versandprovider, nicht das Plugin.</p>



<h2 class="wp-block-heading">Ein täglicher Gesundheitscheck, nicht nur wenn etwas kaputt ist</h2>



<p class="wp-block-paragraph">Neben dem Protokoll Zeile für Zeile führt FluentSMTP jeden Tag automatisch einen Gesundheitscheck der Verbindung durch. Antwortet der Provider nicht mehr, kommt die Warnung, bevor ein Kunde mir schreibt &#8222;die Website versendet keine E-Mails mehr&#8220;. Dasselbe Panel zeigt eine Grafik mit den täglichen Gesamtzahlen erfolgreicher und fehlgeschlagener Versände: nützlich, um auf einen Blick zu erkennen, ob ein Rückgang normal ist oder das Symptom für etwas, das gerade kaputtgeht, ohne jedes Mal das Protokoll Zeile für Zeile öffnen zu müssen. Es gibt auch eine periodische Zusammenfassung per E-Mail, mit den Zahlen der Woche: kein Fehlerhinweis, sondern ein Bulletin, das ich kurz überfliege, um zu wissen, ob das Versandvolumen im Rahmen liegt.</p>



<p class="wp-block-paragraph">Wer wie ich mehrere Websites von der Kommandozeile aus verwaltet, findet in FluentSMTP auch WP-CLI-Befehle: einen Versandtest, einen Gesundheitscheck der Verbindung, Statistiken und das Aufräumen alter Protokolleinträge. Ich nutze sie in denselben Skripten, die schon auf den Servern für andere Wartungsaufgaben laufen, ohne das Panel jeder Website einzeln öffnen zu müssen.</p>



<h2 class="wp-block-heading">Was das E-Mail-Protokoll in WordPress nicht leisten kann</h2>



<p class="wp-block-paragraph">Es ist keine Wunderlösung. Die Aufbewahrung richtet sich nach Tagen, nicht nach einer Höchstzahl an Zeilen. Auf einer Website mit viel transaktionalem Traffic kann die Protokolltabelle schnell wachsen. Es lohnt sich, sie im Blick zu behalten oder eine kürzere Aufbewahrungsdauer einzustellen. Fehlerbenachrichtigungen kommen nur über die konfigurierten Chat-Kanäle, nicht per E-Mail. Macht Sinn: Eine E-Mail, die vor einer fehlgeschlagenen E-Mail warnt, kann selbst fehlschlagen. Aber das sollte man wissen, bevor man sich darauf verlässt.</p>



<p class="wp-block-paragraph">Schließlich zeigt das Protokoll, was auf Seiten von WordPress und auf Seiten des Providers passiert ist. Es garantiert nicht die Zustellung im Postfach des Empfängers. Ist eine E-Mail als versendet markiert, landet aber im Spam, ist das ein Problem des Domain-Rufs oder der SPF/DKIM-Konfiguration. Nichts, was ein Protokoll allein lösen kann. Trotzdem bleibt es die erste Stelle, an der ich nachsehe, bevor ich irgendetwas anderes prüfe.</p>



<p class="wp-block-paragraph">Das Plugin ist kostenlos und Open Source. Die offizielle Seite auf <a href="https://wordpress.org/plugins/fluent-smtp/" target="_blank" rel="nofollow noopener">wordpress.org</a>, ohne Funktionen, die hinter einer Pro-Version blockiert sind. Wer auch nur eine Website mit Kontaktformular betreut, für den lohnt es sich, es nur für das E-Mail-Protokoll in WordPress zu installieren. Löst es das erste &#8222;ich habe die E-Mail nicht bekommen&#8220; in einer Minute statt in einer halben Stunde, hat es sich bereits ausgezahlt.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/31/email-protokoll-wordpress-fluentsmtp/">E-Mail-Protokoll in WordPress: warum ich es mit FluentSMTP kontrolliere</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/31/email-protokoll-wordpress-fluentsmtp/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PHP2Shell: So habe ich meine Server bis zum Patch geschützt</title>
		<link>https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/</link>
					<comments>https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 18:53:44 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[Absicherung]]></category>
		<category><![CDATA[CVE-2026-109618]]></category>
		<category><![CDATA[CVE-2026-109619]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[php2shell]]></category>
		<category><![CDATA[Server-Management]]></category>
		<category><![CDATA[Server-Sicherheit]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=852</guid>

					<description><![CDATA[<p>Zwei schwere Lücken im PHP-Kern (CVE-2026-109618 und CVE-2026-109619): was sie sind, welche Versionen betroffen sind, das Schutzskript und wie ich die Server aktualisiert habe.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/">PHP2Shell: So habe ich meine Server bis zum Patch geschützt</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Am 20. Oktober 2026 wurden zwei schwere Lücken im PHP-Kern veröffentlicht. Der gängige Name ist <strong>PHP2Shell</strong>, die Kennungen lauten CVE-2026-109618 und CVE-2026-109619. Der CVSS-Wert liegt bei 9,2. Sie erlauben Codeausführung auf dem Server ohne Anmeldung, und Angriffe wurden bereits beobachtet.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/php2shell-schema-php-luecke.webp" alt="Schema zu PHP2Shell: fünf Kreise, verbunden mit einem Kasten mit Terminal-Symbol" width="1200" height="630"/></figure>
<p>In diesem Artikel erkläre ich, was das ist und welche PHP-Versionen betroffen sind. Ich erzähle, wie ich die Server, die ich betreue, abgesichert habe, solange es keinen Patch gab. Außerdem beschreibe ich, wie ich zum endgültigen Update gekommen bin. Hier findest du auch das Skript, das ich benutzt habe, und die Befehle zum Starten.</p>
<h2 class="wp-block-heading">Was ist PHP2Shell</h2>
<p>Das Problem liegt darin, wie PHP Anfragen vom Typ <code>multipart/form-data</code> liest. Das ist das Format, mit dem der Browser Dateien sendet. PHP verarbeitet es, bevor irgendein Skript startet.</p>
<p>Darum braucht es kein Upload-Formular. Eine einzige Anfrage an irgendeine PHP-Datei der Seite kann genügen.</p>
<p>Die beiden Lücken in Kürze:</p>
<ul class="wp-block-list">
<li><strong>CVE-2026-109618.</strong> Wenn der Speicher ausgeht, während PHP die Anfrage liest, werden Daten beschädigt. Der Schaden zeigt sich beim Aufräumen am Ende der Anfrage und kann zur Codeausführung führen.</li>
<li><strong>CVE-2026-109619.</strong> Ein Teil des Zustands einer Anfrage bleibt im Speicher. Eine spätere Anfrage im selben Prozess verwendet ihn wieder.</li>
</ul>
<p>Diese technischen Details stammen aus der <a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">Analyse des niederländischen NCSC</a> und aus anderen öffentlichen Quellen. Ich habe nicht versucht, die Lücken auf meinen Servern auszunutzen.</p>
<h2 class="wp-block-heading">Welche PHP-Versionen sind betroffen</h2>
<p>Die korrigierten Versionen erschienen am 21. Oktober 2026: <strong>PHP 8.5.12, 8.4.27, 8.3.36 und 8.2.35</strong>. Jede ältere Version ist betroffen.</p>
<p>Für die Zweige 8.1 und älter gibt es keine offizielle korrigierte Version, weil sie nicht mehr unterstützt werden. PHP hat nur minimale Backports des Quellcodes für 7.4, 8.0 und 8.1 veröffentlicht. Für noch ältere Zweige ist nichts bekannt.</p>
<p>Das Risiko betrifft jede Art, PHP auszuführen. Am stärksten ausgesetzt ist mod_php. Auch PHP-FPM, php-cgi, FrankenPHP und LiteSpeed zählen. Dazu kommen alle Anwendungen darüber: WordPress, Joomla, Drupal, Nextcloud, phpMyAdmin, Roundcube und das ISPConfig-Panel.</p>
<p>Bei Debian gibt es ein Detail, das man kennen sollte. Debian übernimmt die Korrekturen als Backport und ändert die PHP-Versionsnummer nicht. Ein Server kann eine niedrige Nummer zeigen und trotzdem schon korrigiert sein. Deshalb vergleicht das Skript keine Versionen. Es prüft, ob das Changelog des installierten Pakets die beiden CVEs nennt.</p>
<h2 class="wp-block-heading">Die Schutzmaßnahme, bis der Patch da ist</h2>
<p>Als die Lücken öffentlich wurden, waren die Patches für die Distributionen noch nicht verfügbar. Ich wollte die Server nicht ungeschützt lassen. Also habe ich das Risiko mit einem Skript verringert.</p>
<p><strong>Die Schutzmaßnahme ist kein Patch.</strong> Wer <code>session.upload_progress</code> abschaltet, schließt nur einen der bekannten Angriffswege zur CVE-2026-109619. Gegen die CVE-2026-109618 hilft das nicht. Die Maßnahme sollte die Angriffsfläche bis zum Update verkleinern, nicht das Update ersetzen.</p>
<h3 class="wp-block-heading">Was das Skript macht</h3>
<p>Das Skript ist für Debian 12 und 13 gedacht. Es funktioniert mit Apache (mod_php, FastCGI, suPHP, PHP-FPM) und mit Nginx (PHP-FPM). Es erkennt Server, die mit ISPConfig verwaltet werden, ob Apache oder Nginx.</p>
<p>Die Optionen:</p>
<ul class="wp-block-list">
<li>Ohne Optionen prüft es nur. Für jedes PHP-Paket kontrolliert es, ob das Changelog die beiden CVEs nennt. Es meldet Prozesse, die noch eine alte Binärdatei nutzen, und liest die aktiven Einstellungen und die offenen Web-Ports aus.</li>
<li><code>--update</code> aktualisiert zuerst den Paketindex.</li>
<li><code>--logs</code> sucht verdächtige Spuren: neue PHP-Dateien in Upload- oder Cache-Ordnern, typische Webshell-Muster, Verweise auf php2shell in den Logs. Das ist eine Heuristik und beweist keine Kompromittierung.</li>
<li><code>--harden</code> wendet die unten beschriebene Absicherung an.</li>
<li><code>--dry-run</code> zeigt zusammen mit <code>--harden</code>, was passieren würde, ohne etwas zu ändern.</li>
<li><code>--skip-disable-functions</code> lässt zusammen mit <code>--harden</code> <code>disable_functions</code> unangetastet.</li>
<li><code>--rollback</code> nimmt zurück, was <code>--harden</code> angewendet hat.</li>
</ul>
<h3 class="wp-block-heading">Was sich mit &#8211;harden ändert</h3>
<p>Für jede PHP-Version in <code>/etc/php</code> legt das Skript die Datei <code>mods-available/cve-hardening.ini</code> an. Es aktiviert sie mit <code>phpenmod</code> für die SAPIs fpm, apache2 und cgi. So gilt der Schutz für alle Seiten auf einmal.</p>
<p>Die originalen <code>php.ini</code> bleiben unberührt. Die CLI wird nicht angefasst, also laufen composer, artisan und wp-cli weiter.</p>
<p>Die Datei setzt drei Dinge:</p>
<ul class="wp-block-list">
<li><code>session.upload_progress.enabled = Off</code></li>
<li><code>expose_php = Off</code></li>
<li><code>disable_functions</code> mit exec, passthru, shell_exec, system, proc_open, popen und pcntl_exec, zusätzlich zu den bereits vorhandenen.</li>
</ul>
<p>Danach testet das Skript die Konfiguration und lädt PHP-FPM und Apache nur neu, wenn der Test besteht. Vor dem Schreiben legt es ein Backup in <code>/var/backups/php-cve-hardening/</code> an.</p>
<h3 class="wp-block-heading">Die Grenzen, die du kennen solltest</h3>
<ul class="wp-block-list">
<li><code>disable_functions</code> kann manche Anwendungen kaputt machen, zum Beispiel WordPress mit bestimmten Plugins oder Magento. Dann nimm <code>--rollback</code> oder starte erneut mit <code>--skip-disable-functions</code>.</li>
<li>Die Direktiven <code>php_admin_value</code> in den PHP-FPM-Pools haben Vorrang vor der globalen Datei. Das Skript meldet sie, ändert sie aber nicht.</li>
<li>Bei ISPConfig werden die Dateien in <code>pool.d</code> vom Panel neu erzeugt. Overrides korrigierst du im Panel unter Sites, Website, Options, Custom php.ini settings, nicht von Hand.</li>
<li>Die <code>php.ini</code> einzelner Seiten (FastCGI- oder suPHP-Modus von ISPConfig) und PHP-Versionen in <code>/opt</code> werden nicht verwaltet. Sie müssen separat geprüft werden.</li>
</ul>
<h2 class="wp-block-heading">So startest du das Skript</h2>
<p>Das Skript liegt hier: <a href="https://cisilino.com/mscripts/php20261.sh">https://cisilino.com/mscripts/php20261.sh</a></p>
<p>Bevor du etwas aus dem Internet ausführst, lies es. Du kannst es herunterladen und öffnen:</p>
<pre class="wp-block-code"><code>wget -qO php20261.sh https://cisilino.com/mscripts/php20261.sh
less php20261.sh</code></pre>
<p>Wenn du es direkt aus der Ferne starten willst, brauchst du Root-Rechte. Das sind die Befehle, in der Reihenfolge, die ich empfehle.</p>
<p>1. Nur prüfen, mit Aktualisierung des Paketindex:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --update</code></pre>
<p>2. Absicherung simulieren, ohne Änderungen:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden --dry-run</code></pre>
<p>3. Anwenden:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden</code></pre>
<p>4. Entfernen, falls etwas kaputtgeht:</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --rollback</code></pre>
<p>Nach <code>--harden</code> und nach <code>--rollback</code> musst du nichts von Hand neu starten. Das Skript lädt PHP-FPM und Apache selbst neu, nachdem es die Konfiguration getestet hat. Ein Neustart ist dagegen nach dem Update des PHP-Pakets nötig, weil die laufenden Prozesse die alte Binärdatei im Speicher halten:</p>
<pre class="wp-block-code"><code>sudo systemctl restart 'php*-fpm' apache2</code></pre>
<p>Die Anführungszeichen um <code>php*-fpm</code> sind wichtig. Ohne sie expandiert die Shell das Sternchen, nicht systemctl. Wenn der Server nur Nginx nutzt, lass <code>apache2</code> weg.</p>
<p>Das Ergebnis des Skripts ist auch ein Exit-Code: 0, wenn alles in Ordnung ist, 1 bei Warnungen, 2 wenn das System verwundbar ist oder etwas getan werden muss.</p>
<h2 class="wp-block-heading">Auf Servern mit ISPConfig: ispconfig-multipart-guard</h2>
<p><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/ispconfig-logo.png" alt="ISPConfig-Logo" width="239" height="90"></a></p>
<p>Wer ISPConfig nutzt, hat ein zusätzliches Werkzeug. Das Projekt hat das Skript <strong>ispconfig-multipart-guard</strong> veröffentlicht, beschrieben in der <a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">offiziellen ISPConfig-Meldung</a>.</p>
<p>Das Prinzip ist einfach. Wenn das Problem in Multipart-Anfragen liegt, stoppt der Webserver sie, bevor sie PHP erreichen:</p>
<ul class="wp-block-list">
<li>Es blockiert Anfragen mit <code>Content-Type: multipart/...</code> für alle Seiten, für ausgewählte Seiten oder für das Control Panel;</li>
<li>bei Nginx antwortet es auf PHP-Anfragen der blockierten Seiten mit 415, statische Dateien bleiben unverändert. Bei Apache antwortet es mit 403, auch bei statischen Dateien;</li>
<li>standardmäßig blockiert es nur das ISPConfig-Panel. Die Seiten blockierst du mit Befehlen wie <code>block all</code> oder <code>block esempio.it</code>;</li>
<li>es legt einen Cronjob an, der alle 5 Minuten seine Hooks wiederherstellt, falls ISPConfig die Konfiguration neu schreibt;</li>
<li>es hat eine Liste erlaubter IPs, die weiterhin Dateien hochladen dürfen.</li>
</ul>
<p>Es läuft auf Debian und Ubuntu, mit ISPConfig 3.2 oder 3.3 (auch ohne ISPConfig), mit Nginx oder mit Apache 2.4 und neuer.</p>
<p>Der Preis: Auf blockierten Seiten funktionieren Uploads nicht. Das gilt auch für CMS-Medien, Formularanhänge, Anhänge im Webmail und Importe in phpMyAdmin. Normale Seiten und urlencoded-Formulare laufen weiter. Auch das ist eine vorläufige Maßnahme und keine Korrektur.</p>
<p>Dieses Skript und meines schließen sich nicht gegenseitig aus. Meines greift in die PHP-Konfiguration ein, das von ISPConfig in den Webserver. Ich habe sie nicht in der Praxis verglichen und kann dir nicht sagen, wie sie sich zusammen in jeder Konfiguration verhalten.</p>
<h2 class="wp-block-heading">Das endgültige Update, in zwei Schritten</h2>
<p>Die Schutzmaßnahmen sollten nur kurz gelten. Ich habe die Server aktualisiert, sobald die Updates verfügbar waren, in zwei Schritten:</p>
<ol class="wp-block-list">
<li><strong>Direkt kompilieren.</strong> Solange die Distribution auf dem jeweiligen Server ihr Paket noch nicht veröffentlicht hatte, habe ich PHP aus dem Quellcode in den korrigierten Versionen kompiliert.</li>
<li><strong>Offizielles Paket.</strong> Als das offizielle Paket der Distribution für diesen Server erschien, habe ich die Kompilierung durch das Paket ersetzt.</li>
</ol>
<p>Der zweite Schritt ist wichtig. Ein von Hand kompiliertes PHP aktualisiert sich nicht zusammen mit dem Rest des Systems. Das Paket der Distribution schon, und das Skript kann das über das Changelog prüfen.</p>
<h2 class="wp-block-heading">Was du jetzt tun solltest</h2>
<ol class="wp-block-list">
<li>Aktualisiere PHP auf 8.5.12, 8.4.27, 8.3.36 oder 8.2.35. Unter Debian kannst du auch das Paket der Distribution nehmen, wenn das Changelog die beiden CVEs nennt.</li>
<li>Starte PHP-FPM und Apache neu und lass das Skript dann ohne Optionen noch einmal laufen. Es darf keine Probleme mehr melden.</li>
<li>Wenn du PHP 8.1 oder älter nutzt, plane den Wechsel auf einen unterstützten Zweig. Für diese Zweige gibt es keine offizielle korrigierte Version.</li>
<li>Lass <code>session.upload_progress</code> deaktiviert, bis zur nächsten Korrekturversion, falls deine Anwendung es nicht braucht.</li>
<li>Prüfe Logs und neue Dateien, mit <code>--logs</code> oder von Hand, und suche nach PHP-Dateien, die in Upload-Ordnern aufgetaucht sind.</li>
<li>Vergiss PHP in Containern, Images und Appliances von Dritten nicht. Sie müssen separat aktualisiert werden.</li>
<li>Wenn deine Seite WordPress nutzt: Das <a href="https://www.cisilino.com/de/2026/10/29/sicherheits-plugin-fur-wordpress/">Plugin Cisilino Site Defense Network</a> löst PHP2Shell nicht. Es schützt aber den Login und führt ein Aktivitätsprotokoll, das hilft zu verstehen, was auf der Seite passiert.</li>
</ol>
<h2 class="wp-block-heading">Quellen</h2>
<ul class="wp-block-list">
<li><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">ISPConfig: PHP2Shell, kritische PHP-Schwachstellen (CVE-2026-109618, CVE-2026-109619)</a></li>
<li><a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">NCSC-NL: Wiki zu PHP2Shell</a></li>
</ul>
<p>ISPConfig, PHP, Debian und die anderen genannten Namen sind Marken der jeweiligen Inhaber. Sie dienen nur zur Identifizierung der Produkte.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/">PHP2Shell: So habe ich meine Server bis zum Patch geschützt</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/29/php2shell-cve-2026-109618-server-absichern/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Welche Datenbank belegt den Speicherplatz? Ein Skript für MySQL und PostgreSQL</title>
		<link>https://www.cisilino.com/de/2026/10/20/skript-speicherplatz-datenbank-mysql/</link>
					<comments>https://www.cisilino.com/de/2026/10/20/skript-speicherplatz-datenbank-mysql/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 07:53:00 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[Bash-Skript]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Speicherplatz]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=268</guid>

					<description><![CDATA[<p>Die Festplatte des Servers läuft voll und Sie wissen nicht, welche Datenbank schuld ist? Ein Skript, das den Platzbedarf von MySQL und PostgreSQL misst und im Zeitverlauf vergleicht.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/20/skript-speicherplatz-datenbank-mysql/">Welche Datenbank belegt den Speicherplatz? Ein Skript für MySQL und PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Das passiert früher oder später jedem: Die Festplatte des Servers läuft voll, der Alarm geht los und die Frage ist immer dieselbe. <strong>Welche Datenbank belegt den ganzen Platz?</strong> Beim Speicherplatz von MySQL gibt es dafür selten eine schnelle Antwort.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/speicherplatz-datenbanken-mysql-postgresql.webp" alt="Speicherplatz von MySQL als Ringdiagramm in vier farbige Abschnitte geteilt, rechts drei Legendenbalken" width="1200" height="630"/></figure>
<p>Ich stand davor nach einer Optimierung. Ich hatte <code>OPTIMIZE TABLE</code> ausgeführt in der Hoffnung, Platz zurückzugewinnen, aber das Dateisystem schrumpfte nicht wie erwartet. Ohne ein „Vorher“, mit dem man das „Nachher“ vergleichen konnte, ließ sich nicht sagen, ob die Optimierung gewirkt hatte, welche Datenbank wirklich verantwortlich war und ob der fehlende Platz vielleicht ganz woanders steckte.</p>
<p>Die Werkzeuge, die ich hatte, beantworteten nur die Hälfte des Problems.</p>
<ul class="wp-block-list">
<li><code>du</code> zeigt die Dateien auf der Platte, sagt aber nicht, zu welcher Tabelle sie gehören.</li>
<li><code>information_schema</code> (oder <code>pg_class</code> bei PostgreSQL) liefert die logischen Größen, sieht aber weder Binlogs noch <code>ibdata1</code>, Undo-Logs oder WAL.</li>
<li>beide zeigen nur einen einzigen Augenblick: Der Vergleich über die Zeit bleibt Handarbeit mit Copy-and-paste und Tabellenkalkulation.</li>
</ul>
<p>Ich brauchte etwas Einfaches: <strong>den Zustand aller Datenbanken zu einem bestimmten Zeitpunkt festhalten, später erneut festhalten und mir sagen lassen, was sich geändert hat</strong>, die Unterschiede sortiert vom größten zum kleinsten. So fällt der Übeltäter sofort auf.</p>
<p>Herausgekommen ist ein einzelnes Bash-Skript ohne Abhängigkeiten außer dem Datenbank-Client und den üblichen Linux-Werkzeugen (<code>awk</code>, <code>du</code>, <code>df</code>). Es funktioniert mit <strong>MySQL/MariaDB</strong> und mit <strong>PostgreSQL</strong>. Ich teile es hier in der Hoffnung, jemandem denselben frustrierenden Nachmittag zu ersparen.</p>
<h2 class="wp-block-heading">Was es tut</h2>
<p>Das Skript hat vier Befehle.</p>
<ul class="wp-block-list">
<li><code>snapshot</code> speichert den aktuellen Zustand (Datenbanken, Tabellen, Dateien im Datenverzeichnis, Platz des Dateisystems).</li>
<li><code>compare</code> vergleicht zwei Momentaufnahmen und zeigt die Unterschiede, die größten zuerst.</li>
<li><code>report</code> zeigt die größten Datenbanken und Tabellen einer einzelnen Momentaufnahme.</li>
<li><code>list</code> listet die gespeicherten Momentaufnahmen auf.</li>
</ul>
<p>Der Vergleich ist in vier Abschnitte unterteilt, damit keine blinden Flecken bleiben.</p>
<ul class="wp-block-list">
<li><strong>Größe jeder Datenbank</strong> (Daten + Indizes).</li>
<li><strong>freier oder rückgewinnbarer Platz</strong> (<code>data_free</code> bei MySQL, Schätzung der toten Tupel bei PostgreSQL).</li>
<li><strong>Größe jeder Tabelle</strong>.</li>
<li><strong>tatsächliche Dateien im Datenverzeichnis</strong>: Datenbankordner, <code>ibdata1</code>, Binlogs, Undo, <code>pg_wal</code>.</li>
</ul>
<p>Der letzte Abschnitt löst das Rätsel oft. Viele Male gehört der „auffällige“ Platz zu keiner Datenbank, sondern zu den Binärlogs oder zu den gemeinsamen InnoDB-Dateien.</p>
<h2 class="wp-block-heading">Voraussetzungen</h2>
<ul class="wp-block-list">
<li>Linux mit Bash 4 oder höher, <code>awk</code>, <code>du</code>, <code>df</code> (alles Standard).</li>
<li>der Client <code>mysql</code> oder <code>psql</code>, mit einem Benutzer, der die Metadaten aller Datenbanken lesen darf.</li>
<li>Root-Rechte (oder die des Benutzers <code>mysql</code>/<code>postgres</code>), wenn Sie auch die Dateien auf der Platte messen wollen; ohne sie wird dieser Abschnitt mit einer Warnung übersprungen.</li>
</ul>
<h2 class="wp-block-heading">Installation</h2>
<p>Speichern Sie das Skript (am Ende des Artikels) in einer Datei und machen Sie es ausführbar.</p>
<pre class="wp-block-code"><code>nano db_space_compare.sh
chmod +x db_space_compare.sh</code></pre>
<h3 class="wp-block-heading">Zugangsdaten</h3>
<p>Für MySQL/MariaDB ist der sauberste Weg eine Datei <code>~/.my.cnf</code> mit den Zugangsdaten, damit sie nicht im Befehlsverlauf auftauchen.</p>
<pre class="wp-block-code"><code>&#91;client]
user=root
password=LA_TUA_PASSWORD</code></pre>
<p>Alternativ können Sie die Optionen über eine Umgebungsvariable übergeben.</p>
<pre class="wp-block-code"><code>export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"</code></pre>
<p>Für PostgreSQL funktionieren <code>~/.pgpass</code>, die Variablen <code>PG*</code> oder <code>PG_OPTS</code>.</p>
<pre class="wp-block-code"><code>export PG_OPTS="-U postgres -h localhost"</code></pre>
<h2 class="wp-block-heading">So verwenden Sie es</h2>
<p>Der typische Ablauf ist <em>vorher, Optimierung, nachher, Vergleich</em>.</p>
<pre class="wp-block-code"><code># 1. fotografia prima dell'ottimizzazione
sudo ./db_space_compare.sh snapshot prima

# 2. esegui l'ottimizzazione (OPTIMIZE TABLE, VACUUM FULL, ...)

# 3. fotografia dopo
sudo ./db_space_compare.sh snapshot dopo

# 4. confronto
sudo ./db_space_compare.sh compare prima dopo</code></pre>
<p>Für PostgreSQL genügt es, die Option <code>-e postgres</code> hinzuzufügen.</p>
<pre class="wp-block-code"><code>sudo ./db_space_compare.sh -e postgres snapshot prima
sudo ./db_space_compare.sh -e postgres snapshot dopo
sudo ./db_space_compare.sh -e postgres compare prima dopo</code></pre>
<p>Die verfügbaren Optionen sind.</p>
<ul class="wp-block-list">
<li><code>-e mysql|postgres</code> wählt die Engine (Standard: <code>mysql</code>).</li>
<li><code>-d DIR</code> ändert den Ordner, in dem die Momentaufnahmen gespeichert werden (Standard: <code>./db_snapshots</code>).</li>
<li><code>-n N</code> legt fest, wie viele Zeilen pro Abschnitt angezeigt werden (Standard: 20).</li>
</ul>
<p>Geben Sie keine Bezeichnung an, erhält die Momentaufnahme Datum und Uhrzeit als Namen. Das ist praktisch, um das Wachstum über die Zeit zu überwachen.</p>
<h2 class="wp-block-heading">Beispielausgabe</h2>
<p>Hier ein Vergleich mit Testdaten, extra gebaut, um einen typischen Fall zu zeigen: Die Datenbank <code>shop</code> schrumpft nach der Optimierung, aber die Datenbank <code>log</code> wächst und die Binlogs verdoppeln sich.</p>
<pre class="wp-block-code"><code>Confronto 'prima' (2026-10-11 20:00:00)  -&gt;  'dopo' (2026-10-11 22:00:00)
Filesystem /var: usato 46.6 GiB -&gt; 48.4 GiB (+1.9 GiB)

=== Database - dimensione totale ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop                                                    5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%

=== Tabelle - dimensione (dati + indici) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log.eventi                                             21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop.ordini                                             5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%
  log.nuova                                                   0 B      1.0 KiB      +1.0 KiB  NUOVO

=== File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  mysql-bin.*                                             3.7 GiB      8.4 GiB      +4.7 GiB  +125.0%
  shop                                                    5.7 GiB      3.6 GiB      -2.0 GiB  -36.1%
  ibdata1                                                11.2 GiB     11.2 GiB           0 B</code></pre>
<p>Hier ist der Übeltäter offensichtlich: Die Tabelle <code>log.eventi</code> ist um mehr als 6 GiB gewachsen, und die Dateien <code>mysql-bin.*</code> sind von 3,7 auf 8,4 GiB gestiegen. Die Optimierung von <code>shop</code> hat funktioniert, aber der Gewinn wurde vom Rest aufgefressen.</p>
<h2 class="wp-block-heading">Wie man die Ergebnisse zum Speicherplatz von MySQL liest</h2>
<ul class="wp-block-list">
<li>Am Anfang jedes Abschnitts stehen die Einträge mit der größten Änderung. Eine Datenbank oder Tabelle, die nach der Optimierung <strong>nicht schrumpft</strong> (oder sogar wächst), ist der erste Verdächtige.</li>
<li>Schrumpfen die Tabellen, das Dateisystem aber nicht, liegt der Platz woanders: Sehen Sie sich den Abschnitt über die Dateien im Datenverzeichnis an (Binlogs, <code>ibdata1</code>, Undo, <code>pg_wal</code>).</li>
<li><strong>MySQL/MariaDB:</strong> <code>ibdata1</code> schrumpft nie, auch nicht mit <code>OPTIMIZE TABLE</code>. Mit <code>innodb_file_per_table=OFF</code> ist der Wert von <code>data_free</code> gemeinsam genutzt und wenig aussagekräftig.</li>
<li><strong>PostgreSQL:</strong> Ein einfaches <code>VACUUM</code> gibt keinen Platz an das Betriebssystem zurück, dafür braucht man <code>VACUUM FULL</code> (oder <code>pg_repack</code>). Der angezeigte „rückgewinnbare“ Platz ist eine Schätzung auf Basis der toten Tupel. Wie PostgreSQL den Platz selbst berechnet, erklärt die <a href="https://www.postgresql.org/docs/current/diskusage.html" target="_blank" rel="noopener">Dokumentation zur Plattenbelegung</a>.</li>
</ul>
<h2 class="wp-block-heading">Wachstum über die Zeit überwachen</h2>
<p>Hängt das Problem nicht an einer einzelnen Optimierung, sondern an schrittweisem Wachstum, lohnt es sich, die Momentaufnahmen mit cron zu automatisieren, eine pro Stunde, und dann zwei beliebige Zeitpunkte zu vergleichen.</p>
<pre class="wp-block-code"><code># crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot &gt;/dev/null 2&gt;&amp;1</code></pre>
<p>Ohne Bezeichnung heißt jede Momentaufnahme nach Datum und Uhrzeit (zum Beispiel <code>20261011-140000</code>). Mit <code>list</code> sehen Sie die vorhandenen, mit <code>compare</code> vergleichen Sie zwei. In wenigen Tagen zeigt sich, welche Datenbank ungewöhnlich wächst.</p>
<pre class="wp-block-code"><code>./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000</code></pre>
<h2 class="wp-block-heading">Das vollständige Skript</h2>
<p>Hier ist der Code. Kopieren Sie ihn in eine Datei namens <code>db_space_compare.sh</code>, machen Sie sie ausführbar, und Sie sind startklar.</p>
<pre class="wp-block-code"><code>#!/usr/bin/env bash
#
# db_space_compare.sh
# Fotografa l'occupazione di spazio di tutti i database (MySQL/MariaDB o
# PostgreSQL) e confronta due fotografie (es. prima/dopo l'ottimizzazione)
# per capire quale database, tabella o file occupa spazio in modo anomalo.
#
# Uso:
#   db_space_compare.sh &#91;opzioni] snapshot &#91;etichetta]   (default: data-ora)
#   db_space_compare.sh &#91;opzioni] compare  &lt;prima&gt; &lt;dopo&gt;
#   db_space_compare.sh &#91;opzioni] report   &lt;etichetta&gt;
#   db_space_compare.sh &#91;opzioni] list
#
# Opzioni:
#   -e mysql|postgres   motore del database (default: mysql)
#   -d DIR              cartella delle fotografie (default: ./db_snapshots)
#   -n N                righe mostrate per sezione (default: 20)
#
# Credenziali:
#   MySQL/MariaDB: ~/.my.cnf oppure MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
#   PostgreSQL:    ~/.pgpass, variabili PG* oppure PG_OPTS="-U postgres -h localhost"
#                  (non mettere -d in PG_OPTS: lo script si collega a ogni database)
#
# Per misurare anche i file su disco (cartelle dei DB, ibdata1, binlog, undo,
# pg_wal) va eseguito come root o come utente mysql/postgres; altrimenti
# quella parte viene saltata con un avviso.
#
# Esempio:
#   sudo ./db_space_compare.sh snapshot prima
#   ... ottimizzazione (OPTIMIZE TABLE / VACUUM FULL / ...) ...
#   sudo ./db_space_compare.sh snapshot dopo
#   sudo ./db_space_compare.sh compare prima dopo

set -euo pipefail
export LC_ALL=C

ENGINE=mysql
OUTDIR=./db_snapshots
TOPN=20
SNAP_IN_PROGRESS=""

die()  { echo "ERRORE: $*" &gt;&amp;2; exit 1; }
warn() { echo "ATTENZIONE: $*" &gt;&amp;2; }
usage() { sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0"; exit "${1:-0}"; }

cleanup() {
  if &#91;&#91; -n $SNAP_IN_PROGRESS ]]; then
    rm -rf -- "$SNAP_IN_PROGRESS"
    echo "Fotografia incompleta rimossa: $SNAP_IN_PROGRESS" &gt;&amp;2
  fi
}
trap cleanup EXIT

while getopts ":e:d:n:h" opt; do
  case $opt in
    e) ENGINE=$OPTARG ;;
    d) OUTDIR=$OPTARG ;;
    n) TOPN=$OPTARG ;;
    h) usage 0 ;;
    *) usage 1 ;;
  esac
done
shift $((OPTIND - 1))

&#91;&#91; $ENGINE == mysql || $ENGINE == postgres ]] || die "motore non valido: '$ENGINE' (usa mysql o postgres)"
&#91;&#91; $TOPN =~ ^&#91;0-9]+$ ]] || die "-n deve essere un numero"

read -r -a MYSQL_ARGS &lt;&lt;&lt; "${MYSQL_OPTS:-}"
read -r -a PG_ARGS    &lt;&lt;&lt; "${PG_OPTS:-}"

my() { mysql "${MYSQL_ARGS&#91;@]+"${MYSQL_ARGS&#91;@]}"}" --batch --skip-column-names "$@"; }
pg() { psql  "${PG_ARGS&#91;@]+"${PG_ARGS&#91;@]}"}" -X -q -A -t -F $'\t' -v ON_ERROR_STOP=1 "$@"; }

# Funzioni awk comuni: formattazione in KiB/MiB/GiB, chiave e valore di riga.
#   nk = numero di colonne che formano il nome (1 = database, 2 = db.tabella)
#   vc = colonne da sommare per il valore (es. "4,5" = dati + indici)
AWK_LIB='
function h(b,   a, i, u) {
  split("B KiB MiB GiB TiB PiB", u, " ")
  a = (b &lt; 0) ? -b : b; i = 1
  while (a &gt;= 1024 &amp;&amp; i &lt; 6) { a /= 1024; i++ }
  if (i == 1) return sprintf("%s%d B", (b &lt; 0 ? "-" : ""), a)
  return sprintf("%s%.1f %s", (b &lt; 0 ? "-" : ""), a, u&#91;i])
}
function hs(b) { return (b &gt; 0 ? "+" : "") h(b) }
function key(   k, i) { k = $1; for (i = 2; i &lt;= nk; i++) k = k "." $i; return k }
function val(   n, c, i, s) { n = split(vc, c, ","); s = 0; for (i = 1; i &lt;= n; i++) s += $(c&#91;i]); return s }
'

meta_get() { awk -F= -v k="$2" '$1 == k { sub(/^&#91;^=]*=/, ""); print; exit }' "$1/meta.txt"; }

# ---------------------------------------------------------------- raccolta dati

# Occupazione reale su disco delle voci di primo livello della data directory:
# cartelle dei database, ibdata1, binlog, undo, redo log, pg_wal, base, ...
# I file numerati (mysql-bin.000123, relay-log.000045, ...) sono sommati in una voce.
collect_disk() {
  local d=$1 out=$2
  : &gt; "$out"
  if &#91;&#91; -z $d || ! -r $d || ! -x $d ]]; then
    warn "data directory '${d:-?}' non leggibile: salto i file su disco (esegui come root per includerli)"
    return 0
  fi
  { du -sb -- "$d"/* 2&gt;/dev/null || true; } |
    awk -F'\t' -v p="$d/" '
      { n = $2
        if (index(n, p) == 1) n = substr(n, length(p) + 1)
        if (n ~ /\.&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]$/) sub(/&#91;0-9]+$/, "*", n)
        s&#91;n] += $1 }
      END { for (n in s) printf "%s\t%.0f\n", n, s&#91;n] }' &gt; "$out"
}

write_meta() {
  local dir=$1 datadir=$2 version=$3
  {
    echo "timestamp=$(date '+%Y-%m-%d %H:%M:%S')"
    echo "engine=$ENGINE"
    echo "host=$(hostname)"
    echo "version=$version"
    echo "datadir=$datadir"
    if &#91;&#91; -n $datadir &amp;&amp; -e $datadir ]]; then
      { df -P -B1 -- "$datadir" 2&gt;/dev/null || true; } |
        awk 'NR == 2 { print "fs_mount=" $6; print "fs_used=" $3; print "fs_avail=" $4 }'
    fi
  } &gt; "$dir/meta.txt"
}

# File prodotti da ogni fotografia (separati da TAB):
#   tables.tsv     db, tabella, engine, dati, indici, libero/recuperabile, righe
#   databases.tsv  db, dati, indici, libero/recuperabile, totale
#   disk.tsv       voce della data directory, byte su disco
#   meta.txt       data, host, versione, filesystem

snapshot_mysql() {
  local dir=$1 pre="" datadir version
  command -v mysql &gt;/dev/null 2&gt;&amp;1 || die "client 'mysql' non trovato"
  my -e "SELECT 1" &gt;/dev/null || die "connessione a MySQL/MariaDB fallita (controlla ~/.my.cnf o MYSQL_OPTS)"

  # MySQL 8 tiene in cache le statistiche di information_schema fino a 24 ore:
  # senza questo, i valori "dopo" l'ottimizzazione potrebbero essere quelli vecchi.
  if &#91;&#91; -n $(my -e "SHOW VARIABLES LIKE 'information_schema_stats_expiry'") ]]; then
    pre="SET SESSION information_schema_stats_expiry = 0;"
  fi

  my -e "$pre
    SELECT table_schema, table_name, IFNULL(engine, '-'),
           IFNULL(data_length, 0), IFNULL(index_length, 0), IFNULL(data_free, 0), IFNULL(table_rows, 0)
      FROM information_schema.TABLES
     WHERE table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
       AND table_schema NOT IN ('information_schema', 'performance_schema', 'sys')" &gt; "$dir/tables.tsv"

  my -e "$pre
    SELECT s.schema_name,
           IFNULL(SUM(t.data_length), 0), IFNULL(SUM(t.index_length), 0), IFNULL(SUM(t.data_free), 0),
           IFNULL(SUM(t.data_length), 0) + IFNULL(SUM(t.index_length), 0)
      FROM information_schema.SCHEMATA s
      LEFT JOIN information_schema.TABLES t
             ON t.table_schema = s.schema_name
            AND t.table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
     WHERE s.schema_name NOT IN ('information_schema', 'performance_schema', 'sys')
     GROUP BY s.schema_name" &gt; "$dir/databases.tsv"

  datadir=$(my -e "SELECT @@datadir"); datadir=${datadir%/}
  version=$(my -e "SELECT VERSION()")
  collect_disk "$datadir" "$dir/disk.tsv"
  write_meta "$dir" "$datadir" "$version"
}

snapshot_postgres() {
  local dir=$1 maindb=${PGDATABASE:-postgres} datadir version db
  command -v psql &gt;/dev/null 2&gt;&amp;1 || die "client 'psql' non trovato"
  pg -d "$maindb" -c "SELECT 1" &gt;/dev/null || die "connessione a PostgreSQL fallita (controlla PG_OPTS, PGUSER, ~/.pgpass)"

  pg -d "$maindb" -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY 1" &gt; "$dir/.dblist"

  # Per ogni database: dimensione di ogni tabella (catalogo incluso, es. pg_largeobject)
  # e stima dello spazio occupato da tuple morte (bloat recuperabile con VACUUM FULL).
  : &gt; "$dir/tables.tsv"
  while IFS= read -r db; do
    &#91;&#91; -n $db ]] || continue
    pg -d "$db" -c "
      SELECT current_database(), s.schemaname || '.' || s.relname, 'table',
             pg_table_size(s.relid), pg_indexes_size(s.relid),
             CASE WHEN s.n_live_tup + s.n_dead_tup &gt; 0
                  THEN (pg_table_size(s.relid)::numeric * s.n_dead_tup / (s.n_live_tup + s.n_dead_tup))::bigint
                  ELSE 0 END,
             s.n_live_tup
        FROM pg_stat_all_tables s
       WHERE s.schemaname &lt;&gt; 'information_schema'
         AND s.schemaname NOT LIKE 'pg_toast%'" &lt; /dev/null &gt;&gt; "$dir/tables.tsv" \
      || warn "impossibile leggere le tabelle del database '$db'"
  done &lt; "$dir/.dblist"

  pg -d "$maindb" -c "SELECT datname, pg_database_size(oid) FROM pg_database WHERE datallowconn AND NOT datistemplate" &gt; "$dir/.dbsize"
  awk -F'\t' '
    FILENAME == ARGV&#91;1] { d&#91;$1] += $4; i&#91;$1] += $5; f&#91;$1] += $6; next }
    { printf "%s\t%.0f\t%.0f\t%.0f\t%s\n", $1, d&#91;$1], i&#91;$1], f&#91;$1], $2 }' \
    "$dir/tables.tsv" "$dir/.dbsize" &gt; "$dir/databases.tsv"

  datadir=$(pg -d "$maindb" -c "SHOW data_directory" 2&gt;/dev/null || true)
  version=$(pg -d "$maindb" -c "SHOW server_version")
  collect_disk "$datadir" "$dir/disk.tsv"

  # Dettaglio di base/&lt;oid&gt; con il nome del database al posto dell'oid
  if &#91;&#91; -n $datadir &amp;&amp; -r $datadir/base &amp;&amp; -x $datadir/base ]]; then
    pg -d "$maindb" -c "SELECT oid, datname FROM pg_database" &gt; "$dir/.oidmap"
    { du -sb -- "$datadir"/base/* 2&gt;/dev/null || true; } |
      awk -F'\t' '
        FILENAME == ARGV&#91;1] { name&#91;$1] = $2; next }
        { o = $2; sub(/.*\//, "", o); printf "base/%s\t%s\n", ((o in name) ? name&#91;o] : o), $1 }' \
        "$dir/.oidmap" - &gt;&gt; "$dir/disk.tsv"
  fi
  write_meta "$dir" "$datadir" "$version"
}

# --------------------------------------------------------------- presentazione

print_fs() {
  local d=$1 used avail mount
  used=$(meta_get "$d" fs_used); avail=$(meta_get "$d" fs_avail); mount=$(meta_get "$d" fs_mount)
  &#91;&#91; -n $used ]] || return 0
  awk -v u="$used" -v a="$avail" -v m="$mount" "$AWK_LIB"'
    BEGIN { printf "Filesystem %s: usato %s, libero %s\n", m, h(u), h(a) }'
}

top_section() {
  local f=$1 nk=$2 vc=$3 title=$4
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $f ]]; then echo "  (nessun dato)"; return 0; fi
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    { v = val(); printf "%.0f\t%s\t%s\n", v, key(), h(v) }' "$f" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top &amp;&amp; $1 &gt; 0 { printf "  %-60s %12s\n", $2, $3 }'
}

compare_section() {
  local fa=$1 fb=$2 nk=$3 vc=$4 title=$5
  &#91;&#91; -f $fa ]] || fa=/dev/null
  &#91;&#91; -f $fb ]] || fb=/dev/null
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $fa &amp;&amp; ! -s $fb ]]; then echo "  (nessun dato)"; return 0; fi
  printf '  %-50s %12s %12s %13s  %s\n' "NOME" "PRIMA" "DOPO" "DIFFERENZA" "NOTE"
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    FILENAME == ARGV&#91;1] { k = key(); A&#91;k] = val(); S&#91;k] = 1; next }
                        { k = key(); B&#91;k] = val(); S&#91;k] = 1 }
    END {
      for (k in S) {
        inA = (k in A); inB = (k in B)
        a = inA ? A&#91;k] : 0; b = inB ? B&#91;k] : 0; d = b - a
        if (!inA)                 note = "NUOVO"
        else if (!inB)            note = "RIMOSSO"
        else if (a &gt; 0 &amp;&amp; d != 0) note = sprintf("%+.1f%%", 100 * d / a)
        else                      note = ""
        printf "%.0f\t%s\t%s\t%s\t%s\t%s\n", (d &lt; 0 ? -d : d), k, h(a), h(b), hs(d), note
      }
    }' "$fa" "$fb" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top { printf "  %-50s %12s %12s %13s  %s\n", $2, $3, $4, $5, $6 }'
}

# -------------------------------------------------------------------- comandi

cmd_snapshot() {
  local label=${1:-$(date '+%Y%m%d-%H%M%S')}
  &#91;&#91; $label =~ ^&#91;A-Za-z0-9]&#91;A-Za-z0-9._-]*$ ]] || die "etichetta non valida: usa solo lettere, numeri, . _ -"
  local dir=$OUTDIR/$label
  &#91;&#91; ! -e $dir ]] || die "la fotografia '$label' esiste già ($dir)"
  mkdir -p -- "$dir"
  SNAP_IN_PROGRESS=$dir
  echo "Fotografia '$label' ($ENGINE) in corso..."
  "snapshot_$ENGINE" "$dir"
  rm -f -- "$dir/.dblist" "$dir/.dbsize" "$dir/.oidmap"
  SNAP_IN_PROGRESS=""
  echo "Salvata in $dir ($(wc -l &lt; "$dir/tables.tsv") tabelle)"
  print_fs "$dir"
  top_section "$dir/databases.tsv" 1 5 "Database più grandi"
}

cmd_report() {
  local label=${1:-}
  &#91;&#91; -n $label ]] || die "uso: report &lt;etichetta&gt;"
  local d=$OUTDIR/$label
  &#91;&#91; -f $d/meta.txt ]] || die "fotografia '$label' non trovata in $OUTDIR"
  echo "Fotografia '$label' del $(meta_get "$d" timestamp) - $(meta_get "$d" engine) $(meta_get "$d" version) su $(meta_get "$d" host)"
  print_fs "$d"
  top_section "$d/databases.tsv" 1 5   "Database più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 4,5 "Tabelle più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 6   "Tabelle con più spazio recuperabile (data_free / tuple morte stimate)"
  top_section "$d/disk.tsv"      1 2   "Voci più grandi nella data directory"
}

cmd_compare() {
  local a=${1:-} b=${2:-}
  &#91;&#91; -n $a &amp;&amp; -n $b ]] || die "uso: compare &lt;etichetta_prima&gt; &lt;etichetta_dopo&gt;"
  &#91;&#91; $a != "$b" ]] || die "le due etichette sono uguali"
  local da=$OUTDIR/$a db=$OUTDIR/$b
  &#91;&#91; -f $da/meta.txt ]] || die "fotografia '$a' non trovata in $OUTDIR"
  &#91;&#91; -f $db/meta.txt ]] || die "fotografia '$b' non trovata in $OUTDIR"
  &#91;&#91; $(meta_get "$da" engine) == "$(meta_get "$db" engine)" ]] || warn "le due fotografie sono di motori diversi"

  echo "Confronto '$a' ($(meta_get "$da" timestamp))  -&gt;  '$b' ($(meta_get "$db" timestamp))"
  local ua ub m
  ua=$(meta_get "$da" fs_used); ub=$(meta_get "$db" fs_used); m=$(meta_get "$db" fs_mount)
  if &#91;&#91; -n $ua &amp;&amp; -n $ub ]]; then
    awk -v a="$ua" -v b="$ub" -v m="$m" "$AWK_LIB"'
      BEGIN { printf "Filesystem %s: usato %s -&gt; %s (%s)\n", m, h(a), h(b), hs(b - a) }'
  fi

  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 5   "Database - dimensione totale"
  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 4   "Database - spazio libero/recuperabile"
  compare_section "$da/tables.tsv"    "$db/tables.tsv"    2 4,5 "Tabelle - dimensione (dati + indici)"
  compare_section "$da/disk.tsv"      "$db/disk.tsv"      1 2   "File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL)"

  cat &lt;&lt;'EOF'

Come leggere il risultato:
  - In cima a ogni sezione ci sono le voci cambiate di più: un database o una
    tabella che dopo l'ottimizzazione non cala, o addirittura cresce, è il
    primo sospetto.
  - Se le tabelle calano ma il filesystem no, lo spazio sta altrove: guarda la
    sezione "File nella data directory" (binlog, ibdata1, undo, pg_wal).
  - MySQL/MariaDB: ibdata1 non si restringe mai, nemmeno con OPTIMIZE; con
    innodb_file_per_table=OFF il data_free è condiviso e poco indicativo.
  - PostgreSQL: VACUUM semplice non restituisce spazio al disco, serve
    VACUUM FULL (o pg_repack); lo spazio "recuperabile" è una stima.
EOF
}

cmd_list() {
  if &#91;&#91; ! -d $OUTDIR ]]; then echo "Nessuna fotografia in $OUTDIR"; return 0; fi
  local d found=0
  for d in "$OUTDIR"/*/; do
    &#91;&#91; -f $d/meta.txt ]] || continue
    found=1
    printf '%-24s %s  %-8s  %s\n' "$(basename "$d")" "$(meta_get "$d" timestamp)" \
      "$(meta_get "$d" engine)" "$(meta_get "$d" host)"
  done
  &#91;&#91; $found -eq 1 ]] || echo "Nessuna fotografia in $OUTDIR"
}

cmd=${1:-}
shift || true
case $cmd in
  snapshot) cmd_snapshot "$@" ;;
  compare)  cmd_compare "$@" ;;
  report)   cmd_report "$@" ;;
  list)     cmd_list ;;
  ""|help)  usage 0 ;;
  *)        die "comando sconosciuto: '$cmd' (usa snapshot, compare, report, list)" ;;
esac</code></pre>
<h2 class="wp-block-heading">Abschließende Hinweise</h2>
<ul class="wp-block-list">
<li>Das Skript liest den Speicherplatz von MySQL nur aus und ist auf den Datenbanken <strong>schreibgeschützt</strong>: Es führt nur <code>SELECT</code> auf den Metadaten aus. Es schreibt ausschließlich in den Ordner der Momentaufnahmen.</li>
<li>Unter MySQL 8 setzt es <code>information_schema_stats_expiry = 0</code> in der Sitzung, weil sonst die zwischengespeicherten Statistiken (bis zu 24 Stunden) auch nach der Optimierung „alte“ Werte zeigen könnten.</li>
<li>Schlägt eine Momentaufnahme auf halbem Weg fehl, wird sie automatisch entfernt, um keine unvollständigen Daten zu hinterlassen.</li>
<li>Bevor Sie es in der Produktion einsetzen, probieren Sie es in einer Testumgebung aus und prüfen die Berechtigungen des Datenbankbenutzers.</li>
</ul>
<p>Wenn Sie es nutzen und einen Fall finden, den es nicht abdeckt, oder Ideen zur Erweiterung haben (andere Engines, CSV-Export, E-Mail-Benachrichtigungen), schreiben Sie mir in die Kommentare. Läuft MySQL nach einem Upgrade gar nicht mehr, hilft dieser Artikel: <a href="https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/">MySQL startet nach dem Debian-12-Upgrade nicht mehr</a>.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/20/skript-speicherplatz-datenbank-mysql/">Welche Datenbank belegt den Speicherplatz? Ein Skript für MySQL und PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/20/skript-speicherplatz-datenbank-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL 5.7 in Docker, geschützt mit nftables</title>
		<link>https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/</link>
					<comments>https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 17 Oct 2026 08:10:45 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Firewall]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[nftables]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=275</guid>

					<description><![CDATA[<p>So betreiben Sie MySQL 5.7 in Docker auf einem modernen Server, ohne es dem Internet auszusetzen: docker-compose, geschlossener Port und nftables-Firewall.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/">MySQL 5.7 in Docker, geschützt mit nftables</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">Vorbemerkung: warum MySQL 5.7 in Docker</h2>
<p>Wir brauchten <strong>MySQL 5.7 in Docker</strong> auf einem Server, der sonst mit einer aktuellen Distribution läuft. Einige Altanwendungen sind nicht mit MySQL 8 kompatibel (geänderte Standard-Authentifizierung, strengere SQL-Modi, neue reservierte Wörter), und sie kurzfristig zu migrieren kam nicht in Frage.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-5-7-docker-nftables-absichern.webp" alt="MySQL 5.7 in Docker: eine Datenbank in einem Kasten mit orangefarbenem gestricheltem Rand, davor ein Kreis und ein Balken" width="1200" height="630"/></figure>
<p>Eine so alte Version direkt auf dem Host zu installieren, bringt mehrere Probleme. Die offiziellen Repositories aktueller Distributionen bieten sie nicht mehr an. Pakete von Drittanbietern können mit dem Rest des Systems in Konflikt geraten, und ein OS-Update könnte sie zerstören.</p>
<p>Wir haben uns deshalb entschieden, sie <strong>in einem Docker-Container zu isolieren</strong>. Der Host bleibt sauber und aktuell, die Datenbankversion ist festgelegt und reproduzierbar. Daten und Konfiguration liegen in einem Ordner, der sich leicht sichern und verschieben lässt.</p>
<p>Dieser Artikel erzählt, wie wir sie eingerichtet haben und vor allem, auf welche Hindernisse wir unterwegs gestoßen sind. Es sind die unbeabsichtigte Freigabe des Ports 3306 und ein iptables-Fehler beim Neustart. Dazu kam das Schreiben einer Firewall mit <strong>nftables</strong>, die nur autorisierte IPs durchlässt.</p>
<blockquote class="wp-block-quote">
<p><strong>Hinweis:</strong> MySQL 5.7 hat im Oktober 2023 das Ende des Supports (EOL) erreicht und erhält keine Sicherheitspatches mehr. Es ist eine Übergangslösung: Das Ziel bleibt die Migration auf MySQL 8.x oder MariaDB. Gerade deshalb ist es umso wichtiger, es nicht offen zu lassen.</p>
</blockquote>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">1. Ordnerstruktur</h2>
<pre class="wp-block-code"><code>/opt/mysql57/
├── docker-compose.yml
├── .env
├── conf.d/        # configurazioni personalizzate (my.cnf aggiuntivi)
├── data/          # dati del database (volume persistente)
└── logs/
</code></pre>
<p>Die Datei <code>.env</code> enthält die Zugangsdaten, damit sie nicht im Compose landen.</p>
<pre class="wp-block-code"><code>TZ=Europe/Rome
MYSQL_ROOT_PASSWORD=una_password_molto_lunga_e_casuale
MYSQL_DATABASE=nome_db
MYSQL_USER=nome_utente
MYSQL_PASSWORD=altra_password_robusta
</code></pre>
<p>Schützen Sie sie mit <code>chmod 600 .env</code>.</p>
<h2 class="wp-block-heading">2. Die docker-compose.yml</h2>
<pre class="wp-block-code"><code>services:
  mysql57:
    image: mysql:5.7
    container_name: mysql57
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "3306:3306"
    environment:
      TZ: "${TZ:-Europe/Rome}"
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
      MYSQL_DATABASE: "${MYSQL_DATABASE}"
      MYSQL_USER: "${MYSQL_USER}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
    command:
      - --default-authentication-plugin=mysql_native_password
    volumes:
      - ./data:/var/lib/mysql
      - ./conf.d:/etc/mysql/conf.d:ro
      - ./logs:/var/log/mysql
    healthcheck:
      test: &#91;"CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 20s
      timeout: 5s
      retries: 10
      start_period: 45s
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
</code></pre>
<p>Einige Entscheidungen, die man beachten sollte.</p>
<ul class="wp-block-list">
<li><strong><code>restart: unless-stopped</code></strong>: Die Datenbank startet nach einem Neustart des Servers von selbst wieder.</li>
<li><strong>Volumes als Bind-Mount</strong>: Daten, Konfiguration und Logs liegen auf dem Host, Backups und Migrationen sind also einfache Ordnerkopien.</li>
<li><strong>Healthcheck</strong>: erlaubt anderen Diensten zu warten, bis MySQL wirklich bereit ist (<code>$$</code> sorgt dafür, dass die Variable im Container und nicht von Compose aufgelöst wird).</li>
<li><strong><code>ulimits nofile</code></strong>: vermeidet den Fehler „too many open files“ unter Last.</li>
</ul>
<h2 class="wp-block-heading">3. Erstes Problem: Der Port von MySQL 5.7 in Docker ist für die ganze Welt offen</h2>
<p>Die Zeile <code>- "3306:3306"</code> entspricht <code>0.0.0.0:3306:3306</code>: Docker veröffentlicht den Port auf <strong>allen Schnittstellen</strong> des Hosts, auch der öffentlichen.</p>
<p>Das Heimtückischste ist, dass Docker seine eigenen Regeln direkt in die Firewall des Kernels schreibt und diese vor denen von UFW oder firewalld ausgewertet werden. Eine aktive Firewall <strong>genügt nicht</strong>: Der Port bleibt erreichbar. Die Docker-Dokumentation beschreibt dieses Verhalten unter <a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener">Packet filtering and firewalls</a>.</p>
<p>Die möglichen Wege sind.</p>
<ol class="wp-block-list">
<li><strong>Nur localhost</strong>: <code>"127.0.0.1:3306:3306"</code>, wenn die Datenbank nur vom Host oder über einen SSH-Tunnel genutzt wird.</li>
<li><strong>Kein veröffentlichter Port</strong>: Sind die Clients andere Container, genügt ein gemeinsames Docker-Netzwerk.</li>
<li><strong>Bestimmte IP</strong>: nur auf einer Adresse eines privaten Netzes/VPN veröffentlichen.</li>
<li><strong>Öffentlicher Port mit IP-Filter</strong>: unser Fall, weil sich ein externer Client (eine phpMyAdmin-Instanz auf einem anderen Server) verbinden können muss.</li>
</ol>
<p>Für Fall 4 braucht man eine Firewall, die den Verkehr der von Docker veröffentlichten Ports korrekt filtert.</p>
<h2 class="wp-block-heading">4. Zweites Problem: iptables-Fehler beim Start</h2>
<p>Nachdem wir die Firewall angefasst hatten, lieferte <code>docker compose up</code>.</p>
<pre class="wp-block-code"><code>failed to create network mysql57_default: Error response from daemon:
Failed to Setup IP tables: Unable to enable ACCEPT OUTGOING rule:
(iptables failed: iptables --wait -t filter -A DOCKER-FORWARD -i br-... -j ACCEPT:
iptables: No chain/target/match by that name.
</code></pre>
<p>Der Fehler hängt nicht vom Compose ab: Die Kette <code>DOCKER-FORWARD</code> (in neueren Docker-Versionen eingeführt) existierte nicht mehr. Sie war von etwas gelöscht worden, das die Regeln <strong>nach</strong> dem Start des Daemons bereinigt hatte. Ein Neustart von Docker (<code>systemctl restart docker</code>) legt sie neu an, doch das Problem kehrt zurück, bis der wahre Schuldige beseitigt ist.</p>
<h3 class="wp-block-heading">Der Schuldige: <code>flush ruleset</code></h3>
<p>Unsere <code>/etc/nftables.conf</code> begann mit.</p>
<pre class="wp-block-code"><code>flush ruleset
</code></pre>
<p>Dieser Befehl löscht <strong>alle</strong> nftables-Tabellen, auch die von Docker angelegten. Außerdem enthielt die Datei eine statische Kopie der Docker-Regeln, mit von Hand eingetragenem Bridge-Namen (<code>br-278550235c3f</code>) und Container-IP (<code>172.18.0.2</code>). Der Bridge-Name ändert sich jedoch jedes Mal, wenn das Compose-Netzwerk neu erstellt wird. Die Kopie war also sofort veraltet, und die Tabelle <code>ip nat</code> geriet in Konflikt mit der von Docker verwalteten.</p>
<p><strong>Goldene Regel:</strong> Mit Docker verwendet man in der nftables-Datei kein <code>flush ruleset</code> und kopiert keine seiner Regeln. Man verwaltet nur seine eigene Firewall, in eigenen Tabellen.</p>
<h2 class="wp-block-heading">5. Die Firewall mit nftables für MySQL 5.7 in Docker</h2>
<p>Das Prinzip: Die von Docker veröffentlichten Ports werden mit DNAT umgeschrieben und dann vom Host an den Container weitergeleitet, durchlaufen also die Kette <strong><code>forward</code></strong>, nicht <code>input</code>. Eine Regel <code>drop</code> in einer eigenen Kette <code>forward</code> mit etwas niedrigerer Priorität (also zuerst ausgewertet) verwirft das Paket unabhängig von den <code>accept</code>-Regeln von Docker.</p>
<p>Um fremde Tabellen nicht anzufassen, verwenden wir das Muster <code>table ...</code> + <code>delete table ...</code>: Es macht die Datei ohne Fehler und ohne <code>flush</code> wiederholt ausführbar.</p>
<p><code>/etc/nftables.conf</code>.</p>
<pre class="wp-block-code"><code>#!/usr/sbin/nft -f

# NIENTE "flush ruleset": cancellerebbe anche le tabelle di Docker

# ---------------- firewall host ----------------
table inet filter
delete table inet filter

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        iifname "lo" accept
        ip6 nexthdr icmpv6 accept
        iifname "enp7s0" accept                       # rete privata
        ip saddr 203.0.113.20 tcp dport 22 accept     # SSH da host fidato
        ip saddr 203.0.113.30 tcp dport 22 accept     # SSH da VPN
        ip saddr 203.0.113.20 icmp type echo-request accept
        tcp dport { 80, 443 } accept
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

# ---------------- guardia MySQL (porta pubblicata da Docker) ----------------
table ip mysql_guard
delete table ip mysql_guard

table ip mysql_guard {
    set allowed {
        type ipv4_addr
        flags interval
        elements = {
            203.0.113.10        # phpMyAdmin
        }
    }

    chain forward {
        type filter hook forward priority filter - 5; policy accept;
        iifname "enp7s0" accept
        ct status dnat tcp dport 3306 ip saddr @allowed accept
        ct status dnat tcp dport 3306 counter drop
    }
}
</code></pre>
<p>Wie die Schutzregel funktioniert.</p>
<ul class="wp-block-list">
<li><code>ct status dnat</code> wählt nur Pakete aus, die durch DNAT gelaufen sind, also die für die von Docker veröffentlichten Ports bestimmten.</li>
<li>Die erste Regel akzeptiert, wer im Set <code>allowed</code> ist; die zweite verwirft alle anderen und zählt sie (<code>counter</code>).</li>
<li>Lokaler Verkehr (<code>127.0.0.1</code>) und Verkehr zwischen Containern im selben Netzwerk läuft nicht über das Forward des Routers und wird daher nicht berührt.</li>
</ul>
<blockquote class="wp-block-quote">
<p><strong>Kompatibilität:</strong> In einer ersten Version verwendeten wir <code>ct original proto-dst 3306</code>, aber einige Versionen von <code>nft</code> antworten mit <em>„Can&#8217;t parse symbolic invalid expressions“</em>. Im Hook <code>forward</code> ist das DNAT bereits erfolgt und der Container-Port entspricht dem veröffentlichten (<code>3306:3306</code>). Daher ist <code>tcp dport 3306</code> zusammen mit <code>ct status dnat</code> gleichwertig und funktioniert überall. Ändern Sie das Mapping (z. B. <code>3307:3306</code>), muss die Regel an den internen Port angepasst werden.</p>
</blockquote>
<h3 class="wp-block-heading">Änderungen anwenden, ohne sich auszusperren</h3>
<p>Halten Sie eine zweite SSH-Sitzung als Reserve offen, dann.</p>
<pre class="wp-block-code"><code>nft -c -f /etc/nftables.conf     # solo verifica della sintassi
nft delete table ip nat          # una tantum: elimina la vecchia copia statica
nft -f /etc/nftables.conf
systemctl restart docker         # Docker ricrea le sue tabelle pulite
cd /opt/mysql57 &amp;&amp; docker compose up -d
</code></pre>
<p>Damit es beim Booten geladen wird, stellen Sie sicher, dass der Dienst aktiv ist (<code>systemctl enable --now nftables</code>) und dass <code>/etc/nftables.conf</code> kein <code>flush ruleset</code> enthält.</p>
<h3 class="wp-block-heading">IPs zur Laufzeit verwalten</h3>
<pre class="wp-block-code"><code>nft add element ip mysql_guard allowed { 203.0.113.50 }
nft delete element ip mysql_guard allowed { 203.0.113.50 }
</code></pre>
<p>Um es dauerhaft zu machen, aktualisieren Sie auch das Set in der Datei.</p>
<h2 class="wp-block-heading">6. Überprüfung</h2>
<p>Vom autorisierten Server.</p>
<pre class="wp-block-code"><code>nc -zv IP_PUBBLICO_DB 3306        # deve rispondere "succeeded"
</code></pre>
<p>Von jeder anderen IP.</p>
<pre class="wp-block-code"><code>nmap -Pn -p 3306 IP_PUBBLICO_DB   # deve risultare "filtered" / timeout
</code></pre>
<p>Auf dem Server muss der Zähler der Drop-Regel bei jedem nicht autorisierten Versuch wachsen.</p>
<pre class="wp-block-code"><code>nft list chain ip mysql_guard forward
ss -tlnp | grep 3306
</code></pre>
<h2 class="wp-block-heading">7. Zweite Barriere: die MySQL-Benutzer</h2>
<p>Die Firewall ersetzt nicht die Berechtigungen der Datenbank. Legen Sie den Benutzer mit auf die IP des Clients beschränktem Host an.</p>
<pre class="wp-block-code"><code>CREATE USER 'pma_user'@'203.0.113.10' IDENTIFIED BY 'password_robusta';
GRANT ALL ON nome_db.* TO 'pma_user'@'203.0.113.10';
FLUSH PRIVILEGES;
</code></pre>
<p>Gelingt die TCP-Verbindung, MySQL antwortet aber <code>Access denied</code>, liegt das Problem hier und nicht in der Firewall.</p>
<h2 class="wp-block-heading">8. Was man sich zu MySQL 5.7 in Docker merken sollte</h2>
<ul class="wp-block-list">
<li><strong>Docker und die Host-Firewall ignorieren sich nicht</strong>: Zu verstehen, welche Kette der Verkehr durchläuft (<code>input</code> oder <code>forward</code>), ist der Schlüssel zu allem.</li>
<li><strong>Niemals <code>flush ruleset</code></strong> auf einem Host mit Docker; verwenden Sie eigene Tabellen.</li>
<li><strong>Kopieren Sie keine von Docker erzeugten Regeln</strong> in Ihre Dateien: Bridge-Namen und IPs ändern sich.</li>
<li>Haben Sie den Port schon freigegeben, prüfen Sie die Logs in <code>./logs</code> und erwägen Sie, die <strong>Passwörter zu ändern</strong>, vor allem das von root.</li>
<li>Wo möglich, ziehen Sie ein <strong>VPN (WireGuard) oder einen SSH-Tunnel</strong> der öffentlichen Freigabe einer Datenbank vor.</li>
<li>Planen Sie die <strong>Migration</strong> von MySQL 5.7 auf eine unterstützte Version.</li>
</ul>
<p>Haben Sie Fragen oder eine andere Lösung für dasselbe Problem? Schreiben Sie es in die Kommentare. Zum Thema MySQL und Debian lesen Sie auch: <a href="https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/">MySQL startet nach dem Debian-12-Upgrade nicht mehr</a>.</p><p>The post <a href="https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/">MySQL 5.7 in Docker, geschützt mit nftables</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL startet nach dem Debian-12-Upgrade nicht mehr: Ursache und Lösung</title>
		<link>https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/</link>
					<comments>https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 07:50:06 +0000</pubDate>
				<category><![CDATA[Server und Datenbanken]]></category>
		<category><![CDATA[Debian 12]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Update]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=267</guid>

					<description><![CDATA[<p>Nach dem Upgrade auf Debian 12 startet MySQL nicht mehr, und in den Logs taucht mariadb.conf.d auf. So erkennen Sie die Ursache und beheben sie, ohne die Daten anzufassen.</p>
<p>The post <a href="https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/">MySQL startet nach dem Debian-12-Upgrade nicht mehr: Ursache und Lösung</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">Das Symptom: MySQL startet nicht mehr (mysql.service failed)</h2>
<p>MySQL startet nicht mehr: Der Dienst ist fehlgeschlagen, und das Log meldet den Fehler <code>Can't read dir of '/etc/mysql/mariadb.conf.d/'</code>.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-startet-nicht-debian-12-update.webp" alt="MySQL startet nicht mehr: ein Zylinder als Sinnbild einer Datenbank mit einem orangefarbenen Warnkreis daneben" width="1200" height="630"/></figure>
<pre class="wp-block-code"><code># service mysql status
× mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; preset: enabled)
     Active: failed (Result: exit-code)
     Status: "Server startup in progress"

mysqld: Can't read dir of '/etc/mysql/mariadb.conf.d/' (OS errno 2 - No such file or directory)
mysqld: &#91;ERROR] Stopped processing the 'includedir' directive in file /etc/mysql/my.cnf at line 29.
mysqld: &#91;ERROR] Fatal error in defaults handling. Program aborted!
systemd&#91;1]: Failed to start mysql.service - MySQL Community Server.</code></pre>
<p>Beim Prüfen der installierten Pakete ist MySQL ganz normal vorhanden (<code>mysql-community-server</code>-8.0-Pakete aus dem offiziellen Oracle-Repository).</p>
<pre class="wp-block-code"><code>dpkg -l | grep mysql</code></pre>
<h2 class="wp-block-heading">Die Ursache: Die Alternative my.cnf zeigt auf mariadb.cnf</h2>
<p>Unter Debian ist die Datei <code>/etc/mysql/my.cnf</code> keine echte Datei, sondern ein Link, der von <strong>update-alternatives</strong> verwaltet wird. Mehrere Pakete können ihre eigene Version registrieren, und im automatischen Modus gewinnt die mit der höchsten Priorität.</p>
<p>Beim Upgrade wurde das Paket <strong>mariadb-common</strong> installiert (und danach entfernt), das <code>/etc/mysql/mariadb.cnf</code> mit <strong>Priorität 500</strong> registriert, höher als <code>/etc/mysql/mysql.cnf</code> (Priorität 300). Das Paket wurde entfernt, aber nicht vollständig gelöscht (Status <code>rc</code>), sodass seine Konfiguration aktiv blieb. In Zeile 29 dieser Datei steht das Include von <code>/etc/mysql/mariadb.conf.d/</code>, einem Verzeichnis, das nicht existiert: MySQL liest die Konfiguration, findet das Verzeichnis nicht und bricht ab. Das Ergebnis: „MySQL startet nicht mehr“, obwohl die Pakete in Ordnung sind.</p>
<p><strong>Die gute Nachricht:</strong> Die Daten in <code>/var/lib/mysql</code> wurden nicht angetastet. Es ist nur ein Konfigurationsproblem.</p>
<h2 class="wp-block-heading">Diagnose: prüfen, worauf my.cnf zeigt</h2>
<pre class="wp-block-code"><code>ls -l /etc/mysql/my.cnf /etc/alternatives/my.cnf
update-alternatives --display my.cnf
sed -n '25,32p' /etc/mysql/my.cnf
dpkg -l | grep -i mariadb</code></pre>
<p>Wenn das Problem hier liegt, sieht die Ausgabe ähnlich aus wie diese.</p>
<pre class="wp-block-code"><code>/etc/alternatives/my.cnf -&gt; /etc/mysql/mariadb.cnf
/etc/mysql/my.cnf -&gt; /etc/alternatives/my.cnf

my.cnf - modalità automatica
  la versione migliore del collegamento è /etc/mysql/mariadb.cnf
  il collegamento attualmente punta a /etc/mysql/mariadb.cnf
/etc/mysql/mariadb.cnf - priorità 500
/etc/mysql/my.cnf.fallback - priorità 100
/etc/mysql/mysql.cnf - priorità 300

!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

rc  mariadb-common   1:10.11.18-0+deb12u1   all   MariaDB common configuration files</code></pre>
<h2 class="wp-block-heading">Lösung Schritt für Schritt</h2>
<h3 class="wp-block-heading">1. Die Konfiguration sichern</h3>
<pre class="wp-block-code"><code>cp -a /etc/mysql /root/etc-mysql-backup-$(date +%F)</code></pre>
<h3 class="wp-block-heading">2. Die Reste von mariadb-common entfernen</h3>
<p>Das Purge entfernt <code>mariadb.cnf</code> und den zugehörigen Eintrag in update-alternatives.</p>
<pre class="wp-block-code"><code>apt purge mariadb-common</code></pre>
<h3 class="wp-block-heading">3. my.cnf wieder auf mysql.cnf zeigen lassen</h3>
<pre class="wp-block-code"><code>update-alternatives --remove my.cnf /etc/mysql/mariadb.cnf 2&gt;/dev/null
update-alternatives --set my.cnf /etc/mysql/mysql.cnf</code></pre>
<h3 class="wp-block-heading">4. Die Konfiguration prüfen</h3>
<pre class="wp-block-code"><code>update-alternatives --display my.cnf
cat /etc/mysql/my.cnf
my_print_defaults mysqld</code></pre>
<p>Jetzt muss <code>my.cnf</code> auf <code>/etc/mysql/mysql.cnf</code> zeigen und <code>/etc/mysql/conf.d/</code> sowie <code>/etc/mysql/mysql.conf.d/</code> einbinden. Gibt <code>my_print_defaults mysqld</code> die Parameter ohne Fehler aus, ist die Konfiguration korrekt.</p>
<h3 class="wp-block-heading">5. MySQL starten</h3>
<pre class="wp-block-code"><code>systemctl start mysql
systemctl status mysql
journalctl -u mysql -n 50 --no-pager</code></pre>
<h2 class="wp-block-heading">Wenn my.cnf kein symbolischer Link ist</h2>
<p>Ist <code>/etc/mysql/my.cnf</code> eine normale Datei oder existiert <code>/etc/mysql/mysql.cnf</code> nicht, können Sie die Konfiguration von Hand neu anlegen.</p>
<pre class="wp-block-code"><code>mv /etc/mysql/my.cnf /etc/mysql/my.cnf.mariadb.bak
cat &gt; /etc/mysql/my.cnf &lt;&lt;'EOF'
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
EOF
mkdir -p /etc/mysql/conf.d
systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Schnelle vorläufige Lösung für „MySQL startet nicht mehr“</h2>
<p>Muss die Datenbank sofort wieder online, genügt es, das fehlende Verzeichnis anzulegen. Das ist aber nur ein Provisorium: MySQL würde weiterhin die für MariaDB gedachte Konfiguration lesen, daher wenden Sie die vollständige Lösung besser so bald wie möglich an.</p>
<pre class="wp-block-code"><code>mkdir /etc/mysql/mariadb.conf.d &amp;&amp; systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Wie Sie verhindern, dass es wieder passiert</h2>
<p>Es lohnt sich herauszufinden, welches Upgrade MariaDB installiert hat, anhand des apt-Verlaufs. Wie wir MySQL 5.7 in Docker betreiben, ohne es dem Internet auszusetzen, steht in <a href="https://www.cisilino.com/de/2026/10/17/mysql-5-7-docker-nftables-schutz/">diesem Artikel</a>. Was sich in Debian 12 geändert hat, erklären die <a href="https://www.debian.org/releases/bookworm/amd64/release-notes/" target="_blank" rel="noopener">Release Notes zu Debian 12</a>.</p>
<pre class="wp-block-code"><code>grep -i -B3 -A3 mariadb /var/log/apt/history.log*</code></pre>
<p>Damit <code>mariadb-common</code> künftig nicht erneut installiert wird, kann man es mit einem APT-Pin mit negativer Priorität sperren.</p>
<pre class="wp-block-code"><code>cat &gt; /etc/apt/preferences.d/no-mariadb &lt;&lt;'EOF'
Package: mariadb-*
Pin: release *
Pin-Priority: -1
EOF</code></pre>
<p>Nach jedem größeren Upgrade ist es außerdem gute Praxis, den Status der Alternative mit <code>update-alternatives --display my.cnf</code> zu prüfen.</p>
<h2 class="wp-block-heading">Häufig gestellte Fragen</h2>
<h3 class="wp-block-heading">MySQL startet nicht mehr: Verliere ich die Daten meiner Datenbanken?</h3>
<p>Nein. Das Problem betrifft nur die Konfigurationsdateien in <code>/etc/mysql</code>; die Daten in <code>/var/lib/mysql</code> bleiben unversehrt.</p>
<h3 class="wp-block-heading">Warum hat Debian mariadb.cnf gewählt?</h3>
<p>Weil die Alternative <code>my.cnf</code> im automatischen Modus war und <code>mariadb.cnf</code> Priorität 500 hat, höher als die 300 von <code>mysql.cnf</code>. Mit <code>update-alternatives --set</code> wird die Auswahl manuell und ändert sich nicht mehr von selbst.</p>
<h3 class="wp-block-heading">Gilt das auch für Ubuntu?</h3>
<p>Ja, der Mechanismus von <code>update-alternatives</code> für <code>my.cnf</code> ist unter Ubuntu und den anderen von Debian abgeleiteten Distributionen derselbe.</p>
<h2 class="wp-block-heading">Fazit</h2>
<p>Der Fehler <strong>„Can&#8217;t read dir of /etc/mysql/mariadb.conf.d/“</strong> nach einem Debian-Upgrade bedeutet keinen Schaden an MySQL, sondern eine MariaDB-Restkonfiguration, die über update-alternatives aktiv geblieben ist. Wenn Sie <code>mariadb-common</code> mit <code>apt purge</code> entfernen und <code>my.cnf</code> wieder auf <code>mysql.cnf</code> setzen, läuft MySQL wieder normal.</p></p><p>The post <a href="https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/">MySQL startet nach dem Debian-12-Upgrade nicht mehr: Ursache und Lösung</a> first appeared on <a href="https://www.cisilino.com/de/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/de/2026/10/13/mysql-startet-nicht-debian-12/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
