<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hécate R.</title>
    <description>The latest articles on DEV Community by Hécate R. (@hecate-regnier).</description>
    <link>https://dev.to/hecate-regnier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1743124%2F09e1adb2-1ae4-4547-870b-71e5f754d459.jpg</url>
      <title>DEV Community: Hécate R.</title>
      <link>https://dev.to/hecate-regnier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hecate-regnier"/>
    <language>en</language>
    <item>
      <title>Sharing an article about databases files in MariaDB</title>
      <dc:creator>Hécate R.</dc:creator>
      <pubDate>Thu, 05 Nov 2026 09:50:12 +0000</pubDate>
      <link>https://dev.to/hecate-regnier/sharing-an-article-about-databases-files-in-mariadb-50fd</link>
      <guid>https://dev.to/hecate-regnier/sharing-an-article-about-databases-files-in-mariadb-50fd</guid>
      <description>&lt;p&gt;&lt;a href="https://hecate.neurozone.fr/2026/10/23/inside-database-data-files-1-3-innodb-from-mysql-to-mariadb/" rel="noopener noreferrer"&gt;https://hecate.neurozone.fr/2026/10/23/inside-database-data-files-1-3-innodb-from-mysql-to-mariadb/&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I hope I can have some feedback !&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How-to: the MariaDB settings that really matter, and how to check them</title>
      <dc:creator>Hécate R.</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/hecate-regnier/how-to-the-mariadb-settings-that-really-matter-and-how-to-check-them-5h25</link>
      <guid>https://dev.to/hecate-regnier/how-to-the-mariadb-settings-that-really-matter-and-how-to-check-them-5h25</guid>
      <description>&lt;p&gt;The Internet is full of hundred-line &lt;code&gt;my.cnf&lt;/code&gt; files, copied from server to server, where nobody remembers why each line is there. Most of MariaDB’s defaults are sensible; only about ten MariaDB settings make a real difference. This tutorial goes through them using a real server: MariaDB 12.3 on Rocky Linux, about thirty sites (WordPress, Matomo, Piwigo…), 32 GB of RAM shared with PHP, nginx and other services, spinning disks.&lt;/p&gt;

&lt;p&gt;For each MariaDB setting: what it does, how to choose the value, and above all how to &lt;strong&gt;check&lt;/strong&gt; it’s right. A MariaDB setting you can’t measure is a setting you shouldn’t touch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hecate.neurozone.fr/wp-content/uploads/2026/11/how-to-the-mariadb-settings-that-really-matter_diagram_en.webp" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fupy74r5gaq217dokx129.webp" alt="Eight MariaDB settings with how to choose each and how to check it, plus a real check after a restart" width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Eight settings, how to choose them, and how to measure each one.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the configuration lives, and how to read the real value
&lt;/h2&gt;

&lt;p&gt;MariaDB reads &lt;code&gt;/etc/my.cnf&lt;/code&gt; then the files in &lt;code&gt;/etc/my.cnf.d/&lt;/code&gt;. MariaDB settings go in a &lt;code&gt;[mariadb]&lt;/code&gt; section (read only by MariaDB) or &lt;code&gt;[mysqld]&lt;/code&gt; (also read by MySQL-compatible tools). The classic trap: the same variable defined in two files, the last one read wins. Two commands tell the truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mariadbd &lt;span class="nt"&gt;--print-defaults&lt;/span&gt; &lt;span class="c"&gt;# what the server will read at next start&lt;/span&gt;
mariadb &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool_size'"&lt;/span&gt; &lt;span class="c"&gt;# the current value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A misspelt variable in the file is ignored with a mere warning in the error log. Always compare the current value with the one you think you set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mariadb]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Memory -------------------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;innodb_buffer_pool_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;12G&lt;/span&gt;
&lt;span class="py"&gt;innodb_log_file_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;2G&lt;/span&gt;
&lt;span class="py"&gt;innodb_log_buffer_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;span class="py"&gt;tmp_table_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;span class="py"&gt;max_heap_table_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Durability ----------------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;innodb_flush_log_at_trx_commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;sync_binlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;innodb_flush_method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;O_DIRECT&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- I/O (spinning disks) ----------------------------------
&lt;/span&gt;&lt;span class="py"&gt;innodb_io_capacity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;innodb_io_capacity_max&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;400&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Connexions ----------------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;max_connections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;400&lt;/span&gt;
&lt;span class="py"&gt;thread_cache_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;64&lt;/span&gt;
&lt;span class="py"&gt;wait_timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;28800&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Tables et fichiers --------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;table_open_cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;4096&lt;/span&gt;
&lt;span class="py"&gt;table_open_cache_instances&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;
&lt;span class="py"&gt;table_definition_cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;4096&lt;/span&gt;
&lt;span class="py"&gt;open_files_limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;68000&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Binlogs, replication ------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;server_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;log_bin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/data/mysql/binlogs/logbin&lt;/span&gt;
&lt;span class="py"&gt;binlog_format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ROW&lt;/span&gt;
&lt;span class="py"&gt;binlog_expire_logs_seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;432000&lt;/span&gt;
&lt;span class="py"&gt;max_binlog_size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;512M&lt;/span&gt;
&lt;span class="py"&gt;log_slave_updates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ON&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# --- Hygiene -------------------------------------------------------------
&lt;/span&gt;&lt;span class="py"&gt;character_set_server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;utf8mb4&lt;/span&gt;
&lt;span class="py"&gt;collation_server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;utf8mb4_uca1400_ai_ci&lt;/span&gt;
&lt;span class="py"&gt;transaction_isolation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;READ-COMMITTED&lt;/span&gt;
&lt;span class="py"&gt;skip_name_resolve&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ON&lt;/span&gt;
&lt;span class="py"&gt;local_infile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;OFF&lt;/span&gt;
&lt;span class="py"&gt;max_allowed_packet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;
&lt;span class="py"&gt;slow_query_log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ON&lt;/span&gt;
&lt;span class="py"&gt;long_query_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;log_slow_verbosity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;query_plan,explain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Memory
&lt;/h2&gt;

&lt;h3&gt;
  
  
  innodb_buffer_pool_size, the setting that matters most
&lt;/h3&gt;

&lt;p&gt;It’s the cache where InnoDB keeps table and index pages. As long as the data actually used fits, reads don’t touch the disk, which is often a factor of a hundred in response time. The usual rule: 60 to 75% of RAM on a server dedicated to the database, 30 to 40% on a server that also runs the application and the web server. Here, 12 GB out of 32.&lt;/p&gt;

&lt;p&gt;To know whether it’s enough, measure the share of reads served by the cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hit_ratio_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_VALUE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GLOBAL_STATUS&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Innodb_buffer_pool_reads'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_VALUE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GLOBAL_STATUS&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Innodb_buffer_pool_read_requests'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above 99% over a normal day, memory isn’t your problem. Below 95%, every query that touches cold data reads the disk: grow the cache if RAM allows, or look for queries that scan whole tables. Since MariaDB 10.5 there’s no need to split the cache into &lt;code&gt;innodb_buffer_pool_instances&lt;/code&gt;: it’s handled internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  innodb_log_file_size: sizing the redo log
&lt;/h3&gt;

&lt;p&gt;Every change is first written to the redo log, then the modified pages are written to the data files in the background. A redo log that’s too small forces InnoDB to flush pages in a hurry when it fills up, which causes latency spikes. A good size covers about an hour of writes. Measure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_VALUE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GLOBAL_STATUS&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Innodb_lsn_current'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;SLEEP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_VALUE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GLOBAL_STATUS&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;VARIABLE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Innodb_lsn_current'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;redo_mb_per_hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the result is 400 MB per hour, a 512 MB to 1 GB log is enough; here 2 GB leaves room for imports. A bigger log only lengthens crash recovery, from seconds to a few minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-connection memory: the multiplication trap
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sort_buffer_size&lt;/code&gt;, &lt;code&gt;join_buffer_size&lt;/code&gt;, &lt;code&gt;read_buffer_size&lt;/code&gt;, &lt;code&gt;read_rnd_buffer_size&lt;/code&gt; are allocated &lt;strong&gt;per connection&lt;/strong&gt; , sometimes several times per query. A 64 MB &lt;code&gt;join_buffer_size&lt;/code&gt; looks harmless, but multiplied by 400 connections it could in theory claim 25 GB. Keep the defaults unless a measurement says otherwise, and raise them only for one session (&lt;code&gt;SET SESSION&lt;/code&gt;) during a heavy job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tmp_table_size&lt;/code&gt; and &lt;code&gt;max_heap_table_size&lt;/code&gt; go together: the smaller of the two sets the maximum size of an in-memory temporary table before it moves to disk. Watch the ratio of &lt;code&gt;Created_tmp_disk_tables&lt;/code&gt; to &lt;code&gt;Created_tmp_tables&lt;/code&gt;: above 10 to 20%, some queries sort or group large volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durability
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;innodb_flush_log_at_trx_commit = 1&lt;/code&gt; writes and syncs the redo log at every commit; &lt;code&gt;sync_binlog = 1&lt;/code&gt; does the same for the binlog. Together they guarantee that a power cut loses no committed transaction and that the replica stays consistent with the primary. Values 2 or 0 reduce synchronous writes, at the cost of up to one second of lost transactions if the system crashes: acceptable on a replica you can rebuild, never on the primary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;innodb_flush_method = O_DIRECT&lt;/code&gt; makes InnoDB write straight to disk without going through the system cache: data isn’t cached twice (in InnoDB’s cache and in the kernel’s). It’s the default in recent versions; check it rather than assume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I/O
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;innodb_io_capacity&lt;/code&gt; tells InnoDB how many operations per second the storage can absorb for background work (page flushing, purge). The default suits SSDs. On spinning disks, 200 keeps InnoDB from saturating the disk at the expense of queries; &lt;code&gt;innodb_io_capacity_max&lt;/code&gt; sets the ceiling when it falls behind. To check: &lt;code&gt;iostat -x 5&lt;/code&gt; during a busy period; the &lt;code&gt;%util&lt;/code&gt; column must not stay stuck at 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connections
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;max_connections&lt;/code&gt; should cover the sum of connections the applications can open, with a margin, no more: each connection uses memory, and raising this limit to “fix” &lt;em&gt;Too many connections&lt;/em&gt; errors often hides an application that doesn’t close its connections. Look at the real peak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Max_used_connections'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Aborted_c%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Threads_created'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;thread_cache_size&lt;/code&gt; keeps threads ready for reuse: if &lt;code&gt;Threads_created&lt;/code&gt; grows quickly, raise it. &lt;code&gt;wait_timeout&lt;/code&gt; closes idle connections; 8 hours suits web applications, which reconnect on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tables and files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;table_open_cache&lt;/code&gt; keeps tables open between queries; it’s split into &lt;code&gt;table_open_cache_instances&lt;/code&gt; to limit contention between threads. If &lt;code&gt;Opened_tables&lt;/code&gt; keeps growing, the cache is too small.&lt;/p&gt;

&lt;p&gt;The trap that cost me time: this cache depends on how many files the process may open. MariaDB computes its limits from &lt;code&gt;open_files_limit&lt;/code&gt;, but systemd imposes its own (&lt;code&gt;LimitNOFILE&lt;/code&gt;). If systemd’s is lower, MariaDB &lt;strong&gt;silently&lt;/strong&gt; shrinks its caches at startup: I found a server set to 8 instances that was only using 3. The fix is a systemd override file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/mariadb.service.d/limits.conf&lt;/span&gt;
&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;68000

&lt;span class="err"&gt;systemctl&lt;/span&gt; &lt;span class="err"&gt;daemon-reload&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;systemctl&lt;/span&gt; &lt;span class="err"&gt;restart&lt;/span&gt; &lt;span class="err"&gt;mariadb&lt;/span&gt;
&lt;span class="err"&gt;mariadb&lt;/span&gt; &lt;span class="err"&gt;-e&lt;/span&gt; &lt;span class="err"&gt;"SHOW&lt;/span&gt; &lt;span class="err"&gt;GLOBAL&lt;/span&gt; &lt;span class="err"&gt;VARIABLES&lt;/span&gt; &lt;span class="err"&gt;WHERE&lt;/span&gt; &lt;span class="err"&gt;Variable_name&lt;/span&gt; &lt;span class="err"&gt;IN&lt;/span&gt; &lt;span class="err"&gt;('open_files_limit','table_open_cache_instances')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Binlogs and replication
&lt;/h2&gt;

&lt;p&gt;The binlog records every change; it serves replication and point-in-time recovery. &lt;code&gt;binlog_format = ROW&lt;/code&gt; records the changed rows rather than the statements: bigger, but unambiguous (a statement with &lt;code&gt;NOW()&lt;/code&gt; or &lt;code&gt;RAND()&lt;/code&gt; doesn’t give the same result replayed elsewhere). &lt;code&gt;binlog_expire_logs_seconds&lt;/code&gt; purges old files: keep at least the interval between two full backups, or point-in-time recovery becomes impossible for the period in between. &lt;code&gt;server_id&lt;/code&gt; must be unique across the whole replication topology, and &lt;code&gt;log_slave_updates&lt;/code&gt; lets a replica act as a source itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hygiene
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;utf8mb4&lt;/strong&gt; : real 4-byte UTF-8, emoji included. The historical &lt;code&gt;utf8&lt;/code&gt; set is limited to 3 bytes and truncates some characters;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;collation_server&lt;/strong&gt; : recent versions offer UCA 14.0 collations (&lt;code&gt;utf8mb4_uca1400_ai_ci&lt;/code&gt;), more accurate for comparisons and sorting;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;READ-COMMITTED&lt;/strong&gt; : fewer locks than the default REPEATABLE-READ, a good fit for web applications;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;skip_name_resolve&lt;/strong&gt; : no DNS lookup on every connection (slow DNS would mean slow connections), and grants are given by IP address;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;local_infile = OFF&lt;/strong&gt; : closes a door that lets a client make the server read files;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;max_allowed_packet&lt;/strong&gt; : the maximum size of a query or a row; too small, and imports and large contents fail;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the slow query log&lt;/strong&gt; : beyond 5 seconds, with the execution plan. Read it every week: it’s the best optimisation source there is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The method for any MariaDB setting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;change &lt;strong&gt;one&lt;/strong&gt; setting at a time, and note the measurement before and after;&lt;/li&gt;
&lt;li&gt;many variables can be changed live (&lt;code&gt;SET GLOBAL&lt;/code&gt;) for a trial; then write them to the file, or they vanish at restart;&lt;/li&gt;
&lt;li&gt;those that need a restart (redo log size, &lt;code&gt;open_files_limit&lt;/code&gt;) get scheduled;&lt;/li&gt;
&lt;li&gt;keep the configuration under version control, with the reason for each non-default value as a comment. In six months, that comment will be worth more than the value itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checks after a change
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart mariadb &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl is-active mariadb
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; mariadb &lt;span class="nt"&gt;-n&lt;/span&gt; 30 &lt;span class="nt"&gt;--no-pager&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'warn|error'&lt;/span&gt;
mariadb &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW GLOBAL VARIABLES"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/vars.after
diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sort&lt;/span&gt; /tmp/vars.before&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sort&lt;/span&gt; /tmp/vars.after&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a Prometheus exporter watches the database, compare over a few days the cache hit ratio, on-disk temporary tables, connections and query latency. A MariaDB setting is only good if those curves confirm it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real check, 75 minutes after a restart
&lt;/h2&gt;

&lt;p&gt;To show what these checks give on a real server, here is my web server’s MariaDB 12.3 on 2 November 2026, read with the monitoring account (which only needs to read variables and counters). The machine had rebooted 75 minutes earlier, which makes the numbers more instructive, not less.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;innodb_buffer_pool_size&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;buffer_pool_gb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;innodb_log_file_size&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;redo_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;max_connections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;innodb_flush_log_at_trx_commit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;sync_binlog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------+---------+-------------------+---------------------------------+---------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;buffer_pool_gb&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;redo_mb&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;max_connections&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;innodb_flush_log_at_trx_commit&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;sync_binlog&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------+---------+-------------------+---------------------------------+---------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;----------------+---------+-------------------+---------------------------------+---------------+&lt;/span&gt;

&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="k"&gt;GLOBAL&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Variable_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Uptime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4487&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Innodb_buffer_pool_read_requests&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;5163358&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Innodb_buffer_pool_reads&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;121600&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Innodb_log_waits&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Max_used_connections&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Threads_created&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Connections&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;11846&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Created_tmp_tables&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;37637&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Created_tmp_disk_tables&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;5223&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How I read it, line by line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buffer pool hit ratio&lt;/strong&gt; : 1 − 121 600 / 5 163 358 = &lt;strong&gt;97.6 %&lt;/strong&gt;. On a server that has been up for weeks I expect above 99.9 %. Here the cache is still filling: every page read from disk since the restart counts as a miss. That’s why the ratio must be compared over days, never right after a restart, and why a restart is followed by a slow first hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Innodb_log_waits = 0&lt;/strong&gt; : no transaction had to wait for room in the log buffer. With a 2 GB redo log, even the start-up traffic didn’t saturate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max_used_connections = 61&lt;/strong&gt; out of 400 allowed, and &lt;strong&gt;Threads_created = 61&lt;/strong&gt; for 11 846 connections: the thread cache reuses threads instead of creating one per connection. If Threads_created grew almost as fast as Connections, thread_cache_size would be too small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary tables on disk&lt;/strong&gt; : 5 223 out of 37 637, i.e. 14 %. That’s mostly WordPress queries sorting text columns, which go to disk regardless of tmp_table_size. Above 25 %, it’s worth looking for the queries responsible before raising any limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;innodb_flush_log_at_trx_commit = 1 and sync_binlog = 1&lt;/strong&gt; : full durability on the primary. The replica runs with 2 and 0, the reasons are in the MariaDB series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total time: two queries, one minute. Run them before and after every change, and keep the outputs: that’s what turns “it feels faster” into a measurement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series of sysadmin tutorials (tag “how-to”).&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Series “How-to”, tutorial 6/77. · &lt;a href="https://hecate.neurozone.fr/?p=595" rel="noopener noreferrer"&gt;← Previous tutorial&lt;/a&gt; · &lt;a href="https://hecate.neurozone.fr/?p=1531" rel="noopener noreferrer"&gt;Next tutorial →&lt;/a&gt; · &lt;a href="https://blog.neurozone.fr/?p=2044" rel="noopener noreferrer"&gt;Version française&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=377" rel="noopener noreferrer"&gt;How InnoDB stores data on disk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=53" rel="noopener noreferrer"&gt;MySQL export without lock tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=379" rel="noopener noreferrer"&gt;Oracle and the three engines side by side&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post &lt;a href="https://hecate.neurozone.fr/2026/11/01/how-to-the-mariadb-settings-that-really-matter/" rel="noopener noreferrer"&gt;How-to: the MariaDB settings that really matter, and how to check them&lt;/a&gt; appeared first on &lt;a href="https://hecate.neurozone.fr" rel="noopener noreferrer"&gt;Hécate Regnier - Neurozone&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sysadminnotebook</category>
      <category>english</category>
      <category>howto</category>
    </item>
    <item>
      <title>How-to: install and configure WireGuard between a server and home</title>
      <dc:creator>Hécate R.</dc:creator>
      <pubDate>Sun, 01 Nov 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/hecate-regnier/how-to-install-and-configure-wireguard-between-a-server-and-home-3527</link>
      <guid>https://dev.to/hecate-regnier/how-to-install-and-configure-wireguard-between-a-server-and-home-3527</guid>
      <description>&lt;p&gt;This tutorial links a hosted server (a Rocky Linux VPS) to the home network through a WireGuard tunnel. At the end, the server reaches the home machines, the house reaches the server, and only one UDP port is exposed to the Internet. It’s the basis for many uses: backups sent to a NAS, monitoring a public server from home, access to services you really don’t want to publish.&lt;/p&gt;

&lt;p&gt;Every configuration line is explained, along with the checks to run and the most common failures. The addresses are examples; adapt them to your own addressing plan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hecate.neurozone.fr/wp-content/uploads/2026/11/how-to-install-and-configure-wireguard-on-rocky-linux_diagram_en.webp" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fioek76hd2my1n526qie6.webp" alt="Topology of the WireGuard tunnel between a home router and a VPS, and five common failures with their causes" width="799" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The tunnel, one exposed UDP port, and the five failures you will meet.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why WireGuard
&lt;/h2&gt;

&lt;p&gt;WireGuard fits in a few thousand lines of code in the Linux kernel, against hundreds of thousands for OpenVPN or IPsec. It offers no choice of algorithm: the cryptography is fixed (Curve25519, ChaCha20-Poly1305, BLAKE2s), which removes a whole class of configuration mistakes. It looks stateless: no session to negotiate by hand, a handshake renewed automatically about every two minutes, and a tunnel that survives a peer’s address changing. And it’s silent: a packet not authenticated by a known key gets no answer, so the port looks closed to a scan.&lt;/p&gt;
&lt;h2&gt;
  
  
  Concepts to keep in mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One key pair per end.&lt;/strong&gt; The private key stays on the machine; the public key is its identity for the other side. There’s no user and no password;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peers, not a client and a server.&lt;/strong&gt; Each side declares the other as a peer. What looks like a “server” is simply the peer with a fixed address that waits to be called;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptokey routing.&lt;/strong&gt; The &lt;code&gt;AllowedIPs&lt;/code&gt; directive ties networks to a peer. A packet for those networks leaves encrypted for that peer; a packet received from that peer is accepted only if its source address belongs to those networks;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP only.&lt;/strong&gt; Everything goes through a single UDP port, which crosses ISP boxes’ address translation well.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The addressing plan
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tunnel network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.99.0.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;addresses inside the tunnel, one per end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router (tunnel)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.99.0.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;home side, behind the ISP box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server (tunnel)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.99.0.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the VPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.10.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;what the server must reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server (public)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;203.0.113.25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the VPS’s fixed public address, UDP port 51820&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick a tunnel network that exists nowhere else at home or at the provider: overlapping addresses produce failures that are very hard to understand.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;a Rocky Linux 9 or 10 server with a fixed public IPv4 address and root access;&lt;/li&gt;
&lt;li&gt;a home router that does WireGuard (MikroTik RouterOS 7, OpenWrt, pfSense, OPNsense…), or failing that an always-on Linux machine;&lt;/li&gt;
&lt;li&gt;console access to the server at the provider, in case a firewall mistake cuts SSH.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 1: install the tools
&lt;/h2&gt;

&lt;p&gt;The kernel module ships with Rocky Linux since version 9. Only the userspace tools are missing, &lt;code&gt;wg&lt;/code&gt; and &lt;code&gt;wg-quick&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;wireguard-tools
modprobe wireguard
lsmod | &lt;span class="nb"&gt;grep &lt;/span&gt;wireguard &lt;span class="c"&gt;# the module is loaded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: generate the keys
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;umask &lt;/span&gt;077
&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/wireguard
wg genkey | &lt;span class="nb"&gt;tee &lt;/span&gt;server.key | wg pubkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server.pub
wg genpsk &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; home.psk &lt;span class="c"&gt;# preshared key, optional but recommended&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;server.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;umask 077&lt;/code&gt; ensures the files are created readable by root only, with no window during which another user could read them. The &lt;strong&gt;preshared key&lt;/strong&gt; adds a layer of symmetric encryption on top of the key exchange: it replaces nothing, but protects traffic recorded today against a future quantum computer able to break Curve25519. It must be identical on both sides and travel only over a safe channel.&lt;/p&gt;

&lt;p&gt;Do the same on the router and note its public key. Private keys never appear in a version-controlled file in clear text: put them in a secrets store (Vault, OpenBao, Ansible Vault) if you automate the deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: write /etc/wireguard/wg0.conf
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;10.99.0.3/24&lt;/span&gt;
&lt;span class="py"&gt;ListenPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;51820&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;server private key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;MTU&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;1420&lt;/span&gt;
&lt;span class="py"&gt;SaveConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# home router
&lt;/span&gt;&lt;span class="py"&gt;PublicKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;router public key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;PresharedKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;preshared key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;192.168.10.0/24, 10.99.0.0/24&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The [Interface] section
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Address&lt;/strong&gt; : the server’s address in the tunnel. The &lt;code&gt;/24&lt;/code&gt; mask makes &lt;code&gt;wg-quick&lt;/code&gt; create a route to the whole tunnel network, useful if other servers join the same tunnel later;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ListenPort&lt;/strong&gt; : the UDP listening port. 51820 is the convention; nothing forces you to use it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PrivateKey&lt;/strong&gt; : the content of &lt;code&gt;server.key&lt;/code&gt;. A variant is to leave it out of the file and load it with &lt;code&gt;PostUp = wg set %i private-key /etc/wireguard/server.key&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MTU = 1420&lt;/strong&gt; : the maximum packet size in the tunnel. WireGuard adds 60 bytes of header over IPv4 (80 over IPv6); 1420 leaves room on a standard 1500 Ethernet link. If the home box uses PPPoE (MTU 1492), go down to 1412, or 1380 when in doubt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaveConfig = false&lt;/strong&gt; : keeps &lt;code&gt;wg-quick&lt;/code&gt; from rewriting the file with the current state on shutdown, which would erase your comments and could freeze in a manual change made for a test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The [Peer] section
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PublicKey&lt;/strong&gt; : the router’s identity;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PresharedKey&lt;/strong&gt; : the shared key generated above;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AllowedIPs&lt;/strong&gt; : the most important directive, and the most misunderstood. It’s both a routing table (packets to &lt;code&gt;192.168.10.0/24&lt;/code&gt; and &lt;code&gt;10.99.0.0/24&lt;/code&gt; go to this peer) and a filter (a packet from this peer whose source isn’t in these networks is dropped). Don’t put &lt;code&gt;0.0.0.0/0&lt;/code&gt; here: only traffic for the house goes through the tunnel, the rest of the server’s traffic leaves normally. That’s a split tunnel;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;no Endpoint&lt;/strong&gt; : the server doesn’t know where to reach the house, whose public address may change. The router calls it; the server learns its address at the first handshake and updates it if it changes;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PersistentKeepalive = 25&lt;/strong&gt; : a small packet every 25 seconds. Essential on the router side, because the box forgets an idle UDP translation after 30 to 120 seconds and the server could no longer reach the house. On the server side it’s harmless and keeps the tunnel active both ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: the home side
&lt;/h2&gt;

&lt;p&gt;The router declares the server as a peer, this time with an &lt;code&gt;Endpoint&lt;/code&gt;: the VPS’s public address. Its &lt;code&gt;AllowedIPs&lt;/code&gt; for that peer is just the server’s tunnel address. Example on a MikroTik running RouterOS 7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/interface wireguard add name=wg-servers listen-port=13231 private-key="&amp;lt;router private key&amp;gt;"
/ip address add address=10.99.0.2/24 interface=wg-servers
/interface wireguard peers add interface=wg-servers \
    public-key="&amp;lt;server public key&amp;gt;" preshared-key="&amp;lt;preshared key&amp;gt;" \
    endpoint-address=203.0.113.25 endpoint-port=51820 \
    allowed-address=10.99.0.3/32 persistent-keepalive=25s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router’s firewall must also allow traffic between the WireGuard interface and the LAN (forward chain), placed &lt;strong&gt;before&lt;/strong&gt; the generic drop rules. A common rule that drops private addresses (“bogons”) must apply only to the Internet-facing interface: otherwise it drops the WireGuard tunnel network itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: the server’s firewall
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--add-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;51820/udp
&lt;span class="c"&gt;# attach wg0 to a zone: 'trusted' to allow everything from home,&lt;/span&gt;
&lt;span class="c"&gt;# or a dedicated zone that opens only what's needed (SSH, exporters…)&lt;/span&gt;
firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--new-zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wireguard
firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wireguard &lt;span class="nt"&gt;--add-interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wg0
firewall-cmd &lt;span class="nt"&gt;--permanent&lt;/span&gt; &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;wireguard &lt;span class="nt"&gt;--add-service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ssh
firewall-cmd &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dedicated zone is more careful than &lt;code&gt;trusted&lt;/code&gt;: if a home machine were compromised, it would see only the services you chose on the server. The server doesn’t need to forward packets between networks (&lt;code&gt;net.ipv4.ip_forward&lt;/code&gt; can stay at 0) since it’s only an endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wg-quick&lt;/code&gt; creates the &lt;code&gt;wg0&lt;/code&gt; interface, gives it its address, loads the WireGuard configuration and adds the routes derived from &lt;code&gt;AllowedIPs&lt;/code&gt;. The service comes back at boot. To apply a change to the file without dropping the WireGuard tunnel: &lt;code&gt;wg syncconf wg0 &amp;lt;(wg-quick strip wg0)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: check, in order
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wg show wg0
# peer: &amp;lt;router public key&amp;gt;
# endpoint: 198.51.100.7:13231
# allowed ips: 192.168.10.0/24, 10.99.0.0/24
# latest handshake: 41 seconds ago
# transfer: 1.20 MiB received, 3.45 MiB sent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;latest handshake&lt;/strong&gt; under two minutes: keys and UDP port are right;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;endpoint&lt;/strong&gt; filled in: the server knows the house’s current address;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transfer&lt;/strong&gt; growing both ways: traffic flows.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route get 192.168.10.20 &lt;span class="c"&gt;# must answer "dev wg0"&lt;/span&gt;
ping &lt;span class="nt"&gt;-c3&lt;/span&gt; 10.99.0.2 &lt;span class="c"&gt;# the router in the tunnel&lt;/span&gt;
ping &lt;span class="nt"&gt;-c3&lt;/span&gt; 192.168.10.20 &lt;span class="c"&gt;# a home machine&lt;/span&gt;
ssh admin@192.168.10.20 &lt;span class="c"&gt;# a real TCP connection&lt;/span&gt;
&lt;span class="c"&gt;# from a home machine, the other way:&lt;/span&gt;
ssh root@10.99.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t stop at ping. A firewall can accept ICMP early in its chain and reject new TCP connections further down: ping works, nothing else. Test a real connection, in both directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like on my server
&lt;/h2&gt;

&lt;p&gt;Here is the same check on my web server, captured on 2 November 2026, about an hour after a reboot. Addresses are rewritten to the plan used in this tutorial; everything else is as printed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# wg show wg0
interface: wg0
  public key: (hidden)
  private key: (hidden)
  listening port: 51820

peer: (router public key)
  endpoint: 198.51.100.7:13231
  allowed ips: 192.168.10.0/24, 10.99.0.0/24
  latest handshake: 1 minute, 9 seconds ago
  transfer: 4.01 MiB received, 74.50 MiB sent
  persistent keepalive: every 25 seconds

# ip route get 192.168.10.20
192.168.10.20 dev wg0 src 10.99.0.3 uid 0

# ping -c3 -q 10.99.0.2
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 8.060/8.424/8.976/0.396 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;latest handshake: 1 minute, 9 seconds ago&lt;/strong&gt; : WireGuard renews the session key every two minutes as long as traffic flows, so anything under about two and a half minutes is normal. A value in hours, or no line at all, means the two sides no longer agree on keys or the UDP port is blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transfer: 4.01 MiB received, 74.50 MiB sent&lt;/strong&gt; : the asymmetry is expected here, the server pushes backups and metrics towards home. What matters is that both counters grow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;persistent keepalive: every 25 seconds&lt;/strong&gt; : the setting from the [Peer] section, applied. Without it, the router’s NAT forgets the flow after a few minutes of silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8.4 ms&lt;/strong&gt; round trip between a datacentre and a home fibre line: the tunnel itself adds well under a millisecond, the rest is the distance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more tests worth keeping in a notebook. The first measures the largest packet the WireGuard tunnel accepts without fragmentation, which is how you confirm the MTU of 1420:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ping -c2 -M do -s 1392 192.168.10.20 # 1392 + 28 header bytes = 1420
2 packets transmitted, 2 received, 0% packet loss, time 1001ms

# ping -c1 -M do -s 1393 192.168.10.20 # one byte more
ping: sendmsg: Message too long
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-M do&lt;/code&gt; forbids fragmentation, &lt;code&gt;-s&lt;/code&gt; sets the payload size, and ICMP plus IP add 28 bytes. 1420 passes, 1421 is refused locally with &lt;em&gt;Message too long&lt;/em&gt;: the interface MTU is exactly what the configuration asked for. If large transfers freeze while small ones work, this is the first test to run, from both ends.&lt;/p&gt;

&lt;p&gt;The second is a real TCP connection, because ping alone proves nothing about the firewall: an SSH session from the server to a home machine through the WireGuard tunnel opened in 0.27 s, banner included.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most common failures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No handshake
&lt;/h3&gt;

&lt;p&gt;The UDP port is closed (at the provider or in firewalld), the public keys are swapped, or the router has the wrong &lt;code&gt;Endpoint&lt;/code&gt; address. &lt;code&gt;tcpdump -ni eth0 udp port 51820&lt;/code&gt; on the server shows whether the router’s packets arrive: if they arrive with no answer, it’s a key; if they don’t arrive, it’s the network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handshake, but no traffic
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AllowedIPs&lt;/code&gt; don’t match on both sides: the router sends packets whose source isn’t in the server’s list, and the server silently drops them. Or the return route is missing at home.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ping works, TCP doesn’t
&lt;/h3&gt;

&lt;p&gt;A firewall rule treats ICMP separately. The typical case is a “bogons” address list containing &lt;code&gt;10.0.0.0/8&lt;/code&gt; applied to every interface: it drops the WireGuard tunnel network in the forward chain. Restrict it to the Internet interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small exchanges work, large transfers freeze
&lt;/h3&gt;

&lt;p&gt;An MTU problem. Full-size packets exceed the allowed size somewhere on the path and are lost. Test with &lt;code&gt;ping -M do -s 1372 192.168.10.20&lt;/code&gt;, increasing the size until it fails, then lower the tunnel’s &lt;code&gt;MTU&lt;/code&gt; accordingly on both sides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything works, then stops after a while
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PersistentKeepalive&lt;/code&gt; is missing on the router side, and the box forgot the address translation. Or the house’s public address changed and the server hasn’t learned it yet: it’s up to the router to restart the handshake, which the keepalive does on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Home machine names don’t resolve
&lt;/h3&gt;

&lt;p&gt;The tunnel carries addresses, not names. To resolve &lt;code&gt;nas.home.lan&lt;/code&gt; from the server, send only that domain to the home DNS server, for example with NetworkManager’s dnsmasq plugin, rather than replacing the server’s whole DNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintaining the WireGuard tunnel
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotating keys without losing access&lt;/strong&gt; : first add the server’s new public key as a second peer on the router, switch the server to its new key, check the handshake, then remove the old peer. Never change both sides blindly from outside;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding a second server&lt;/strong&gt; : new tunnel address (&lt;code&gt;10.99.0.4&lt;/code&gt;), new peer on the router with &lt;code&gt;allowed-address=10.99.0.4/32&lt;/code&gt;. Each address must appear in a single peer only, or routing becomes unpredictable;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; : the age of the latest handshake is the best indicator. Beyond three minutes, the tunnel is down. A script that reads &lt;code&gt;wg show wg0 latest-handshakes&lt;/code&gt; and publishes the value for Prometheus, or a simple probe from home, is enough;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing a router remotely&lt;/strong&gt; : turn on its safe mode (Safe Mode on RouterOS) before any change. If the session is lost, the router undoes everything done since.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;The same tunnel can carry monitoring (Prometheus querying the server’s exporters), backups (rsync to an account jailed in a NAS directory) and administration (SSH limited to the tunnel interface). Once the tunnel is in place, many ports you might have been tempted to open to the Internet no longer need to be.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series of sysadmin tutorials (tag “how-to”).&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Series “How-to”, tutorial 5/77. · &lt;a href="https://hecate.neurozone.fr/?p=1485" rel="noopener noreferrer"&gt;← Previous tutorial&lt;/a&gt; · &lt;a href="https://hecate.neurozone.fr/?p=596" rel="noopener noreferrer"&gt;Next tutorial →&lt;/a&gt; · &lt;a href="https://blog.neurozone.fr/?p=2043" rel="noopener noreferrer"&gt;Version française&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=2815" rel="noopener noreferrer"&gt;A home server that never came back&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=147" rel="noopener noreferrer"&gt;Home server housekeeping on Rocky Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=102" rel="noopener noreferrer"&gt;Protecting servers with CrowdSec&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post &lt;a href="https://hecate.neurozone.fr/2026/11/01/wireguard-tunnel-rocky-linux-server-to-home-network/" rel="noopener noreferrer"&gt;How-to: install and configure WireGuard between a server and home&lt;/a&gt; appeared first on &lt;a href="https://hecate.neurozone.fr" rel="noopener noreferrer"&gt;Hécate Regnier - Neurozone&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>sysadminnotebook</category>
      <category>english</category>
      <category>howto</category>
    </item>
    <item>
      <title>PHP2Shell: understanding the October 2026 PHP flaws and checking your servers</title>
      <dc:creator>Hécate R.</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/hecate-regnier/php2shell-understanding-the-october-2026-php-flaws-and-checking-your-servers-15k8</link>
      <guid>https://dev.to/hecate-regnier/php2shell-understanding-the-october-2026-php-flaws-and-checking-your-servers-15k8</guid>
      <description>&lt;p&gt;PHP2Shell is the name given to two PHP vulnerabilities published on 21 October 2026, CVE-2026-109618 and CVE-2026-109619. They allow, with a simple HTTP request and no authentication, running code on a server that runs PHP. With PHP powering about 70% of websites whose server technology is known (WordPress, Drupal, Joomla and a host of applications), the reach is considerable: government sites, Wikimedia and the Internet Archive were taken offline as a precaution, and some experts compared it with Heartbleed.&lt;/p&gt;

&lt;p&gt;This post explains what these flaws are, without going into exploitation details, starting from how PHP manages a request’s memory. It then describes how to check a server is really protected, which takes a little more than a version number, and what I did on my own servers. The facts come from the PHP project’s security advisories and the Wikipedia page about the affair.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hecate.neurozone.fr/wp-content/uploads/2026/11/php2shell-understanding-the-october-2026-php-flaws-and-check_diagram_en.webp" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu4e5yyqjl6fcby8v5h45.webp" alt="PHP2Shell timeline from 17 to 23 October 2026 and four checks: version, restart, every copy, changelog" width="800" height="419"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Exploited before publication: four checks that go beyond the version number.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The PHP2Shell facts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CVE-2026-109618&lt;/th&gt;
&lt;th&gt;CVE-2026-109619&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;advisory title&lt;/td&gt;
&lt;td&gt;“RCE via OOM corruption”&lt;/td&gt;
&lt;td&gt;“RCE via stale global state”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nature&lt;/td&gt;
&lt;td&gt;memory corruption on out-of-memory during request cleanup&lt;/td&gt;
&lt;td&gt;request-owned internal state surviving after its memory was freed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 3.1&lt;/td&gt;
&lt;td&gt;9.8 (no authentication, no interaction)&lt;/td&gt;
&lt;td&gt;8.1 (high attack complexity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 4.0&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;affected versions&lt;/td&gt;
&lt;td&gt;PHP 5.4 to 8.5&lt;/td&gt;
&lt;td&gt;PHP 5.4 to 8.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fixed in&lt;/td&gt;
&lt;td&gt;8.2.35, 8.3.36, 8.4.27, 8.5.12&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date (2026)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;August&lt;/td&gt;
&lt;td&gt;discovery by two teams, helped by AI-assisted analysis tools; one of the bugs had been spotted in a 2024 audit, left unfixed for lack of agreement on the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17 October&lt;/td&gt;
&lt;td&gt;first exploitation attempt seen by a honeypot, four days before publication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 October&lt;/td&gt;
&lt;td&gt;public announcement, leaks (a demo video released by mistake), fixes already visible at one distributor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21 October, 13:34 UTC&lt;/td&gt;
&lt;td&gt;advisories and fixed versions published; exploitation observed the same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;23 October&lt;/td&gt;
&lt;td&gt;follow-up advisory CVE-2026-115560, tied to session upload progress&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two elements of this timeline deserve attention. The flaw was exploited &lt;strong&gt;before&lt;/strong&gt; it was published: when the fix comes out, there’s no grace period any more. And the first bug had been spotted two years earlier, left unfixed for lack of a demonstration that it was exploitable: the arrival of tools able to build that demonstration changed everything. Expect this scenario to repeat with other old “theoretical” bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding: the life of a request in PHP
&lt;/h2&gt;

&lt;p&gt;To grasp what “corruption on out-of-memory” and “stale global state” mean, you need to know how PHP handles a request, especially with PHP-FPM, the most common mode behind nginx.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;reused processes&lt;/strong&gt; : PHP-FPM starts a group of processes (workers), each handling one request after another, hundreds or thousands of times before being recycled (&lt;code&gt;pm.max_requests&lt;/code&gt;). It isn’t a fresh process per request;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a four-step cycle&lt;/strong&gt; : when the process starts, PHP initialises its extensions once and for all (the &lt;em&gt;MINIT&lt;/em&gt; phase). Then for each request: request-specific initialisation (&lt;em&gt;RINIT&lt;/em&gt;), script execution, then cleanup (&lt;em&gt;RSHUTDOWN&lt;/em&gt;), which must return the process to a clean state for the next request;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;per-request memory&lt;/strong&gt; : PHP’s memory manager (the Zend Memory Manager) allocates the script’s memory in an area owned by the request, freed in one go at the end. It’s fast and avoids most leaks, but it assumes nothing keeps, beyond the request, a pointer into that area;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a memory limit&lt;/strong&gt; : &lt;code&gt;memory_limit&lt;/code&gt; caps a request’s memory. When a script exceeds it, PHP interrupts execution… in the middle of whatever it was doing, cleanup included.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The two bugs, at this level of explanation
&lt;/h3&gt;

&lt;p&gt;The first bug concerns precisely that case: running out of memory &lt;em&gt;during&lt;/em&gt; a request’s cleanup leaves some internal structures in an inconsistent, half-freed state. An attacker who knows how to trigger that exhaustion at the right moment, with a request built for it, gets a corruption of the process’s memory.&lt;/p&gt;

&lt;p&gt;The second bug belongs to the same family: a piece of internal state belonging to a request isn’t reset and survives the release of its memory. The next request, handled by the same process, finds a reference to an area that was freed then reused, possibly filled by the attacker. It’s a use-after-free, a classic in programs written in C.&lt;/p&gt;

&lt;p&gt;In both PHP2Shell bugs, controlled memory corruption makes it possible to hijack the PHP process’s execution flow, hence to run code with its rights: those of the web server user, who can read the sites’ files, their database credentials, and often write into their directories. The name “PHP2Shell” sums up the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why every mode is affected
&lt;/h3&gt;

&lt;p&gt;Both bugs are in PHP’s core, not in a particular module. PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, LiteSpeed: everything that serves web requests is concerned. The command line is less so, since no remote attacker sends it requests. And the fact that FPM processes are reused from one request to the next is precisely what makes state that “survives” a request exploitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a web application firewall isn’t enough
&lt;/h2&gt;

&lt;p&gt;Web application firewall vendors published temporary rules within the first days. They have their use against published exploitation tools, but a memory corruption flaw triggered by the shape of a request lends itself poorly to filtering: varying the payload is often enough. PHP’s maintainers said it plainly: no workaround is complete, only updating protects. The US National Security Agency went as far as recommending taking offline the sites that couldn’t be updated immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a server: the version isn’t enough
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="c"&gt;# the command-line binary&lt;/span&gt;
rpm &lt;span class="nt"&gt;-qa&lt;/span&gt; &lt;span class="s1"&gt;'php*-fpm'&lt;/span&gt; &lt;span class="s1"&gt;'php*-common'&lt;/span&gt; &lt;span class="nt"&gt;--qf&lt;/span&gt; &lt;span class="s1"&gt;'%{NAME} %{VERSION} installed %{INSTALLTIME:date}\n'&lt;/span&gt;
systemctl show &lt;span class="nt"&gt;-p&lt;/span&gt; ActiveEnterTimestamp php-fpm &lt;span class="c"&gt;# did PHP-FPM restart AFTER the update?&lt;/span&gt;
needs-restarting &lt;span class="nt"&gt;-s&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; php &lt;span class="c"&gt;# is a process still using old libraries?&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /opt/remi/&lt;span class="k"&gt;*&lt;/span&gt;/root/usr/sbin/php-fpm 2&amp;gt;/dev/null &lt;span class="c"&gt;# parallel versions (software collections)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the installed version&lt;/strong&gt; must be at least 8.2.35, 8.3.36, 8.4.27 or 8.5.12. For older versions, no longer maintained by the project, only source patches exist; distributions and some vendors (cPanel, CloudLinux) packaged them;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the running process&lt;/strong&gt; must have restarted after the update. A PHP-FPM started before installation keeps running the old code in memory, whatever package is on disk. &lt;code&gt;needs-restarting -s&lt;/code&gt; lists the services concerned;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;every copy of PHP&lt;/strong&gt; : a server can have several (the main package, parallel collections in &lt;code&gt;/opt/remi&lt;/code&gt;, containers bundling their own PHP). Each must be checked;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;backported numbers&lt;/strong&gt; : on a distribution that fixes without changing version (RHEL and derivatives for their stock PHP), it’s the package’s changelog you must read (&lt;code&gt;rpm -q --changelog&lt;/code&gt;), not the version number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I found on my servers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# main
php-fpm 8.5.12 installed 21 Oct 2026 at 21:10 (advisory published at 15:34 Paris time)
# homelab
php-fpm 8.4.27 installed 21 Oct 2026 at 20:30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My two servers running PHP use packages from the Remi repository, which tracks official releases closely: the fixed versions were installed the very evening of publication, within five to six hours. One server’s PHP-FPM process had restarted since, and so had the other’s, but &lt;code&gt;needs-restarting&lt;/code&gt; still flagged it for a library updated afterwards: one more restart settled that. It’s the check most often forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  The follow-up flaw: upload progress
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; 99-local.ini : disable session upload progress (CVE-2026-115560)
&lt;/span&gt;&lt;span class="py"&gt;session.upload_progress.enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 23 October, the PHP team published an advisory on a third flaw, CVE-2026-115560, distinct from the first two: a configuration-dependent code execution tied to the session upload progress tracking feature. When it’s on (the default), PHP writes into the session, during an upload, information partly coming from the request. If no application uses it, disabling it costs nothing. I checked no PHP code on my sites used the feature (searching for &lt;code&gt;PHP_SESSION_UPLOAD_PROGRESS&lt;/code&gt; and &lt;code&gt;upload_progress&lt;/code&gt;), then disabled it through the common configuration deployed by Ansible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting an attempt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; php-fpm &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"-7 days"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"exited on signal (11|6)|SIGSEGV|SIGABRT"&lt;/span&gt;
&lt;span class="c"&gt;# WARNING: [pool www] child 12345 exited on signal 11 (SIGSEGV) after 3.2 seconds from start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed exploitation attempt against memory corruption often shows up as a crash of the targeted process. PHP-FPM processes dying on signal 11 (segmentation fault) or 6 (abort), especially in a series from the same address in the web server logs, are a signal to watch. An alert rule on those messages, in a tool like Wazuh, costs little.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;update speed matters more than ever&lt;/strong&gt; : exploitation before publication, exploit tools within days. A package repository that tracks upstream releases closely, and automatic security updates, make the difference;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;an update only applies when the process using the code restarts&lt;/strong&gt;. Check it, don’t assume it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;end-of-life versions&lt;/strong&gt; (PHP 8.1 and older) only got source patches. A server on an old version means compiling it yourself at the worst possible moment;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reduce what’s exposed&lt;/strong&gt; : an unused feature (upload progress) disabled is one flaw fewer on the day it’s discovered;&lt;/li&gt;
&lt;li&gt;and a deeper shift: AI-assisted analysis tools find and demonstrate old flaws faster than before. “Never exploited” bugs may not stay that way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Sources: PHP project security advisories (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp); Wikipedia page “PHP2Shell”; Help Net Security and Recorded Future, October 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series of sysadmin tutorials (tag “how-to”).&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Series “How-to”, tutorial 1/77. · &lt;a href="https://hecate.neurozone.fr/?p=1441" rel="noopener noreferrer"&gt;Next tutorial →&lt;/a&gt; · &lt;a href="https://blog.neurozone.fr/?p=2885" rel="noopener noreferrer"&gt;Version française&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=1441" rel="noopener noreferrer"&gt;The Brotli flaw CVE-2026-111497&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=1484" rel="noopener noreferrer"&gt;Node.js October 2026 flaws&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hecate.neurozone.fr/?p=1531" rel="noopener noreferrer"&gt;How these flaws work under the hood&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The post &lt;a href="https://hecate.neurozone.fr/2026/10/30/php2shell-understanding-the-october-2026-php-flaws-and-checking-your-servers/" rel="noopener noreferrer"&gt;PHP2Shell: understanding the October 2026 PHP flaws and checking your servers&lt;/a&gt; appeared first on &lt;a href="https://hecate.neurozone.fr" rel="noopener noreferrer"&gt;Hécate Regnier - Neurozone&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>sysadminnotebook</category>
      <category>cve</category>
      <category>english</category>
    </item>
  </channel>
</rss>
